Add "attributes" adapter for Apigee.

This sets attributes from the API key. A list checker can then
be used to reject invalid keys. Attributes are also now pushed
to analytics.

Fix CI test for goimports.
diff --git a/adapter/BUILD b/adapter/BUILD
index 66aabee..e6120d7 100644
--- a/adapter/BUILD
+++ b/adapter/BUILD
@@ -6,8 +6,10 @@
     name = "go_default_library",
     srcs = [
         "apigee.go",
+        "apigeeKeyAttributes.go",
         "apigeeKeyChecker.go",
         "apigeeReport.go",
+        "applications.go",
     ],
     deps = [
         "//adapter/config:go_default_library",
@@ -21,9 +23,10 @@
     name = "tests",
     size = "medium",
     srcs = [
-      "apigeeKeyChecker_test.go",
-      "apigeeReport_test.go",
-     ],
+        "apigeeKeyAttributes_test.go",
+        "apigeeKeyChecker_test.go",
+        "apigeeReport_test.go",
+    ],
     library = ":go_default_library",
     deps = [
         "//mock:go_default_library",
diff --git a/adapter/apigee.go b/adapter/apigee.go
index 6fe3853..a1f7b75 100644
--- a/adapter/apigee.go
+++ b/adapter/apigee.go
@@ -27,4 +27,5 @@
 	r.RegisterListsBuilder(newKeyCheckBuilder())
 	//r.RegisterApplicationLogsBuilder(newReportBuilder())
 	r.RegisterAccessLogsBuilder(newReportBuilder())
+	r.RegisterAttributesGeneratorBuilder(newKeyAttrsBuilder())
 }
diff --git a/adapter/apigeeKeyAttributes.go b/adapter/apigeeKeyAttributes.go
new file mode 100644
index 0000000..448bb93
--- /dev/null
+++ b/adapter/apigeeKeyAttributes.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 (
+	"fmt"
+
+	"github.com/apid/istioApigeeAdapter/adapter/config"
+	"istio.io/mixer/pkg/adapter"
+)
+
+const (
+	keyAttrsName = "apigeeKeyAttributes"
+	keyAttrsDesc = "Set attributes based on an Apigee API key"
+
+	keyParam           = "apiKey"
+	pathParam          = "requestPath"
+	successParam       = "success"
+	successStringParam = "successString"
+	clientIDParam      = "clientID"
+	appNameParam       = "applicationName"
+)
+
+var keyAttrsConf = &config.VerifyKeyParams{}
+
+type keyAttrsBuilder struct {
+	adapter.DefaultBuilder
+}
+
+type keyAttrsGenerator struct {
+	env          adapter.Env
+	applications *applicationManager
+}
+
+func newKeyAttrsBuilder() adapter.AttributesGeneratorBuilder {
+	return keyAttrsBuilder{
+		adapter.NewDefaultBuilder(keyAttrsName, keyAttrsDesc, keyAttrsConf),
+	}
+}
+
+func (b keyAttrsBuilder) ValidateConfig(c adapter.Config) *adapter.ConfigErrors {
+	return validateKeyParams(c)
+}
+
+func (b keyAttrsBuilder) BuildAttributesGenerator(env adapter.Env, c adapter.Config) (adapter.AttributesGenerator, error) {
+	cfg := c.(*config.VerifyKeyParams)
+	g := &keyAttrsGenerator{
+		env:          env,
+		applications: newApplicationManager(env, defaultAppLifetime, getVerifyPath(cfg)),
+	}
+	env.Logger().Infof("Created Apigee attributes generator to invoke \"%s\"", g.applications.checkURL)
+
+	return g, nil
+}
+
+func (g *keyAttrsGenerator) Generate(in map[string]interface{}) (map[string]interface{}, error) {
+	key := getString(in, keyParam)
+	if key == "" {
+		return nil, fmt.Errorf("Cannot verify API key: value of \"%s\" not found", keyParam)
+	}
+	path := getString(in, pathParam)
+	if path == "" {
+		return nil, fmt.Errorf("Cannot verify API key: value of \"%s\" not found", pathParam)
+	}
+
+	app, err := g.applications.get(key)
+	if err != nil {
+		g.env.Logger().Errorf("Error verifying API key: %s", err)
+		return nil, err
+	}
+
+	out := make(map[string]interface{})
+	out[successParam] = app.valid
+	out[successStringParam] = fmt.Sprintf("%v", app.valid)
+	if app.valid {
+		out[clientIDParam] = app.clientID
+		out[appNameParam] = app.name
+	}
+	return out, nil
+}
+
+func (g *keyAttrsGenerator) Close() error {
+	return nil
+}
diff --git a/adapter/apigeeKeyAttributes_test.go b/adapter/apigeeKeyAttributes_test.go
new file mode 100644
index 0000000..0176917
--- /dev/null
+++ b/adapter/apigeeKeyAttributes_test.go
@@ -0,0 +1,100 @@
+/*
+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 (
+	"testing"
+
+	"github.com/apid/istioApigeeAdapter/adapter/config"
+	"github.com/apid/istioApigeeAdapter/mock"
+)
+
+func TestInvalidKeyAttributes(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] = "99999"
+	in[pathParam] = "/"
+
+	out, err := ag.Generate(in)
+	if err != nil {
+		t.Fatalf("Error checking parameters: %s", err)
+	}
+	if out[successParam] != false {
+		t.Fatalf("Expected success to be false")
+	}
+	if out[successStringParam] != "false" {
+		t.Fatalf("Expected success string to be false")
+	}
+}
+
+func TestValidKeyAttributes(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.ValidAPIKey1
+	in[pathParam] = "/"
+
+	out, err := ag.Generate(in)
+	if err != nil {
+		t.Fatalf("Error checking parameters: %s", err)
+	}
+	if out[successParam] != true {
+		t.Fatalf("Expected success to be true")
+	}
+	if out[successStringParam] != "true" {
+		t.Fatalf("Expected success string to be true")
+	}
+	if out[clientIDParam] != mock.ValidAPIKey1 {
+		t.Fatalf("Invalid client ID %s", out[clientIDParam])
+	}
+	if out[appNameParam] != "TestApp1" {
+		t.Fatalf("Invalid application name %s", out[appNameParam])
+	}
+}
diff --git a/adapter/apigeeKeyChecker.go b/adapter/apigeeKeyChecker.go
index 38e3dff..50f61ac 100644
--- a/adapter/apigeeKeyChecker.go
+++ b/adapter/apigeeKeyChecker.go
@@ -17,14 +17,10 @@
 package adapter
 
 import (
-	"bytes"
-	"encoding/json"
 	"fmt"
-	"net/http"
 	"net/url"
 
 	"github.com/apid/istioApigeeAdapter/adapter/config"
-	"github.com/apid/istioApigeeAdapter/common"
 	"istio.io/mixer/pkg/adapter"
 )
 
@@ -42,8 +38,8 @@
 }
 
 type keyChecker struct {
-	env      adapter.Env
-	checkURL string
+	env         adapter.Env
+	application *applicationManager
 }
 
 func newKeyCheckBuilder() adapter.ListsBuilder {
@@ -52,7 +48,11 @@
 	}
 }
 
-func (b keyCheckBuilder) ValidateConfig(c adapter.Config) (ce *adapter.ConfigErrors) {
+func (b keyCheckBuilder) ValidateConfig(c adapter.Config) *adapter.ConfigErrors {
+	return validateKeyParams(c)
+}
+
+func validateKeyParams(c adapter.Config) (ce *adapter.ConfigErrors) {
 	cfg := c.(*config.VerifyKeyParams)
 	if cfg.Organization == "" {
 		ce = ce.Appendf("organization", "Organization parameter must be specified")
@@ -71,6 +71,16 @@
 
 func (b keyCheckBuilder) NewListsAspect(env adapter.Env, c adapter.Config) (adapter.ListsAspect, error) {
 	cfg := c.(*config.VerifyKeyParams)
+	kc := &keyChecker{
+		env:         env,
+		application: newApplicationManager(env, defaultAppLifetime, getVerifyPath(cfg)),
+	}
+	env.Logger().Infof("Created Apigee Key Checker to invoke \"%s\"", kc.application.checkURL)
+
+	return kc, nil
+}
+
+func getVerifyPath(cfg *config.VerifyKeyParams) string {
 	var basePath string
 
 	if cfg.VerificationURL == "" {
@@ -78,14 +88,7 @@
 	} else {
 		basePath = cfg.VerificationURL
 	}
-
-	kc := &keyChecker{
-		env:      env,
-		checkURL: basePath + verifyKeyPath,
-	}
-	env.Logger().Infof("Created Apigee Key Checker to invoke \"%s\"", kc.checkURL)
-
-	return kc, nil
+	return basePath + verifyKeyPath
 }
 
 func (l *keyChecker) Close() error {
@@ -98,22 +101,10 @@
 // Test command ./bazel-bin/cmd/client/mixc check -a target.service=f.default.svc.cluster.local --stringmap_attributes request.headers=x-api-key:1tu9pl04Srua2MtsAGtu6ViPxSYSSX2I
 
 func (l *keyChecker) CheckList(symbol string) (bool, error) {
-	apiKeyBody := common.VerifyAPIKeyRequest{
-		Key: symbol,
-	}
-	requestBody, _ := json.Marshal(&apiKeyBody)
-
-	resp, err := http.DefaultClient.Post(l.checkURL, "application/json",
-		bytes.NewBuffer(requestBody))
+	app, err := l.application.get(symbol)
 	if err != nil {
-		l.env.Logger().Errorf("Error contacting verification service: %s", err)
+		l.env.Logger().Errorf("Error verifying API key: %s", err)
 		return false, err
 	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode != 200 {
-		return false, nil
-	} else {
-		return true, nil
-	}
+	return app.valid, nil
 }
diff --git a/adapter/apigeeReport.go b/adapter/apigeeReport.go
index fc2ae2f..16cfefa 100644
--- a/adapter/apigeeReport.go
+++ b/adapter/apigeeReport.go
@@ -36,6 +36,8 @@
 const (
 	regionLookupURL     = "https://edgemicroservices.apigee.net/edgemicro"
 	nanosPerMillisecond = 1000000
+	reportName          = "apigeeReport"
+	reportDesc          = "Report API call data to Apigee"
 )
 
 type (
@@ -58,8 +60,8 @@
 func newReportBuilder() analyticsBuilder {
 	return analyticsBuilder{
 		adapter.NewDefaultBuilder(
-			"apigeeReport",
-			"Report logs to apigee",
+			reportName,
+			reportDesc,
 			&config.ReportParams{},
 		)}
 }
@@ -121,17 +123,11 @@
 	var records []common.AnalyticsRecord
 
 	for _, entry := range entries {
-		sourceIP := getStringLabel(entry, "sourceIP")
 		path := getStringLabel(entry, "urlPath")
 		host := getStringLabel(entry, "hostHeader")
-		method := getStringLabel(entry, "httpMethod")
-		userAgent := getStringLabel(entry, "userAgent")
-		proxyName := getStringLabel(entry, "proxyName")
-		proxyRevision := getIntLabel(entry, "proxyRevision")
 		// Convert nanos (Go) to millis (Java)
 		requestTime := getTimestampLabel(entry, "requestTime") / nanosPerMillisecond
 		responseTime := getTimestampLabel(entry, "responseTime") / nanosPerMillisecond
-		responseCode := getIntLabel(entry, "responseCode")
 
 		r := common.AnalyticsRecord{
 			ClientReceivedStartTimestamp: requestTime,
@@ -140,16 +136,18 @@
 			ClientSentStartTimestamp:   responseTime,
 			ClientSentEndTimestamp:     responseTime + 1,
 			// Missing: Target times
-			ClientIP:           sourceIP,
-			RequestVerb:        method,
-			UserAgent:          userAgent,
-			ResponseStatusCode: responseCode,
+			ClientIP:           getStringLabel(entry, "sourceIP"),
+			RequestVerb:        getStringLabel(entry, "httpMethod"),
+			UserAgent:          getStringLabel(entry, "userAgent"),
+			ResponseStatusCode: getIntLabel(entry, "responseCode"),
 			// Technically wrong because of no scheme and host header
 			RequestURI:       "http://" + host + path,
 			RequestPath:      strings.Split(path, "?")[0],
-			APIProxy:         proxyName,
-			APIProxyRevision: proxyRevision,
+			APIProxy:         getStringLabel(entry, "proxyName"),
+			APIProxyRevision: getIntLabel(entry, "proxyRevision"),
 			RecordType:       "APIAnalytics",
+			DeveloperApp:     getStringLabel(entry, "applicationName"),
+			ClientID:         getStringLabel(entry, "clientID"),
 		}
 
 		records = append(records, r)
@@ -266,7 +264,11 @@
 }
 
 func getStringLabel(le adapter.LogEntry, key string) string {
-	v := le.Labels[key]
+	return getString(le.Labels, key)
+}
+
+func getString(m map[string]interface{}, key string) string {
+	v := m[key]
 	if v == nil {
 		return ""
 	}
@@ -279,7 +281,11 @@
 }
 
 func getIntLabel(le adapter.LogEntry, key string) int {
-	v := le.Labels[key]
+	return getInt(le.Labels, key)
+}
+
+func getInt(m map[string]interface{}, key string) int {
+	v := m[key]
 	if v == nil {
 		return 0
 	}
@@ -294,7 +300,11 @@
 }
 
 func getTimestampLabel(le adapter.LogEntry, key string) int64 {
-	v := le.Labels[key]
+	return getTimestamp(le.Labels, key)
+}
+
+func getTimestamp(m map[string]interface{}, key string) int64 {
+	v := m[key]
 	if v == nil {
 		return 0
 	}
diff --git a/adapter/applications.go b/adapter/applications.go
new file mode 100644
index 0000000..1088725
--- /dev/null
+++ b/adapter/applications.go
@@ -0,0 +1,164 @@
+/*
+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 (
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/apid/istioApigeeAdapter/common"
+	"istio.io/mixer/pkg/adapter"
+)
+
+const (
+	defaultAppLifetime = 5 * time.Second
+)
+
+type application struct {
+	clientID    string
+	valid       bool
+	expiration  time.Time
+	name        string
+	apiProducts []string
+}
+
+type applicationJWT struct {
+	ClientID        string   `json:"client_id"`
+	ApplicationName string   `json:"application_name"`
+	APIProducts     []string `json:"api_product_list"`
+}
+
+type applicationManager struct {
+	env      adapter.Env
+	lifetime time.Duration
+	cache    map[string]*application
+	checkURL string
+	latch    *sync.Mutex
+}
+
+func newApplicationManager(env adapter.Env, lifetime time.Duration, checkURL string) *applicationManager {
+	return &applicationManager{
+		env:      env,
+		lifetime: lifetime,
+		checkURL: checkURL,
+		cache:    make(map[string]*application),
+		latch:    &sync.Mutex{},
+	}
+}
+
+func (a *applicationManager) get(key string) (*application, error) {
+	app := a.cacheGet(key)
+	if app != nil {
+		return app, nil
+	}
+
+	app, err := a.lookupKey(key)
+	if err != nil {
+		return nil, err
+	}
+	a.cachePut(key, app)
+	return app, nil
+}
+
+func (a *applicationManager) cacheGet(key string) *application {
+	a.latch.Lock()
+	app := a.cache[key]
+	if app != nil && app.expiration.Before(time.Now()) {
+		delete(a.cache, key)
+		app = nil
+	}
+	a.latch.Unlock()
+	return app
+}
+
+func (a *applicationManager) cachePut(key string, app *application) {
+	a.latch.Lock()
+	a.cache[key] = app
+	a.latch.Unlock()
+}
+
+func (a *applicationManager) lookupKey(key string) (*application, error) {
+	apiKeyBody := common.VerifyAPIKeyRequest{
+		Key: key,
+	}
+	requestBody, _ := json.Marshal(&apiKeyBody)
+
+	resp, err := http.DefaultClient.Post(a.checkURL, "application/json",
+		bytes.NewBuffer(requestBody))
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode == 200 {
+		return a.parseApplication(resp.Body)
+
+	} else if resp.StatusCode == 401 {
+		return &application{
+			clientID:   key,
+			valid:      false,
+			expiration: time.Now().Add(a.lifetime),
+		}, nil
+
+	} else {
+		return nil, fmt.Errorf("HTTP error looking up application: %d", resp.StatusCode)
+	}
+}
+
+func (a *applicationManager) parseApplication(r io.Reader) (*application, error) {
+	var keyResponse common.VerifyAPIKeyResponse
+	jr := json.NewDecoder(r)
+	err := jr.Decode(&keyResponse)
+	if err != nil {
+		return nil, fmt.Errorf("Invalid JSON in API key response: %s", err)
+	}
+
+	// We got back a JWT, but we don't need to verify it -- just parse the JSON we need
+	parts := strings.Split(keyResponse.Token, ".")
+	if len(parts) != 3 {
+		return nil, errors.New("Invalid JWT returned")
+	}
+	// base64 from Apigee is unpadded, so use "raw" encoding here.
+	rawJWT, err := base64.RawStdEncoding.DecodeString(parts[1])
+	if err != nil {
+		return nil, fmt.Errorf("Error decoding JWT base 64: from \"%s\", %s", keyResponse.Token, err)
+	}
+
+	var jwt applicationJWT
+	err = json.Unmarshal(rawJWT, &jwt)
+	if err != nil {
+		return nil, fmt.Errorf("Error decoding JWT json: %s", err)
+	}
+
+	a.env.Logger().Infof("Looked up data for application %s", jwt.ApplicationName)
+
+	return &application{
+		clientID:    jwt.ClientID,
+		valid:       true,
+		expiration:  time.Now().Add(a.lifetime),
+		name:        jwt.ApplicationName,
+		apiProducts: jwt.APIProducts,
+	}, nil
+}
diff --git a/mock/mockserver.go b/mock/mockserver.go
index 984d5e0..99bbe77 100644
--- a/mock/mockserver.go
+++ b/mock/mockserver.go
@@ -142,7 +142,8 @@
 	}
 
 	if request.Key != ValidAPIKey1 {
-		sendFault(w, 404,
+		// We actually do return 401 in this case!
+		sendFault(w, 401,
 			fmt.Sprintf("API key %s is not valid", request.Key),
 			"steps.oauth.v2.FailedToResolveAPIKey")
 		return
diff --git a/mock/mockserver_test.go b/mock/mockserver_test.go
index e37ee26..49f3207 100644
--- a/mock/mockserver_test.go
+++ b/mock/mockserver_test.go
@@ -151,8 +151,8 @@
 		t.Fatalf("Network error: %s", err)
 	}
 	defer resp.Body.Close()
-	if resp.StatusCode != 404 {
-		t.Fatalf("Got HTTP status code %d -- expected 404", resp.StatusCode)
+	if resp.StatusCode != 401 {
+		t.Fatalf("Got HTTP status code %d -- expected 401", resp.StatusCode)
 	}
 }
 
@@ -169,8 +169,8 @@
 		t.Fatalf("Network error: %s", err)
 	}
 	defer resp.Body.Close()
-	if resp.StatusCode != 404 {
-		t.Fatalf("Got HTTP status code %d -- expected 404", resp.StatusCode)
+	if resp.StatusCode != 401 {
+		t.Fatalf("Got HTTP status code %d -- expected 401", resp.StatusCode)
 	}
 }
 
diff --git a/testdata/configroot/scopes/global/adapters.yml b/testdata/configroot/scopes/global/adapters.yml
index cd4639f..5079e36 100644
--- a/testdata/configroot/scopes/global/adapters.yml
+++ b/testdata/configroot/scopes/global/adapters.yml
@@ -18,7 +18,7 @@
     kind: lists
     impl: apigeeKeyChecker
     params:
-      organization: YOUR_ORG_HERE
+      organization: the-brails
       environment: prod
       # Add verificationURL to use a different service or URL
       #verificationURL: https://YOUR_ORG_HERE-prod.apigee.net/edgemicro-auth
@@ -36,7 +36,24 @@
     kind: access-logs
     impl: apigeeReport
     params:
-      organization: YOUR_ORG_HERE
+      organization: the-brails
       environment: prod
-      key: KEY_FROM_EDGEMICRO_CONFIGURE
-      secret: SECRET_FROM_EDGEMICRO_CONFIGURE
+      # the-brails
+      key: da46c48b8987b395d1b0cce228974372c2046a4f052c9d941a06f68e1cd5acc3
+      secret: 2b31064d4f65f3cc8cc42ef29a3742ccbfc75caf53d05bc70c6d1023560597fa
+      # gregbrail
+      #key: cf9b8f76a0cb29232f4a73bef86f97859e67642746a3dcce3efd1f34d773f544
+      #secret: f6326522114cb4041212279fb2863278280a137142bbef35f386c4f767a0e627
+      #collectionURL: https://edgemicroservices-us-east-1.apigee.net/edgemicro
+  - name: apigeeAttributes
+    kind: attributes
+    impl: apigeeKeyAttributes
+    params:
+      organization: the-brails
+      environment: prod
+  - name: authorizationChecker
+    kind: lists
+    impl: genericListChecker
+    params:
+      listEntries:
+      - "true"
diff --git a/testdata/configroot/scopes/global/descriptors.yml b/testdata/configroot/scopes/global/descriptors.yml
index d66e343..c1aed42 100644
--- a/testdata/configroot/scopes/global/descriptors.yml
+++ b/testdata/configroot/scopes/global/descriptors.yml
@@ -79,9 +79,32 @@
         valueType: STRING
       proxy.revision:
         valueType: INT64
+      authorization.success:
+        valueType: BOOL
+      authorization.success.string:
+        valueType: STRING
+      authorization.client.id:
+        valueType: STRING
+      authorization.application.name:
+        valueType: STRING
       # DEPRECATED, to be removed. Use request.useragent instead.
       request.user-agent:
         valueType: STRING
+  - name: apigeeKeyAttributes
+    revision: "1"
+    attributes:
+      apiKey:
+        valueType: STRING
+      requestPath:
+        valueType: STRING
+      success:
+        valueType: BOOL
+      successString:
+        valueType: STRING
+      clientID:
+        valueType: STRING
+      applicationName:
+        valueType: STRING
 # Enums as struct fields can be symbolic names.
 # However enums inside maps *cannot* be symbolic names.
 metrics:
@@ -207,4 +230,6 @@
       responseCode: 2
       proxyName: 1
       proxyRevision: 2
+      clientID: 1
+      applicationName: 1
 
diff --git a/testdata/configroot/scopes/global/subjects/global/rules.yml b/testdata/configroot/scopes/global/subjects/global/rules.yml
index 32bae3f..5545c95 100644
--- a/testdata/configroot/scopes/global/subjects/global/rules.yml
+++ b/testdata/configroot/scopes/global/subjects/global/rules.yml
@@ -3,10 +3,31 @@
 rules:
 - selector: # must be empty for preprocessing adapters
   aspects:
-  - kind: lists
-    adapter: apigeeAPIKey
+  # Fetch the API key and use it to set various request attributes.
+  # This is where the adapter should cache, because this one gets called three times
+  # (check, quota, and report)
+  - kind: attributes
+    adapter: apigeeAttributes
     params:
-      checkExpression: request.headers["apikey"]
+      input_expressions:
+        apiKey: request.headers["apikey"] | "INVALID_KEY"
+        requestPath: request.path | "/"
+      attribute_bindings:
+        authorization.success.string: successString
+        authorization.success: success
+        authorization.client.id: clientID
+        authorization.application.name: applicationName
+  # 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
+    adapter: authorizationChecker
+    params:
+      checkExpression: authorization.success.string
+  # This is a simpler way to achieve the above!
+  #- kind: lists
+  #  adapter: apigeeAPIKey
+  #  params:
+  #    checkExpression: request.headers["apikey"]
   - kind: quotas
     params:
       quotas:
@@ -44,3 +65,5 @@
           responseCode: response.code | 200
           proxyName: proxy.name | "istio"
           proxyRevision: proxy.revision | 1
+          clientID: authorization.client.id | ""
+          applicationName: authorization.application.name | ""
diff --git a/tools/checkfmt.sh b/tools/checkfmt.sh
index c668975..4d0e581 100755
--- a/tools/checkfmt.sh
+++ b/tools/checkfmt.sh
@@ -1,15 +1,28 @@
 #!/bin/bash
 
+CHECKER=goimports
+which goimports > /dev/null
+if [ $? -ne 0 ]
+then
+  which gofmt > /dev/null
+  if [ $? -ne 0 ]
+  then
+    echo 'Neither goimports nor gofmt could be found'
+    exit 2
+  fi
+  CHECKER=gofmt
+fi
+
 failed=0
 
 lc=`ls *.go 2>/dev/null | wc -l`
 if [ $lc -gt 0 ]
 then
-  lc=`goimports -l . | wc -l`
+  lc=`${CHECKER} -l . | wc -l`
   if [ $lc -gt  0 ]
   then
-    echo "** goimports run required:"
-    gofmt -l .
+    echo "** ${CHECKER} run required:"
+    ${CHECKER} -l .
     failed=1
   fi