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