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 | ""