Add API product data to api key validation.

We are not yet checking the path -- we're simply accepting the
first API product that we see.
We are also not yet populating the attributes, but we will do that
soon.
diff --git a/adapter/BUILD b/adapter/BUILD
index e6120d7..68a50d4 100644
--- a/adapter/BUILD
+++ b/adapter/BUILD
@@ -10,6 +10,7 @@
         "apigeeKeyChecker.go",
         "apigeeReport.go",
         "applications.go",
+        "products.go",
     ],
     deps = [
         "//adapter/config:go_default_library",
diff --git a/adapter/apigeeKeyAttributes.go b/adapter/apigeeKeyAttributes.go
index 448bb93..db93ed5 100644
--- a/adapter/apigeeKeyAttributes.go
+++ b/adapter/apigeeKeyAttributes.go
@@ -18,6 +18,7 @@
 
 import (
 	"fmt"
+	"strconv"
 
 	"github.com/apid/istioApigeeAdapter/adapter/config"
 	"istio.io/mixer/pkg/adapter"
@@ -33,6 +34,7 @@
 	successStringParam = "successString"
 	clientIDParam      = "clientID"
 	appNameParam       = "applicationName"
+	productNameParam   = "apiProduct"
 )
 
 var keyAttrsConf = &config.VerifyKeyParams{}
@@ -44,6 +46,7 @@
 type keyAttrsGenerator struct {
 	env          adapter.Env
 	applications *applicationManager
+	products     *productManager
 }
 
 func newKeyAttrsBuilder() adapter.AttributesGeneratorBuilder {
@@ -58,11 +61,14 @@
 
 func (b keyAttrsBuilder) BuildAttributesGenerator(env adapter.Env, c adapter.Config) (adapter.AttributesGenerator, error) {
 	cfg := c.(*config.VerifyKeyParams)
+	verifyPath, productsPath := getPaths(cfg)
 	g := &keyAttrsGenerator{
 		env:          env,
-		applications: newApplicationManager(env, defaultAppLifetime, getVerifyPath(cfg)),
+		applications: newApplicationManager(env, defaultAppLifetime, verifyPath),
+		products:     newProductManager(productsPath, defaultProductsFetch),
 	}
-	env.Logger().Infof("Created Apigee attributes generator to invoke \"%s\"", g.applications.checkURL)
+	env.Logger().Infof("Created Apigee attributes generator to invoke \"%s\"", verifyPath)
+	env.Logger().Infof("Checking API products using \"%s\"", productsPath)
 
 	return g, nil
 }
@@ -77,19 +83,35 @@
 		return nil, fmt.Errorf("Cannot verify API key: value of \"%s\" not found", pathParam)
 	}
 
+	// Look up API key from cache, making HTTP request if necessary
 	app, err := g.applications.get(key)
 	if err != nil {
 		g.env.Logger().Errorf("Error verifying API key: %s", err)
 		return nil, err
 	}
 
+	success := app.valid
+
+	// Look up API products from cache, making HTTP request if necessary
+	products, err := g.products.getProducts(app.apiProducts)
+	if err != nil {
+		return nil, fmt.Errorf("Cannot fetch API product list: %s", err)
+	}
+	if len(products) == 0 {
+		success = false
+	}
+	// TODO match API products by path
+
 	out := make(map[string]interface{})
-	out[successParam] = app.valid
-	out[successStringParam] = fmt.Sprintf("%v", app.valid)
+	out[successParam] = success
+	out[successStringParam] = strconv.FormatBool(success)
 	if app.valid {
 		out[clientIDParam] = app.clientID
 		out[appNameParam] = app.name
 	}
+	if success {
+		out[productNameParam] = products[0].Name
+	}
 	return out, nil
 }
 
diff --git a/adapter/apigeeKeyAttributes_test.go b/adapter/apigeeKeyAttributes_test.go
index 0176917..b2b5883 100644
--- a/adapter/apigeeKeyAttributes_test.go
+++ b/adapter/apigeeKeyAttributes_test.go
@@ -51,10 +51,10 @@
 		t.Fatalf("Error checking parameters: %s", err)
 	}
 	if out[successParam] != false {
-		t.Fatalf("Expected success to be false")
+		t.Fatal("Expected success to be false")
 	}
 	if out[successStringParam] != "false" {
-		t.Fatalf("Expected success string to be false")
+		t.Fatal("Expected success string to be false")
 	}
 }
 
@@ -86,10 +86,10 @@
 		t.Fatalf("Error checking parameters: %s", err)
 	}
 	if out[successParam] != true {
-		t.Fatalf("Expected success to be true")
+		t.Fatal("Expected success to be true")
 	}
 	if out[successStringParam] != "true" {
-		t.Fatalf("Expected success string to be true")
+		t.Fatal("Expected success string to be true")
 	}
 	if out[clientIDParam] != mock.ValidAPIKey1 {
 		t.Fatalf("Invalid client ID %s", out[clientIDParam])
@@ -97,4 +97,48 @@
 	if out[appNameParam] != "TestApp1" {
 		t.Fatalf("Invalid application name %s", out[appNameParam])
 	}
+	if out[productNameParam] != "TestProduct1" {
+		t.Fatalf("Invalid API product name %s", out[productNameParam])
+	}
+}
+
+func TestValidKeyNoProducts(t *testing.T) {
+	cfg := &config.VerifyKeyParams{
+		Organization:    "foo",
+		Environment:     "test",
+		VerificationURL: "http://" + mockServer.Address(),
+	}
+
+	builder := newKeyAttrsBuilder()
+	ce := builder.ValidateConfig(cfg)
+	if ce != nil {
+		t.Fatalf("Error validating config: %s", ce)
+	}
+
+	ag, err := builder.BuildAttributesGenerator(mockEnv, cfg)
+	if err != nil {
+		t.Fatalf("Error creating aspect: %s", err)
+	}
+	defer ag.Close()
+
+	in := make(map[string]interface{})
+	in[keyParam] = mock.ValidAPIKeyNoProducts
+	in[pathParam] = "/"
+
+	out, err := ag.Generate(in)
+	if err != nil {
+		t.Fatalf("Error checking parameters: %s", err)
+	}
+	if out[successParam] != false {
+		t.Fatal("Expected success to be false")
+	}
+	if out[successStringParam] != "false" {
+		t.Fatal("Expected success string to be false")
+	}
+	if out[clientIDParam] != mock.ValidAPIKeyNoProducts {
+		t.Fatalf("Invalid client ID %s", out[clientIDParam])
+	}
+	if out[appNameParam] != "TestApp2" {
+		t.Fatalf("Invalid application name %s", out[appNameParam])
+	}
 }
diff --git a/adapter/apigeeKeyChecker.go b/adapter/apigeeKeyChecker.go
index 50f61ac..eb7d135 100644
--- a/adapter/apigeeKeyChecker.go
+++ b/adapter/apigeeKeyChecker.go
@@ -28,6 +28,7 @@
 	checkName        = "apigeeKeyChecker"
 	checkDesc        = "Verify an API key from a parameter"
 	verifyKeyPath    = "/verifyApiKey"
+	productsPath     = "/products"
 	defaultVerifyURL = "https://%s-%s.apigee.net/edgemicro-auth"
 )
 
@@ -71,16 +72,17 @@
 
 func (b keyCheckBuilder) NewListsAspect(env adapter.Env, c adapter.Config) (adapter.ListsAspect, error) {
 	cfg := c.(*config.VerifyKeyParams)
+	verifyPath, _ := getPaths(cfg)
 	kc := &keyChecker{
 		env:         env,
-		application: newApplicationManager(env, defaultAppLifetime, getVerifyPath(cfg)),
+		application: newApplicationManager(env, defaultAppLifetime, verifyPath),
 	}
 	env.Logger().Infof("Created Apigee Key Checker to invoke \"%s\"", kc.application.checkURL)
 
 	return kc, nil
 }
 
-func getVerifyPath(cfg *config.VerifyKeyParams) string {
+func getPaths(cfg *config.VerifyKeyParams) (string, string) {
 	var basePath string
 
 	if cfg.VerificationURL == "" {
@@ -88,7 +90,7 @@
 	} else {
 		basePath = cfg.VerificationURL
 	}
-	return basePath + verifyKeyPath
+	return basePath + verifyKeyPath, basePath + productsPath
 }
 
 func (l *keyChecker) Close() error {
diff --git a/adapter/apigeeReport.go b/adapter/apigeeReport.go
index 16cfefa..6551dfc 100644
--- a/adapter/apigeeReport.go
+++ b/adapter/apigeeReport.go
@@ -148,6 +148,7 @@
 			RecordType:       "APIAnalytics",
 			DeveloperApp:     getStringLabel(entry, "applicationName"),
 			ClientID:         getStringLabel(entry, "clientID"),
+			APIProduct:       getStringLabel(entry, "apiProduct"),
 		}
 
 		records = append(records, r)
diff --git a/adapter/products.go b/adapter/products.go
new file mode 100644
index 0000000..c890fd4
--- /dev/null
+++ b/adapter/products.go
@@ -0,0 +1,98 @@
+/*
+Copyright 2017 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package adapter
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/apid/istioApigeeAdapter/common"
+)
+
+const (
+	defaultProductsFetch = time.Minute
+)
+
+type productManager struct {
+	fetchURL   string
+	products   map[string]*common.APIProduct
+	refresh    time.Duration
+	expiration time.Time
+	latch      *sync.Mutex
+}
+
+func newProductManager(fetchURL string, refreshTime time.Duration) *productManager {
+	return &productManager{
+		fetchURL: fetchURL,
+		refresh:  refreshTime,
+		latch:    &sync.Mutex{},
+	}
+}
+
+/*
+getProducts returns the products that match a list of names. For now it is simple -- we will
+use a mutex and update the list when it's expired. In the future we can be fancier with
+periodic refresh.
+*/
+func (m *productManager) getProducts(names []string) ([]common.APIProduct, error) {
+	m.latch.Lock()
+	defer m.latch.Unlock()
+
+	if m.products == nil || m.expiration.Before(time.Now()) {
+		err := m.fetchProductList()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	var products []common.APIProduct
+	for _, name := range names {
+		p := m.products[name]
+		if p != nil {
+			products = append(products, *p)
+		}
+	}
+	return products, nil
+}
+
+func (m *productManager) fetchProductList() error {
+	resp, err := http.Get(m.fetchURL)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode != 200 {
+		return fmt.Errorf("HTTP error %d fetching API product list", resp.StatusCode)
+	}
+
+	var pl common.APIProductResponse
+	jr := json.NewDecoder(resp.Body)
+	err = jr.Decode(&pl)
+	if err != nil {
+		return err
+	}
+
+	m.products = make(map[string]*common.APIProduct)
+	for _, p := range pl.Products {
+		m.products[p.Name] = &p
+	}
+	m.expiration = time.Now().Add(m.refresh)
+	return nil
+}
diff --git a/common/types.go b/common/types.go
index e4bff48..1f9ee43 100644
--- a/common/types.go
+++ b/common/types.go
@@ -87,6 +87,10 @@
 	Scopes         []string    `json:"scopes"`
 }
 
+type APIProductResponse struct {
+	Products []APIProduct `json:"apiProduct"`
+}
+
 type APIFaultMessage struct {
 	Fault APIFault `json:"fault"`
 }
diff --git a/mock/mockserver.go b/mock/mockserver.go
index 99bbe77..8fd5ff7 100644
--- a/mock/mockserver.go
+++ b/mock/mockserver.go
@@ -35,9 +35,10 @@
 var mockInit = &sync.Once{}
 
 const (
-	ValidAPIKey1       = "12345"
-	ValidPublishKey    = "aaaaaa"
-	ValidPublishSecret = "bbbbbb"
+	ValidAPIKey1          = "12345"
+	ValidAPIKeyNoProducts = "23456"
+	ValidPublishKey       = "aaaaaa"
+	ValidPublishSecret    = "bbbbbb"
 )
 
 type MockServer struct {
@@ -92,7 +93,7 @@
 	now := time.Now()
 	p := []common.APIProduct{
 		{
-			Name:           "First",
+			Name:           "TestProduct1",
 			DisplayName:    "First Product",
 			Description:    "First test product",
 			CreatedAt:      now.UnixNano(),
@@ -115,11 +116,14 @@
 			QuotaTimeUnit: "minute",
 		},
 	}
+	pl := common.APIProductResponse{
+		Products: p,
+	}
 
 	w.Header().Set("content-type", "application/json")
 
 	enc := json.NewEncoder(w)
-	enc.Encode(p)
+	enc.Encode(pl)
 }
 
 func (m *MockServer) getAPIKey(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
@@ -141,7 +145,14 @@
 		return
 	}
 
-	if request.Key != ValidAPIKey1 {
+	var returnedJwt jwt.JWT
+
+	switch request.Key {
+	case ValidAPIKey1:
+		returnedJwt = makeJWT1()
+	case ValidAPIKeyNoProducts:
+		returnedJwt = makeJWT2()
+	default:
 		// We actually do return 401 in this case!
 		sendFault(w, 401,
 			fmt.Sprintf("API key %s is not valid", request.Key),
@@ -149,8 +160,7 @@
 		return
 	}
 
-	jwt := makeJWT1()
-	bod, err := jwt.Serialize(mockKey)
+	bod, err := returnedJwt.Serialize(mockKey)
 	if err != nil {
 		sendFault(w, 500, err.Error(), "JWT")
 		return
@@ -259,3 +269,20 @@
 
 	return jws.NewJWT(c, crypto.SigningMethodRS256)
 }
+
+func makeJWT2() jwt.JWT {
+	now := time.Now()
+	c := jws.Claims{}
+	c.SetIssuedAt(now)
+	c.SetNotBefore(now)
+	c.Set("audience", "microgateway")
+	c.Set("jti", "52137037-6ce1-426e-a255-e471f94854e5")
+	c.Set("iss", "https://mock.foo.net/verifyApiKey")
+	c.Set("client_id", ValidAPIKeyNoProducts)
+	c.Set("application_name", "TestApp2")
+	c.Set("api_product_list", []string{
+		"UnknownProduct1",
+	})
+
+	return jws.NewJWT(c, crypto.SigningMethodRS256)
+}
diff --git a/mock/mockserver_test.go b/mock/mockserver_test.go
index 49f3207..4c384ae 100644
--- a/mock/mockserver_test.go
+++ b/mock/mockserver_test.go
@@ -103,7 +103,7 @@
 	}
 
 	dec := json.NewDecoder(resp.Body)
-	var products []common.APIProduct
+	var products common.APIProductResponse
 	err = dec.Decode(&products)
 	if err != nil {
 		t.Fatalf("Error decoding response json: %s", err)
diff --git a/testdata/configroot/scopes/global/descriptors.yml b/testdata/configroot/scopes/global/descriptors.yml
index c1aed42..7e29fba 100644
--- a/testdata/configroot/scopes/global/descriptors.yml
+++ b/testdata/configroot/scopes/global/descriptors.yml
@@ -87,6 +87,8 @@
         valueType: STRING
       authorization.application.name:
         valueType: STRING
+      authorization.apiproduct.name:
+        valueType: STRING
       # DEPRECATED, to be removed. Use request.useragent instead.
       request.user-agent:
         valueType: STRING
@@ -105,6 +107,8 @@
         valueType: STRING
       applicationName:
         valueType: STRING
+      apiProduct:
+        valueType: STRING
 # Enums as struct fields can be symbolic names.
 # However enums inside maps *cannot* be symbolic names.
 metrics:
@@ -232,4 +236,4 @@
       proxyRevision: 2
       clientID: 1
       applicationName: 1
-
+      apiProduct: 1
diff --git a/testdata/configroot/scopes/global/subjects/global/rules.yml b/testdata/configroot/scopes/global/subjects/global/rules.yml
index 5545c95..9bd10df 100644
--- a/testdata/configroot/scopes/global/subjects/global/rules.yml
+++ b/testdata/configroot/scopes/global/subjects/global/rules.yml
@@ -17,6 +17,7 @@
         authorization.success: success
         authorization.client.id: clientID
         authorization.application.name: applicationName
+        authorization.apiproduct.name: apiProduct
   # In "check" reject the request if the key is invalid. It'd be nice if we could
   # customize the error here. We could build yet another adapter for that purpose!
   - kind: lists
@@ -67,3 +68,4 @@
           proxyRevision: proxy.revision | 1
           clientID: authorization.client.id | ""
           applicationName: authorization.application.name | ""
+          apiProduct: authorization.apiproduct.name | ""