├── .circleci
└── config.yml
├── .codecov.yml
├── .gitignore
├── .goreleaser.yaml
├── CONTRIBUTING.md
├── Gopkg.lock
├── Gopkg.toml
├── LICENSE
├── README.md
├── RELEASING.md
├── adapter
├── adapter.go
├── adapter_test.go
├── analytics
│ ├── bucket.go
│ ├── bucket_test.go
│ ├── hybrid_analytics_test.go
│ ├── hybrid_uploader.go
│ ├── legacy_analytics.go
│ ├── legacy_analytics_test.go
│ ├── manager.go
│ ├── manager_test.go
│ ├── record.go
│ ├── record_test.go
│ ├── recovery.go
│ ├── recovery_test.go
│ ├── saas_analytics_test.go
│ ├── saas_uploader.go
│ ├── staging.go
│ ├── staging_test.go
│ └── testdata
│ │ ├── README.md
│ │ ├── cert.pem
│ │ └── key.pem
├── auth
│ ├── auth.go
│ ├── auth_test.go
│ ├── context.go
│ ├── context_test.go
│ ├── jwt.go
│ ├── jwt_test.go
│ ├── structs.go
│ ├── verify_api_key.go
│ └── verify_api_key_test.go
├── authtest
│ └── context.go
├── config
│ ├── apigee.yaml
│ ├── config.pb.go
│ ├── config.pb.html
│ ├── config.proto
│ └── config.proto_descriptor
├── context
│ └── context.go
├── grpc_adapter.go
├── grpc_adapter_test.go
├── integration_test.go
├── product
│ ├── manager.go
│ ├── manager_test.go
│ ├── products.go
│ ├── products_test.go
│ └── structs.go
├── quota
│ ├── bucket.go
│ ├── bucket_test.go
│ ├── manager.go
│ ├── manager_test.go
│ ├── result_cache.go
│ ├── result_cache_test.go
│ └── structs.go
└── util
│ ├── atomic.go
│ ├── atomic_test.go
│ ├── backoff.go
│ ├── backoff_test.go
│ ├── looper.go
│ ├── looper_test.go
│ ├── reservoir.go
│ ├── reservoir_test.go
│ ├── util.go
│ └── util_test.go
├── apigee-istio
├── apigee
│ ├── edge_client.go
│ ├── kvm.go
│ ├── proxies_service.go
│ ├── revision.go
│ └── timestamp.go
├── cmd
│ ├── bindings
│ │ └── bindings.go
│ ├── provision
│ │ └── provision.go
│ ├── root.go
│ └── token
│ │ └── token.go
├── main.go
├── proxies
│ └── proxies.go
└── shared
│ └── shared.go
├── bin
├── build_adapter_docker.sh
├── build_grpc_definitions.sh
├── build_proxy_resources.sh
├── build_release.sh
├── codegen.sh
├── install_docker.sh
├── install_gcloud.sh
├── install_protoc.sh
└── lint_and_vet.sh
├── grpc-server
├── Dockerfile
├── Dockerfile_debug
├── README.md
├── ca-certificates.crt
├── grpc_health_probe
└── main.go
├── proxies
├── auth-proxy-hybrid
│ └── apiproxy
│ │ ├── istio-auth.xml
│ │ ├── policies
│ │ ├── Access-App-Info.xml
│ │ ├── AccessTokenRequest.xml
│ │ ├── Clear-API-Key.xml
│ │ ├── Create-OAuth-Request.xml
│ │ ├── Create-Refresh-Request.xml
│ │ ├── Decode-Basic-Authentication.xml
│ │ ├── DistributedQuota.xml
│ │ ├── Eval-Quota-Result.xml
│ │ ├── Extract-API-Key.xml
│ │ ├── Extract-OAuth-Params.xml
│ │ ├── Extract-Refresh-Params.xml
│ │ ├── Extract-Revoke-Params.xml
│ │ ├── Extract-Rotate-Variables.xml
│ │ ├── Generate-Access-Token.xml
│ │ ├── Generate-JWK.xml
│ │ ├── Generate-VerifyKey-Token.xml
│ │ ├── Get-Private-Key.xml
│ │ ├── Get-Public-Keys.xml
│ │ ├── JavaCallout.xml
│ │ ├── Products-to-JSON.xml
│ │ ├── Raise-Fault-Unknown-Request.xml
│ │ ├── RefreshAccessToken.xml
│ │ ├── Retrieve-Cert.xml
│ │ ├── RevokeRefreshToken.xml
│ │ ├── Send-JWK-Message.xml
│ │ ├── Send-Version.xml
│ │ ├── Set-JWT-Variables.xml
│ │ ├── Set-Quota-Response.xml
│ │ ├── Set-Quota-Variables.xml
│ │ ├── Set-Response.xml
│ │ ├── Update-Keys.xml
│ │ └── Verify-API-Key.xml
│ │ ├── proxies
│ │ └── default.xml
│ │ └── resources
│ │ ├── java
│ │ └── istio-products-javacallout-2.0.0.jar
│ │ └── jsc
│ │ ├── eval-quota-result.js
│ │ ├── generate-jwk.js
│ │ ├── jsrsasign-all-min.js
│ │ ├── jwt-initialization.js
│ │ ├── send-jwk-response.js
│ │ ├── set-jwt-variables.js
│ │ ├── set-quota-variables.js
│ │ └── set-response.js
├── auth-proxy-legacy
│ ├── README.md
│ └── apiproxy
│ │ ├── istio-auth.xml
│ │ ├── manifests
│ │ └── manifest.xml
│ │ ├── policies
│ │ ├── Access-App-Info-2.xml
│ │ ├── Access-App-Info.xml
│ │ ├── AccessTokenRequest.xml
│ │ ├── Authenticate-Call.xml
│ │ ├── AuthenticationError.xml
│ │ ├── Create-OAuth-Request.xml
│ │ ├── Create-Refresh-Request.xml
│ │ ├── DistributedQuota.xml
│ │ ├── Eval-Quota-Result.xml
│ │ ├── Extract-API-Key.xml
│ │ ├── Extract-OAuth-Params.xml
│ │ ├── Extract-Refresh-Params.xml
│ │ ├── Extract-Revoke-Params.xml
│ │ ├── Extract-Rotate-Variables.xml
│ │ ├── Generate-Access-Token.xml
│ │ ├── Generate-JWK.xml
│ │ ├── Generate-VerifyKey-Token.xml
│ │ ├── Get-Private-Key.xml
│ │ ├── Get-Public-Keys.xml
│ │ ├── JavaCallout.xml
│ │ ├── Products-to-JSON-2.xml
│ │ ├── Products-to-JSON.xml
│ │ ├── Raise-Fault-Unknown-Request.xml
│ │ ├── RefreshAccessToken.xml
│ │ ├── Retrieve-Cert.xml
│ │ ├── RevokeRefreshToken.xml
│ │ ├── Send-JWK-Message.xml
│ │ ├── Send-Version.xml
│ │ ├── Set-JWT-Variables.xml
│ │ ├── Set-Quota-Response.xml
│ │ ├── Set-Quota-Variables.xml
│ │ ├── Set-Response.xml
│ │ ├── Update-Keys.xml
│ │ └── Verify-API-Key.xml
│ │ ├── proxies
│ │ └── default.xml
│ │ └── resources
│ │ ├── java
│ │ └── istio-products-javacallout-2.0.0.jar
│ │ └── jsc
│ │ ├── eval-quota-result.js
│ │ ├── generate-jwk.js
│ │ ├── jsrsasign-all-min.js
│ │ ├── jwt-initialization.js
│ │ ├── send-jwk-response.js
│ │ ├── set-jwt-variables.js
│ │ ├── set-quota-variables.js
│ │ └── set-response.js
└── internal-proxy
│ ├── README.md
│ └── apiproxy
│ ├── EdgeMicro.xml
│ ├── policies
│ ├── Authenticate.xml
│ ├── Callout.xml
│ ├── DistributedQuota.xml
│ ├── JSSetupVariables.xml
│ ├── NoOrgOrEnv.xml
│ ├── Return401.xml
│ ├── Return404.xml
│ ├── ReturnVersion.xml
│ └── SetQuotaResponse.xml
│ ├── proxies
│ └── default.xml
│ └── resources
│ ├── java
│ └── edge-micro-javacallout-1.0.0.jar
│ └── jsc
│ └── JSSetupVariables.js
├── samples
└── apigee
│ ├── adapter.yaml
│ ├── authentication-policy.yaml
│ ├── definitions.yaml
│ ├── handler.yaml
│ └── rule.yaml
└── template
└── analytics
├── analytics.pb.html
├── template.proto
├── template.yaml
└── template_handler.gen.go
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Golang CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-go/ for more details
4 | version: 2
5 | jobs:
6 | build-and-test:
7 | docker:
8 | - image: circleci/golang:1.13
9 | working_directory: /go/src/github.com/apigee/istio-mixer-adapter
10 | steps:
11 | - checkout
12 | - run: dep ensure
13 | - run:
14 | command: ./bin/install_protoc.sh
15 | name: install protoc
16 | - run:
17 | command: go generate ./...
18 | name: generate protos
19 | - run:
20 | command: go test -coverprofile=coverage.txt ./...
21 | name: Run tests
22 | - run:
23 | command: bash <(curl -s https://codecov.io/bash)
24 | name: upload codecov
25 | - run:
26 | command: DRYRUN=1 ./bin/build_release.sh
27 | - store_artifacts:
28 | path: /go/src/github.com/apigee/istio-mixer-adapter/dist
29 | destination: dist
30 |
31 | build-test-image:
32 | docker:
33 | - image: circleci/golang:1.13
34 | working_directory: /go/src/github.com/apigee/istio-mixer-adapter
35 | steps:
36 | - checkout
37 | - run: dep ensure
38 | - run:
39 | command: ./bin/install_protoc.sh
40 | name: install protoc
41 | - run:
42 | command: go generate ./...
43 | name: generate protos
44 | - run:
45 | name: Run tests
46 | command: go test -coverprofile=coverage.txt ./...
47 | - run:
48 | name: upload codecov
49 | command: bash <(curl -s https://codecov.io/bash)
50 | - setup_remote_docker
51 | - run:
52 | name: Install docker
53 | command: ./bin/install_docker.sh
54 | - run:
55 | name: Install gcloud
56 | command: ./bin/install_gcloud.sh
57 | - run:
58 | name: Build and push apigee-adapter Docker image with tag latest
59 | command: TAG=${CIRCLE_TAG:-nightly} GCP_PROJECT=apigee-api-management-istio MAKE_PUBLIC=1 DEBUG=1 TARGET_DOCKER_IMAGE=gcr.io/apigee-api-management-istio/apigee-adapter:$TAG TARGET_DOCKER_DEBUG_IMAGE=gcr.io/apigee-api-management-istio/apigee-adapter-debug:$TAG ./bin/build_adapter_docker.sh
60 | - run:
61 | command: DRYRUN=1 ./bin/build_release.sh
62 | - store_artifacts:
63 | path: /go/src/github.com/apigee/istio-mixer-adapter/dist
64 | destination: dist
65 |
66 | workflows:
67 | version: 2
68 | on-commit:
69 | jobs:
70 | - build-test-image:
71 | filters:
72 | branches:
73 | only: master
74 | - build-and-test:
75 | filters:
76 | branches:
77 | ignore: master
78 | nightly:
79 | triggers:
80 | - schedule:
81 | cron: "0 7 * * *"
82 | filters:
83 | branches:
84 | only: master
85 | jobs:
86 | - build-test-image
87 |
88 | on-version-tag:
89 | jobs:
90 | - build-test-image:
91 | filters:
92 | branches:
93 | ignore: /.*/
94 | tags:
95 | ignore: /1\.0\.\d/
96 |
--------------------------------------------------------------------------------
/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | precision: 0
3 | round: up
4 | range: 60..99
5 | ignore:
6 | - "adapter/authtest/"
7 | - "adapter/integrationtest/"
8 | - "bin/"
9 | - "template/"
10 | - "apigee-istio/"
11 | - "context/"
12 | - "auth-proxy/"
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Eclipse artifacts
2 | .project
3 | .pydevproject
4 | # Intellij
5 | *.iml
6 | .idea/
7 | # Visual Studio Code
8 | .vscode/
9 | # Bazel
10 | /bazel-*
11 | vendor
12 | *.descriptor_set
13 | # vi backups
14 | *.bak
15 | # python artifacts
16 | *.pyc
17 | # Vm setup generated files
18 | cluster.env
19 | kubedns
20 | # lint
21 | lintconfig.gen.json
22 | # bazel
23 | bazel-bin
24 | bazel-genfiles
25 | bazel-istio-mixer-adapter
26 | bazel-out
27 | bazel-testlogs
28 | # proto gen
29 | bin/protoc-*
30 | # Vim
31 | *.swp
32 | *.swo
33 |
34 | # Generated proto files
35 | template/analytics/template_handler_service.*
36 | template/analytics/*.pb.go
37 | apigee-istio/apigee-istio
38 |
39 | # dist
40 | dist/
41 | grpc-server/apigee-adapter
42 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # .goreleaser.yml
2 | # Build customization
3 | builds:
4 | - main: ./apigee-istio/main.go
5 | binary: apigee-istio
6 | goos:
7 | - windows
8 | - darwin
9 | - linux
10 | goarch:
11 | - amd64
12 | # Archive customization
13 | archives:
14 | - format: tar.gz
15 | replacements:
16 | darwin: macOS
17 | amd64: 64-bit
18 | files:
19 | - LICENSE
20 | - README.md
21 | - samples/**/*
22 | - install/**/*
23 | release:
24 | draft: true
25 | prerelease: true
26 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to any Google project must be accompanied by a Contributor License
9 | Agreement. This is necessary because you own the copyright to your changes, even
10 | after your contribution becomes part of this project. So this agreement simply
11 | gives us permission to use and redistribute your contributions as part of the
12 | project. Head over to to see your current
13 | agreements on file or to sign a new one.
14 |
15 | You generally only need to submit a CLA once, so if you've already submitted one
16 | (even if it was for a different project), you probably don't need to do it
17 | again.
18 |
19 | ## How to Contribute
20 |
21 | We happily accept pull requests, bugs, and issues here in GitHub.
22 |
23 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
2 | # for detailed Gopkg.toml documentation.
3 |
4 | required = [
5 | "github.com/istio/tools/protoc-gen-docs",
6 | "golang.org/x/tools/imports",
7 | "github.com/google/uuid",
8 | ]
9 |
10 | # Below is for apigee-istio
11 |
12 | [[constraint]]
13 | name = "github.com/spf13/cobra"
14 | version = "0.0.2"
15 |
16 | # Below is for adapter
17 |
18 | [[constraint]]
19 | name = "istio.io/istio"
20 | version = "1.1.5"
21 |
22 | [[constraint]]
23 | name = "github.com/hashicorp/go-multierror"
24 | branch = "master"
25 |
26 | [[override]]
27 | name = "github.com/lestrrat-go/jwx"
28 | revision = "master"
29 |
30 | [[override]]
31 | name = "github.com/gogo/googleapis"
32 | version = "v1.2.0"
33 |
34 | # only used in tests
35 | [[constraint]]
36 | name = "github.com/dgrijalva/jwt-go"
37 | version = "~3.2.0"
38 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Building a new draft release on Github
2 |
3 | 1. set RELEASE env var
4 | (eg. `RELEASE=1.1.2`)
5 |
6 | 2. create a release branch: `git checkout -b $RELEASE-prep`
7 |
8 | 3. make release updates
9 | 1. update README.md to appropriate versions and instructions
10 | 2. update version in `auth-proxy/apiproxy/policies/Send-Version.xml` to match $RELEASE
11 | 3. run `bin/build_proxy_resources.sh`
12 | 4. update image version in `samples/apigee/adapter.yaml` to match $RELEASE
13 |
14 | 4. Commit and push
15 | 1. verify your changes for git: `git status`
16 | 2. add and commit: `git commit -am "prep ${RELEASE}"`
17 | 3. tag the commit: `git tag ${RELEASE}`
18 | 4. push: `git push --set-upstream origin $RELEASE-prep ${RELEASE}`
19 | (CircleCI will automatically build and tag docker image)
20 |
21 | 5. verify the image: gcr.io/apigee-api-management-istio/apigee-adapter:$RELEASE
22 |
23 | 6. `bin/build_release.sh`
24 | (creates a draft release on Github)
25 |
26 | 7. edit Github release:
27 | a. add mixer version and docker image URL to release notes
28 | b. if this is not a pre-release, uncheck `This is a pre-release` checkbox
29 |
30 | 8. submit PR for $RELEASE-prep branch
31 |
32 | 9. merge and final verifications
33 |
34 | 10. publish release on Github
35 |
--------------------------------------------------------------------------------
/adapter/analytics/bucket.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package analytics
16 |
17 | import (
18 | "compress/gzip"
19 | "fmt"
20 | "io"
21 | "io/ioutil"
22 | "os"
23 | "sync"
24 | )
25 |
26 | func newBucket(m *manager, up uploader, tenant, dir string) (*bucket, error) {
27 | b := &bucket{
28 | manager: m,
29 | uploader: up,
30 | tenant: tenant,
31 | dir: dir,
32 | incoming: make(chan []Record, m.sendChannelSize),
33 | }
34 |
35 | var tempFileSpec string
36 | if up.isGzipped() {
37 | tempFileSpec = fmt.Sprintf("%d-*.gz", b.manager.now().Unix())
38 | } else {
39 | tempFileSpec = fmt.Sprintf("%d-*.txt", b.manager.now().Unix())
40 | }
41 |
42 | f, err := ioutil.TempFile(b.dir, tempFileSpec)
43 | if err != nil {
44 | m.log.Errorf("AX Records lost. Can't create bucket file: %s", err)
45 | return nil, err
46 | }
47 | b.w = &fileWriter{
48 | file: f,
49 | writer: f,
50 | }
51 | if up.isGzipped() {
52 | b.w.writer = gzip.NewWriter(f)
53 | }
54 |
55 | m.env.ScheduleDaemon(b.runLoop)
56 | return b, nil
57 | }
58 |
59 | // A bucket writes analytics to a temp file
60 | type bucket struct {
61 | manager *manager
62 | uploader uploader
63 | tenant string
64 | dir string
65 | w *fileWriter
66 | incoming chan []Record
67 | wait *sync.WaitGroup
68 | }
69 |
70 | // write records to bucket
71 | func (b *bucket) write(records []Record) {
72 | if b != nil && len(records) > 0 {
73 | b.incoming <- records
74 | }
75 | }
76 |
77 | // close bucket
78 | func (b *bucket) close(wait *sync.WaitGroup) {
79 | b.wait = wait
80 | close(b.incoming)
81 | }
82 |
83 | func (b *bucket) fileName() string {
84 | return b.w.file.Name()
85 | }
86 |
87 | func (b *bucket) runLoop() {
88 | log := b.manager.log
89 |
90 | for records := range b.incoming {
91 | b.uploader.write(records, b.w.writer)
92 | }
93 |
94 | if err := b.w.close(); err != nil {
95 | log.Errorf("Can't close bucket file: %s", err)
96 | }
97 |
98 | b.manager.stageFile(b.tenant, b.fileName())
99 |
100 | if b.wait != nil {
101 | b.wait.Done()
102 | }
103 | log.Debugf("bucket closed: %s", b.fileName())
104 | }
105 |
106 | type fileWriter struct {
107 | file *os.File
108 | writer io.Writer
109 | }
110 |
111 | func (w *fileWriter) close() error {
112 | if gzw, ok := w.writer.(*gzip.Writer); ok {
113 | if err := gzw.Close(); err != nil {
114 | return fmt.Errorf("gz.Close: %s", err)
115 | }
116 | }
117 |
118 | if err := w.file.Close(); err != nil {
119 | return fmt.Errorf("f.Close: %s", err)
120 | }
121 | return nil
122 | }
123 |
--------------------------------------------------------------------------------
/adapter/analytics/bucket_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package analytics
16 |
17 | import (
18 | "io/ioutil"
19 | "net/http"
20 | "net/url"
21 | "os"
22 | "path/filepath"
23 | "reflect"
24 | "strings"
25 | "sync"
26 | "testing"
27 | "time"
28 |
29 | adaptertest "istio.io/istio/mixer/pkg/adapter/test"
30 | )
31 |
32 | func TestBucket(t *testing.T) {
33 |
34 | testDir, err := ioutil.TempDir("", "")
35 | if err != nil {
36 | t.Fatalf("ioutil.TempDir(): %s", err)
37 | }
38 | defer os.RemoveAll(testDir)
39 |
40 | env := adaptertest.NewEnv(t)
41 | now := time.Now
42 |
43 | uploader := &saasUploader{
44 | log: env.Logger(),
45 | client: http.DefaultClient,
46 | baseURL: &url.URL{},
47 | key: "key",
48 | secret: "secret",
49 | now: now,
50 | }
51 |
52 | opts := Options{
53 | LegacyEndpoint: true,
54 | BufferPath: testDir,
55 | StagingFileLimit: 10,
56 | now: now,
57 | CollectionInterval: time.Minute,
58 | }
59 |
60 | m, err := newManager(uploader, opts)
61 | if err != nil {
62 | t.Fatalf("newManager: %s", err)
63 | }
64 |
65 | tenant := getTenantName("test", "test")
66 | err = m.prepTenant(tenant)
67 | if err != nil {
68 | t.Fatalf("prepTenant: %v", err)
69 | }
70 | tempDir := m.getTempDir(tenant)
71 | stageDir := m.getStagingDir(tenant)
72 |
73 | m.Start(env)
74 | defer m.Close()
75 |
76 | b, err := newBucket(m, uploader, tenant, tempDir)
77 | if err != nil {
78 | t.Fatalf("newBucket: %v", err)
79 | }
80 |
81 | records := []Record{
82 | {
83 | Organization: "test",
84 | Environment: "test",
85 | },
86 | }
87 | b.write(records)
88 |
89 | wait := &sync.WaitGroup{}
90 | wait.Add(1)
91 | b.close(wait)
92 | wait.Wait()
93 |
94 | files, err := ioutil.ReadDir(tempDir)
95 | if err != nil {
96 | t.Errorf("unexpected error %v", err)
97 | }
98 | if len(files) != 0 {
99 | t.Errorf("got %d files, expected %d files: %v", len(files), 0, files)
100 | }
101 |
102 | files, err = ioutil.ReadDir(stageDir)
103 | if err != nil {
104 | t.Errorf("unexpected error %v", err)
105 | }
106 | if len(files) != 1 {
107 | t.Fatalf("got %d files, expected %d files: %v", len(files), 1, files)
108 | }
109 |
110 | if !strings.HasSuffix(files[0].Name(), ".gz") {
111 | t.Errorf("file %s should have .gz suffix", files[0])
112 | }
113 |
114 | stagedFile := filepath.Join(stageDir, files[0].Name())
115 |
116 | recs, err := readRecordsFromGZipFile(stagedFile)
117 | if err != nil {
118 | t.Fatalf("readRecordsFromGZipFile: %v", err)
119 | }
120 |
121 | if !reflect.DeepEqual(records, recs) {
122 | t.Errorf("got: %v, want: %v", recs, records)
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/adapter/analytics/legacy_analytics.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package analytics
16 |
17 | import (
18 | "bytes"
19 | "encoding/json"
20 | "fmt"
21 | "net/http"
22 | "path"
23 |
24 | "github.com/apigee/istio-mixer-adapter/adapter/auth"
25 | "istio.io/istio/mixer/pkg/adapter"
26 | )
27 |
28 | const (
29 | axPath = "/axpublisher/organization/%s/environment/%s"
30 | )
31 |
32 | type legacyAnalytics struct {
33 | client *http.Client
34 | }
35 |
36 | func (oa *legacyAnalytics) Start(env adapter.Env) error { return nil }
37 | func (oa *legacyAnalytics) Close() {}
38 |
39 | func (oa *legacyAnalytics) SendRecords(auth *auth.Context, records []Record) error {
40 | axURL := *auth.ApigeeBase()
41 | axURL.Path = path.Join(axURL.Path, fmt.Sprintf(axPath, auth.Organization(), auth.Environment()))
42 |
43 | request, err := buildRequest(auth, records)
44 | if request == nil || err != nil {
45 | return err
46 | }
47 |
48 | body := new(bytes.Buffer)
49 | json.NewEncoder(body).Encode(request)
50 |
51 | req, err := http.NewRequest(http.MethodPost, axURL.String(), body)
52 | if err != nil {
53 | return err
54 | }
55 |
56 | req.SetBasicAuth(auth.Key(), auth.Secret())
57 | req.Header.Set("Content-Type", "application/json")
58 | req.Header.Set("Accept", "application/json")
59 |
60 | auth.Log().Debugf("sending %d analytics records to: %s", len(records), axURL.String())
61 |
62 | resp, err := oa.client.Do(req)
63 | if err != nil {
64 | return err
65 | }
66 | defer resp.Body.Close()
67 |
68 | buf := bytes.NewBuffer(make([]byte, 0, resp.ContentLength))
69 | _, err = buf.ReadFrom(resp.Body)
70 | if err != nil {
71 | return err
72 | }
73 | respBody := buf.Bytes()
74 |
75 | switch resp.StatusCode {
76 | case 200:
77 | auth.Log().Debugf("analytics accepted: %v", string(respBody))
78 | return nil
79 | default:
80 | return fmt.Errorf("analytics rejected. status: %d, body: %s", resp.StatusCode, string(respBody))
81 | }
82 | }
83 |
84 | func buildRequest(auth *auth.Context, incoming []Record) (*legacyRequest, error) {
85 | if auth == nil || len(incoming) == 0 {
86 | return nil, nil
87 | }
88 | if auth.Organization() == "" || auth.Environment() == "" {
89 | return nil, fmt.Errorf("organization and environment are required in auth: %v", auth)
90 | }
91 |
92 | records := make([]Record, 0, len(incoming))
93 | for _, record := range incoming {
94 | records = append(records, record.ensureFields(auth))
95 | }
96 |
97 | return &legacyRequest{
98 | Organization: auth.Organization(),
99 | Environment: auth.Environment(),
100 | Records: records,
101 | }, nil
102 | }
103 |
104 | type legacyRequest struct {
105 | Organization string `json:"organization"`
106 | Environment string `json:"environment"`
107 | Records []Record `json:"records"`
108 | }
109 |
110 | type legacyResponse struct {
111 | Accepted int `json:"accepted"`
112 | Rejected int `json:"rejected"`
113 | }
114 |
--------------------------------------------------------------------------------
/adapter/analytics/manager_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package analytics
16 |
17 | import (
18 | "net/http"
19 | "net/url"
20 | "testing"
21 | "time"
22 |
23 | adaptertest "istio.io/istio/mixer/pkg/adapter/test"
24 | )
25 |
26 | func TestLegacySelect(t *testing.T) {
27 |
28 | env := adaptertest.NewEnv(t)
29 |
30 | opts := Options{
31 | LegacyEndpoint: true,
32 | BufferPath: "",
33 | StagingFileLimit: 10,
34 | BaseURL: &url.URL{},
35 | Key: "key",
36 | Secret: "secret",
37 | Client: http.DefaultClient,
38 | now: time.Now,
39 | }
40 |
41 | m, err := NewManager(env, opts)
42 | m.Close()
43 | if err != nil {
44 | t.Fatalf("newManager: %s", err)
45 | }
46 |
47 | if _, ok := m.(*legacyAnalytics); !ok {
48 | t.Errorf("want an *legacyAnalytics type, got: %#v", m)
49 | }
50 | }
51 |
52 | func TestStandardSelect(t *testing.T) {
53 |
54 | env := adaptertest.NewEnv(t)
55 |
56 | opts := Options{
57 | BufferPath: "/tmp/apigee-ax/buffer/",
58 | StagingFileLimit: 10,
59 | BaseURL: &url.URL{},
60 | Key: "key",
61 | Secret: "secret",
62 | Client: http.DefaultClient,
63 | now: time.Now,
64 | CollectionInterval: time.Minute,
65 | }
66 |
67 | m, err := NewManager(env, opts)
68 | if err != nil {
69 | t.Fatalf("newManager: %s", err)
70 | }
71 | m.Close()
72 |
73 | if _, ok := m.(*manager); !ok {
74 | t.Errorf("want an *manager type, got: %#v", m)
75 | }
76 | }
77 |
78 | func TestStandardBadOptions(t *testing.T) {
79 |
80 | env := adaptertest.NewEnv(t)
81 |
82 | opts := Options{
83 | BufferPath: "/tmp/apigee-ax/buffer/",
84 | StagingFileLimit: 0,
85 | BaseURL: &url.URL{},
86 | Key: "",
87 | Secret: "",
88 | Client: http.DefaultClient,
89 | now: time.Now,
90 | }
91 |
92 | want := "all analytics options are required"
93 | m, err := NewManager(env, opts)
94 | if err == nil || err.Error() != want {
95 | t.Errorf("want: %s, got: %s", want, err)
96 | }
97 | if m != nil {
98 | t.Errorf("should not get manager")
99 | m.Close()
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/adapter/analytics/record.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package analytics
16 |
17 | import (
18 | "errors"
19 | "time"
20 |
21 | "github.com/apigee/istio-mixer-adapter/adapter/auth"
22 | "github.com/google/uuid"
23 | "github.com/hashicorp/go-multierror"
24 | )
25 |
26 | // A Record is a single event that is tracked via Apigee analytics.
27 | type Record struct {
28 | ClientReceivedStartTimestamp int64 `json:"client_received_start_timestamp"`
29 | ClientReceivedEndTimestamp int64 `json:"client_received_end_timestamp"`
30 | ClientSentStartTimestamp int64 `json:"client_sent_start_timestamp"`
31 | ClientSentEndTimestamp int64 `json:"client_sent_end_timestamp"`
32 | TargetReceivedStartTimestamp int64 `json:"target_received_start_timestamp,omitempty"`
33 | TargetReceivedEndTimestamp int64 `json:"target_received_end_timestamp,omitempty"`
34 | TargetSentStartTimestamp int64 `json:"target_sent_start_timestamp,omitempty"`
35 | TargetSentEndTimestamp int64 `json:"target_sent_end_timestamp,omitempty"`
36 | RecordType string `json:"recordType"`
37 | APIProxy string `json:"apiproxy"`
38 | RequestURI string `json:"request_uri"`
39 | RequestPath string `json:"request_path"`
40 | RequestVerb string `json:"request_verb"`
41 | ClientIP string `json:"client_ip,omitempty"`
42 | UserAgent string `json:"useragent"`
43 | APIProxyRevision int `json:"apiproxy_revision"`
44 | ResponseStatusCode int `json:"response_status_code"`
45 | DeveloperEmail string `json:"developer_email,omitempty"`
46 | DeveloperApp string `json:"developer_app,omitempty"`
47 | AccessToken string `json:"access_token,omitempty"`
48 | ClientID string `json:"client_id,omitempty"`
49 | APIProduct string `json:"api_product,omitempty"`
50 | Organization string `json:"organization"`
51 | Environment string `json:"environment"`
52 | GatewaySource string `json:"gateway_source"`
53 | GatewayFlowID string `json:"gateway_flow_id"`
54 | }
55 |
56 | func (r Record) ensureFields(ctx *auth.Context) Record {
57 | r.RecordType = axRecordType
58 |
59 | // populate from auth context
60 | r.DeveloperEmail = ctx.DeveloperEmail
61 | r.DeveloperApp = ctx.Application
62 | r.AccessToken = ctx.AccessToken
63 | r.ClientID = ctx.ClientID
64 | r.Organization = ctx.Organization()
65 | r.Environment = ctx.Environment()
66 |
67 | r.GatewayFlowID = uuid.New().String()
68 |
69 | // selects best APIProduct based on path, otherwise arbitrary
70 | if len(ctx.APIProducts) > 0 {
71 | r.APIProduct = ctx.APIProducts[0]
72 | }
73 | return r
74 | }
75 |
76 | // validate confirms that a record has correct values in it.
77 | func (r Record) validate(now time.Time) error {
78 | var err error
79 |
80 | // Validate that certain fields are set.
81 | if r.Organization == "" {
82 | err = multierror.Append(err, errors.New("missing Organization"))
83 | }
84 | if r.Environment == "" {
85 | err = multierror.Append(err, errors.New("missing Environment"))
86 | }
87 | if r.GatewayFlowID == "" {
88 | err = multierror.Append(err, errors.New("missing GatewayFlowID"))
89 | }
90 | if r.ClientReceivedStartTimestamp == 0 {
91 | err = multierror.Append(err, errors.New("missing ClientReceivedStartTimestamp"))
92 | }
93 | if r.ClientReceivedEndTimestamp == 0 {
94 | err = multierror.Append(err, errors.New("missing ClientReceivedEndTimestamp"))
95 | }
96 | if r.ClientReceivedStartTimestamp > r.ClientReceivedEndTimestamp {
97 | err = multierror.Append(err, errors.New("ClientReceivedStartTimestamp > ClientReceivedEndTimestamp"))
98 | }
99 |
100 | // Validate that timestamps make sense.
101 | ts := time.Unix(r.ClientReceivedStartTimestamp/1000, 0)
102 | if ts.After(now) {
103 | err = multierror.Append(err, errors.New("ClientReceivedStartTimestamp cannot be in the future"))
104 | }
105 | if ts.Before(now.Add(-90 * 24 * time.Hour)) {
106 | err = multierror.Append(err, errors.New("ClientReceivedStartTimestamp cannot be more than 90 days old"))
107 | }
108 | return err
109 | }
110 |
--------------------------------------------------------------------------------
/adapter/analytics/record_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package analytics
16 |
17 | import (
18 | "strings"
19 | "testing"
20 | "time"
21 | )
22 |
23 | func TestValidationFailure(t *testing.T) {
24 | ts := int64(1521221450) // This timestamp is roughly 11:30 MST on Mar. 16, 2018.
25 | for _, test := range []struct {
26 | desc string
27 | record Record
28 | wantError string
29 | }{
30 | {"good record", Record{
31 | Organization: "hi",
32 | Environment: "test",
33 | ClientReceivedStartTimestamp: ts * 1000,
34 | ClientReceivedEndTimestamp: ts * 1000,
35 | GatewayFlowID: "x",
36 | }, ""},
37 | {"missing org", Record{
38 | Environment: "test",
39 | ClientReceivedStartTimestamp: ts * 1000,
40 | ClientReceivedEndTimestamp: ts * 1000,
41 | GatewayFlowID: "x",
42 | }, "missing Organization"},
43 | {"missing env", Record{
44 | Organization: "hi",
45 | ClientReceivedStartTimestamp: ts * 1000,
46 | ClientReceivedEndTimestamp: ts * 1000,
47 | GatewayFlowID: "x",
48 | }, "missing Environment"},
49 | {"missing start timestamp", Record{
50 | Organization: "hi",
51 | Environment: "test",
52 | ClientReceivedEndTimestamp: ts * 1000,
53 | GatewayFlowID: "x",
54 | }, "missing ClientReceivedStartTimestamp"},
55 | {"missing end timestamp", Record{
56 | Organization: "hi",
57 | Environment: "test",
58 | ClientReceivedStartTimestamp: ts * 1000,
59 | GatewayFlowID: "x",
60 | }, "missing ClientReceivedEndTimestamp"},
61 | {"end < start", Record{
62 | Organization: "hi",
63 | Environment: "test",
64 | ClientReceivedStartTimestamp: ts * 1000,
65 | ClientReceivedEndTimestamp: ts*1000 - 1,
66 | GatewayFlowID: "x",
67 | }, "ClientReceivedStartTimestamp > ClientReceivedEndTimestamp"},
68 | {"in the future", Record{
69 | Organization: "hi",
70 | Environment: "test",
71 | ClientReceivedStartTimestamp: (ts + 1) * 1000,
72 | ClientReceivedEndTimestamp: (ts + 1) * 1000,
73 | GatewayFlowID: "x",
74 | }, "in the future"},
75 | {"too old", Record{
76 | Organization: "hi",
77 | Environment: "test",
78 | ClientReceivedStartTimestamp: (ts - 91*24*3600) * 1000,
79 | ClientReceivedEndTimestamp: (ts - 91*24*3600) * 1000,
80 | GatewayFlowID: "x",
81 | }, "more than 90 days old"},
82 | {"missing GatewayFlowID", Record{
83 | Organization: "hi",
84 | Environment: "test",
85 | ClientReceivedStartTimestamp: ts * 1000,
86 | ClientReceivedEndTimestamp: ts * 1000,
87 | }, "missing GatewayFlowID"},
88 | } {
89 | t.Log(test.desc)
90 |
91 | gotErr := test.record.validate(time.Unix(ts, 0))
92 | if test.wantError == "" {
93 | if gotErr != nil {
94 | t.Errorf("got error %s, want none", gotErr)
95 | }
96 | continue
97 | }
98 | if gotErr == nil {
99 | t.Errorf("got nil error, want one containing %s", test.wantError)
100 | continue
101 | }
102 |
103 | if !strings.Contains(gotErr.Error(), test.wantError) {
104 | t.Errorf("error %s should contain '%s'", gotErr, test.wantError)
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/adapter/analytics/recovery.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package analytics
16 |
17 | import (
18 | "bufio"
19 | "compress/gzip"
20 | "fmt"
21 | "io"
22 | "io/ioutil"
23 | "os"
24 | "path/filepath"
25 |
26 | "github.com/hashicorp/go-multierror"
27 | )
28 |
29 | // crashRecovery cleans up the temp and staging dirs post-crash. This function
30 | // assumes that the temp dir exists and is accessible.
31 | func (m *manager) crashRecovery() error {
32 | dirs, err := ioutil.ReadDir(m.tempDir)
33 | if err != nil {
34 | return err
35 | }
36 | var errs error
37 | for _, d := range dirs {
38 | tenant := d.Name()
39 | tempDir := m.getTempDir(tenant)
40 | tempFiles, err := ioutil.ReadDir(tempDir)
41 | if err != nil {
42 | errs = multierror.Append(errs, err)
43 | continue
44 | }
45 |
46 | m.prepTenant(tenant)
47 | stageDir := m.getStagingDir(tenant)
48 |
49 | // put staged files in upload queue
50 | stagedFiles, err := m.getFilesInStaging()
51 | for _, fi := range stagedFiles {
52 | m.upload(tenant, fi)
53 | }
54 |
55 | // recover temp to staging and upload
56 | for _, fi := range tempFiles {
57 | tempFile := filepath.Join(tempDir, fi.Name())
58 | stageFile := filepath.Join(stageDir, fi.Name())
59 |
60 | dest, err := os.Create(stageFile)
61 | if err != nil {
62 | errs = multierror.Append(errs, fmt.Errorf("create recovery file %s: %s", tempDir, err))
63 | continue
64 | }
65 | if err := m.recoverFile(tempFile, dest); err != nil {
66 | errs = multierror.Append(errs, fmt.Errorf("recoverFile %s: %s", tempDir, err))
67 | if err := os.Remove(stageFile); err != nil {
68 | errs = multierror.Append(errs, fmt.Errorf("remove stage file %s: %s", tempDir, err))
69 | }
70 | continue
71 | }
72 |
73 | if err := os.Remove(tempFile); err != nil {
74 | m.log.Warningf("unable to remove temp file: %s", tempFile)
75 | }
76 |
77 | m.upload(tenant, stageFile)
78 | }
79 | }
80 | return errs
81 | }
82 |
83 | // recoverFile recovers gzipped data in a file and puts it into a new file.
84 | func (m *manager) recoverFile(oldName string, newFile *os.File) error {
85 | m.log.Warningf("recover file: %s", oldName)
86 | in, err := os.Open(oldName)
87 | if err != nil {
88 | return fmt.Errorf("open %s: %s", oldName, err)
89 | }
90 | br := bufio.NewReader(in)
91 | gzr, err := gzip.NewReader(br)
92 | if err != nil {
93 | return fmt.Errorf("gzip.NewReader(%s): %s", oldName, err)
94 | }
95 | defer gzr.Close()
96 |
97 | // buffer size is arbitrary and doesn't really matter
98 | b := make([]byte, 1000)
99 | gzw := gzip.NewWriter(newFile)
100 | for {
101 | var nRead int
102 | if nRead, err = gzr.Read(b); err != nil {
103 | if err != io.EOF && err.Error() != "unexpected EOF" && err.Error() != "gzip: invalid header" {
104 | return fmt.Errorf("scan gzip %s: %s", oldName, err)
105 | }
106 | }
107 | gzw.Write(b[:nRead])
108 | if err != nil {
109 | break
110 | }
111 | }
112 | if err := gzw.Close(); err != nil {
113 | return fmt.Errorf("close gzw %s: %s", oldName, err)
114 | }
115 | if err := newFile.Close(); err != nil {
116 | return fmt.Errorf("close gzw file %s: %s", oldName, err)
117 | }
118 |
119 | m.log.Infof("%s recovered to: %s", oldName, newFile.Name())
120 | return nil
121 | }
122 |
--------------------------------------------------------------------------------
/adapter/analytics/staging.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package analytics
16 |
17 | import (
18 | "fmt"
19 | "io/ioutil"
20 | "os"
21 | "path/filepath"
22 | "sync"
23 |
24 | "github.com/hashicorp/go-multierror"
25 | )
26 |
27 | func (m *manager) stageFile(tenant, tempFile string) {
28 |
29 | stageDir := m.getStagingDir(tenant)
30 | stagedFile := filepath.Join(stageDir, filepath.Base(tempFile))
31 | if err := os.Rename(tempFile, stagedFile); err != nil {
32 | m.log.Errorf("can't rename file: %s", err)
33 | return
34 | }
35 |
36 | m.upload(tenant, stagedFile)
37 | m.log.Debugf("staged file: %s", stagedFile)
38 | }
39 |
40 | func (m *manager) getFilesInStaging() ([]string, error) {
41 | tenantDirs, err := ioutil.ReadDir(m.stagingDir)
42 | if err != nil {
43 | return nil, fmt.Errorf("ReadDir(%s): %s", m.tempDir, err)
44 | }
45 |
46 | var errs error
47 | var filePaths []string
48 | for _, tenantDir := range tenantDirs {
49 | tenantDirPath := filepath.Join(m.stagingDir, tenantDir.Name())
50 |
51 | stagedFiles, err := ioutil.ReadDir(tenantDirPath)
52 | if err != nil {
53 | errs = multierror.Append(errs, fmt.Errorf("ls %s: %s", tenantDirPath, err))
54 | continue
55 | }
56 |
57 | for _, stagedFile := range stagedFiles {
58 | filePaths = append(filePaths, filepath.Join(tenantDirPath, stagedFile.Name()))
59 | }
60 | }
61 | return filePaths, errs
62 | }
63 |
64 | func (m *manager) stageAllBucketsWait() {
65 | wait := &sync.WaitGroup{}
66 | m.stageAllBuckets(wait)
67 | wait.Wait()
68 | }
69 |
70 | func (m *manager) stageAllBuckets(wait *sync.WaitGroup) {
71 | m.bucketsLock.Lock()
72 | buckets := m.buckets
73 | m.buckets = map[string]*bucket{}
74 | m.bucketsLock.Unlock()
75 | for tenant, bucket := range buckets {
76 | m.stageBucket(tenant, bucket, wait)
77 | }
78 | }
79 |
80 | func (m *manager) stageBucket(tenant string, b *bucket, wait *sync.WaitGroup) {
81 | if wait != nil {
82 | wait.Add(1)
83 | }
84 | b.close(wait)
85 | }
86 |
--------------------------------------------------------------------------------
/adapter/analytics/staging_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package analytics
16 |
17 | import (
18 | "io/ioutil"
19 | "net/http"
20 | "net/url"
21 | "os"
22 | "path/filepath"
23 | "testing"
24 | "time"
25 |
26 | "github.com/apigee/istio-mixer-adapter/adapter/auth"
27 | "github.com/apigee/istio-mixer-adapter/adapter/authtest"
28 | adaptertest "istio.io/istio/mixer/pkg/adapter/test"
29 | )
30 |
31 | func TestStagingSizeCap(t *testing.T) {
32 | t.Parallel()
33 | env := adaptertest.NewEnv(t)
34 |
35 | fs := newFakeServer(t)
36 | fs.failUpload = http.StatusInternalServerError
37 | defer fs.close()
38 |
39 | ts := int64(1521221450) // This timestamp is roughly 11:30 MST on Mar. 16, 2018.
40 | now := func() time.Time { return time.Unix(ts, 0) }
41 |
42 | workDir, err := ioutil.TempDir("", "")
43 | if err != nil {
44 | t.Fatalf("ioutil.TempDir(): %s", err)
45 | }
46 | defer os.RemoveAll(workDir)
47 |
48 | baseURL, _ := url.Parse(fs.URL())
49 |
50 | uploader := &saasUploader{
51 | log: env.Logger(),
52 | client: http.DefaultClient,
53 | baseURL: baseURL,
54 | key: "key",
55 | secret: "secret",
56 | now: now,
57 | }
58 |
59 | m, err := newManager(uploader, Options{
60 | BufferPath: workDir,
61 | StagingFileLimit: 3,
62 | now: now,
63 | CollectionInterval: time.Minute,
64 | })
65 | if err != nil {
66 | t.Fatalf("newManager: %s", err)
67 | }
68 |
69 | records := []Record{
70 | {
71 | Organization: "hi",
72 | Environment: "test",
73 | ClientReceivedStartTimestamp: ts * 1000,
74 | ClientReceivedEndTimestamp: ts * 1000,
75 | APIProxy: "proxy",
76 | },
77 | {
78 | Organization: "hi",
79 | Environment: "test",
80 | ClientReceivedStartTimestamp: ts * 1000,
81 | ClientReceivedEndTimestamp: ts * 1000,
82 | APIProduct: "product",
83 | },
84 | }
85 |
86 | m.Start(env)
87 |
88 | t1 := "hi~test"
89 | tc := authtest.NewContext(fs.URL(), env)
90 | tc.SetOrganization("hi")
91 | tc.SetEnvironment("test")
92 | ctx := &auth.Context{Context: tc}
93 |
94 | for i := 1; i < m.stagingFileLimit+3; i++ {
95 | if err := m.SendRecords(ctx, records); err != nil {
96 | t.Errorf("Error on SendRecords(): %s", err)
97 | }
98 | m.stageAllBucketsWait()
99 | }
100 | time.Sleep(50 * time.Millisecond)
101 |
102 | if f := filesIn(m.getTempDir(t1)); len(f) != 0 {
103 | t.Errorf("got %d files, want %d: %v", len(f), 0, f)
104 | }
105 |
106 | if f := filesIn(m.getStagingDir(t1)); len(f) != m.stagingFileLimit {
107 | t.Errorf("got %d files, want %d: %v", len(f), m.stagingFileLimit, f)
108 | }
109 |
110 | m.Close()
111 | }
112 |
113 | func filesIn(path string) []string {
114 | files, err := ioutil.ReadDir(path)
115 | if err != nil {
116 | return nil
117 | }
118 | var result []string
119 | for _, f := range files {
120 | result = append(result, filepath.Join(path, f.Name()))
121 | }
122 | return result
123 | }
124 |
--------------------------------------------------------------------------------
/adapter/analytics/testdata/README.md:
--------------------------------------------------------------------------------
1 | The cert files in this directory were created with the following command:
2 |
3 | go run "$(go env GOROOT)/src/crypto/tls/generate_cert.go" --rsa-bits 1024 --host 127.0.0.1,::1,localhost --ca --start-date "Jan 1 00:00:00 2019" --duration=1000000h
4 |
--------------------------------------------------------------------------------
/adapter/analytics/testdata/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIICEjCCAXugAwIBAgIRAJyI4XwEYhd/WmXtG40IlBowDQYJKoZIhvcNAQELBQAw
3 | EjEQMA4GA1UEChMHQWNtZSBDbzAgFw0xOTAxMDEwMDAwMDBaGA8yMTMzMDEyOTE2
4 | MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
5 | gYkCgYEAyW4e/XGus4dZceHuD3Cw07kE1bXWRpQtIiqWIs+Ol700dIBpNxr85ZUY
6 | 1eB/P7yAl+WiIgOwSXoYrzJmomW+SpYl25POBgdWePn9dDLJY1yRkvSG8Rw0N6sQ
7 | V1iq1vyCQ+7nGopNo8UxgjVFqxoxLeuBQuaTTcp6bbrnF/VWqgsCAwEAAaNmMGQw
8 | DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
9 | MAMBAf8wLAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAA
10 | AAABMA0GCSqGSIb3DQEBCwUAA4GBABb8soLXLHR7lqff1uch/9nTeFZMxWo7i+e4
11 | uGoRiCjXNSUdQsK8Fn0KIU6dAg2a0sOZ8Sgtl+VbPPyh47D/5M5DwFEniA8UMATE
12 | /KCvTeu4JffVS3jPmZX0af0IaOcaJP0IRrTkTRFTWbLmoi153K6wcq9+IsZhy1s0
13 | ZaMjB1bl
14 | -----END CERTIFICATE-----
15 |
--------------------------------------------------------------------------------
/adapter/analytics/testdata/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMluHv1xrrOHWXHh
3 | 7g9wsNO5BNW11kaULSIqliLPjpe9NHSAaTca/OWVGNXgfz+8gJfloiIDsEl6GK8y
4 | ZqJlvkqWJduTzgYHVnj5/XQyyWNckZL0hvEcNDerEFdYqtb8gkPu5xqKTaPFMYI1
5 | RasaMS3rgULmk03Kem265xf1VqoLAgMBAAECgYBNTWypOTqhfV0PPnR9CnNiHYxE
6 | c+9S0MTtasiJfXwssZjy6OD4G+xYMzr/wZM0I6R6Js9tHFtIJ4pXmhEXW9KF4H3K
7 | ZmIMs2slxRLbERv9ApE6+ddyZ5gBmtzD6qWHQ3qChQSjLVtKz4RIUHyKR602C4P2
8 | s00ryAjWMm09Z8gl+QJBANInB+gzSkGqzYZhc01FcdtQZOjCgBJJP0anlENvMcws
9 | W1yRlxcInIL/sly6v7GGXuT1i9GcxTJUKQAZItVQuU0CQQD1X/OO0KNZX2ZwYF25
10 | ms2Zf1WmXVc+9tDpPDPlwX5l/Sbb2hY0SlRksuXLCYhWzyDk6nRjty598ZUD5Fy2
11 | MQS3AkAm8CJv7Kjyl+Iy5vWFOLvK5g98bSVrvfSic8Rt5jl02jcnZLZ5Bxhw0U3M
12 | DrIcA4irpa99bC3BkIR0RzQEEEv1AkBoDw4KJd7wWu3lgGie+tBwZTjceb8zO5az
13 | Is3bhOhmtioRmHZMLK2Hmvqq1VsVfXe0vN0pIJk93gLVCLZsqXMXAkEAxXm9lrGX
14 | zOzdmz0v9WJlHsP0bBz+WpGbI7ArQ7MuNkh1uvuQv3Cevdw2kw24sHpC9i5WzlJl
15 | VTbu3q4nicI0MQ==
16 | -----END PRIVATE KEY-----
17 |
--------------------------------------------------------------------------------
/adapter/auth/auth_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package auth
16 |
17 | import (
18 | "net/http"
19 | "testing"
20 | "time"
21 |
22 | "github.com/apigee/istio-mixer-adapter/adapter/authtest"
23 | "github.com/apigee/istio-mixer-adapter/adapter/context"
24 | adaptertest "istio.io/istio/mixer/pkg/adapter/test"
25 | )
26 |
27 | type testVerifier struct {
28 | keyErrors map[string]error
29 | claims map[string]interface{}
30 | }
31 |
32 | var testJWTClaims = map[string]interface{}{
33 | "client_id": "hi",
34 | "application_name": "taco",
35 | "exp": 14.0,
36 | "api_product_list": []string{"superapp"},
37 | "scopes": []string{"scope"},
38 | }
39 |
40 | func (tv *testVerifier) Verify(ctx context.Context, apiKey string) (map[string]interface{}, error) {
41 | err := tv.keyErrors[apiKey]
42 | if err != nil {
43 | return nil, err
44 | }
45 |
46 | return testJWTClaims, nil
47 | }
48 |
49 | func TestNewManager(t *testing.T) {
50 | env := adaptertest.NewEnv(t)
51 | opts := Options{
52 | PollInterval: time.Hour,
53 | Client: &http.Client{},
54 | }
55 | man, err := NewManager(env, opts)
56 | if err != nil {
57 | t.Fatalf("create and start manager: %v", err)
58 | }
59 | if opts.PollInterval != man.jwtMan.pollInterval {
60 | t.Errorf("pollInterval want: %v, got: %v", opts.PollInterval, man.jwtMan.pollInterval)
61 | }
62 | verifier := man.verifier.(*keyVerifierImpl)
63 | if opts.Client != verifier.client {
64 | t.Errorf("client want: %v, got: %v", opts.Client, verifier.client)
65 | }
66 | man.Close()
67 | }
68 |
69 | func TestAuthenticate(t *testing.T) {
70 | goodAPIKey := "good"
71 | badAPIKey := "bad"
72 | errAPIKey := "error"
73 | missingProductListError := "api_product_list claim is required"
74 |
75 | for _, test := range []struct {
76 | desc string
77 | apiKey string
78 | apiKeyClaimKey string
79 | claims map[string]interface{}
80 | wantError string
81 | }{
82 | {"with valid JWT", "", "", testJWTClaims, ""},
83 | {"with invalid JWT", "", "", map[string]interface{}{"exp": "1"}, missingProductListError},
84 | {"with valid API key", goodAPIKey, "", nil, ""},
85 | {"with invalid API key", badAPIKey, "", nil, ErrBadAuth.Error()},
86 | {"with valid claims API key", "", "goodkey", map[string]interface{}{
87 | "exp": "1",
88 | "api_product_list": "[]",
89 | "goodkey": goodAPIKey,
90 | }, ""},
91 | {"with invalid claims API key", "", "badkey", map[string]interface{}{
92 | "exp": "1",
93 | "somekey": goodAPIKey,
94 | "badkey": badAPIKey,
95 | }, ErrBadAuth.Error()},
96 | {"with missing claims API key", "", "missingkey", map[string]interface{}{
97 | "exp": "1",
98 | }, missingProductListError},
99 | {"error verifying API key", errAPIKey, "", nil, ErrInternalError.Error()},
100 | } {
101 | t.Log(test.desc)
102 |
103 | env := adaptertest.NewEnv(t)
104 |
105 | jwtMan := newJWTManager(time.Hour)
106 | tv := &testVerifier{
107 | keyErrors: map[string]error{
108 | goodAPIKey: nil,
109 | badAPIKey: ErrBadAuth,
110 | errAPIKey: ErrInternalError,
111 | },
112 | }
113 | authMan := &Manager{
114 | env: env,
115 | jwtMan: jwtMan,
116 | verifier: tv,
117 | }
118 | authMan.start()
119 | defer authMan.Close()
120 |
121 | ctx := authtest.NewContext("", adaptertest.NewEnv(t))
122 | _, err := authMan.Authenticate(ctx, test.apiKey, test.claims, test.apiKeyClaimKey)
123 | if err != nil {
124 | if test.wantError != err.Error() {
125 | t.Errorf("wanted error: %s, got: %s", test.wantError, err.Error())
126 | }
127 | } else if test.wantError != "" {
128 | t.Errorf("wanted error, got none")
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/adapter/auth/context.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package auth
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 | "strconv"
21 | "time"
22 |
23 | "github.com/apigee/istio-mixer-adapter/adapter/context"
24 | "github.com/pkg/errors"
25 | )
26 |
27 | const (
28 | apiProductListClaim = "api_product_list"
29 | audienceClaim = "audience"
30 | clientIDClaim = "client_id"
31 | applicationNameClaim = "application_name"
32 | scopesClaim = "scopes"
33 | expClaim = "exp"
34 | developerEmailClaim = "application_developeremail"
35 | accessTokenClaim = "access_token"
36 | )
37 |
38 | var (
39 | // AllValidClaims is a list of the claims expected from a JWT token
40 | AllValidClaims = []string{
41 | apiProductListClaim, audienceClaim, clientIDClaim, applicationNameClaim,
42 | scopesClaim, expClaim, developerEmailClaim,
43 | }
44 | )
45 |
46 | // A Context wraps all the various information that is needed to make requests
47 | // through the Apigee adapter.
48 | type Context struct {
49 | context.Context
50 | ClientID string
51 | AccessToken string
52 | Application string
53 | APIProducts []string
54 | Expires time.Time
55 | DeveloperEmail string
56 | Scopes []string
57 | APIKey string
58 | }
59 |
60 | func parseExp(claims map[string]interface{}) (time.Time, error) {
61 | // JSON decodes this struct to either float64 or string, so we won't
62 | // need to check anything else.
63 | switch exp := claims[expClaim].(type) {
64 | case float64:
65 | return time.Unix(int64(exp), 0), nil
66 | case string:
67 | var expi int64
68 | var err error
69 | if expi, err = strconv.ParseInt(exp, 10, 64); err != nil {
70 | return time.Time{}, err
71 | }
72 | return time.Unix(expi, 0), nil
73 | }
74 | return time.Time{}, fmt.Errorf("unknown type %T for exp %v", claims[expClaim], claims[expClaim])
75 | }
76 |
77 | // if claims can't be processed, returns error and sets no fields
78 | func (a *Context) setClaims(claims map[string]interface{}) error {
79 | if claims[apiProductListClaim] == nil {
80 | return fmt.Errorf("api_product_list claim is required")
81 | }
82 |
83 | products, err := parseArrayOfStrings(claims[apiProductListClaim])
84 | if err != nil {
85 | return errors.Wrapf(err, "unable to interpret api_product_list: %v", claims[apiProductListClaim])
86 | }
87 |
88 | scopes, err := parseArrayOfStrings(claims[scopesClaim])
89 | if err != nil {
90 | return errors.Wrapf(err, "unable to interpret scopes: %v", claims[scopesClaim])
91 | }
92 |
93 | exp, err := parseExp(claims)
94 | if err != nil {
95 | return err
96 | }
97 |
98 | var ok bool
99 | if _, ok = claims[clientIDClaim].(string); !ok {
100 | return errors.Wrapf(err, "unable to interpret %s: %v", clientIDClaim, claims[clientIDClaim])
101 | }
102 | if _, ok = claims[applicationNameClaim].(string); !ok {
103 | return errors.Wrapf(err, "unable to interpret %s: %v", applicationNameClaim, claims[applicationNameClaim])
104 | }
105 | a.ClientID = claims[clientIDClaim].(string)
106 | a.Application = claims[applicationNameClaim].(string)
107 | a.APIProducts = products
108 | a.Scopes = scopes
109 | a.Expires = exp
110 | a.DeveloperEmail, _ = claims[developerEmailClaim].(string)
111 | a.AccessToken, _ = claims[accessTokenClaim].(string)
112 |
113 | return nil
114 | }
115 |
116 | func (a *Context) isAuthenticated() bool {
117 | return a.ClientID != ""
118 | }
119 |
120 | func parseArrayOfStrings(obj interface{}) (results []string, err error) {
121 | if obj == nil {
122 | // nil is ok
123 | } else if arr, ok := obj.([]string); ok {
124 | results = arr
125 | } else if arr, ok := obj.([]interface{}); ok {
126 | for _, unk := range arr {
127 | if obj, ok := unk.(string); ok {
128 | results = append(results, obj)
129 | } else {
130 | err = fmt.Errorf("unable to interpret: %v", unk)
131 | break
132 | }
133 | }
134 | } else if str, ok := obj.(string); ok {
135 | err = json.Unmarshal([]byte(str), &results)
136 | } else {
137 | err = fmt.Errorf("unable to interpret: %v", obj)
138 | }
139 | return
140 | }
141 |
--------------------------------------------------------------------------------
/adapter/auth/context_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package auth
16 |
17 | import (
18 | "reflect"
19 | "strconv"
20 | "testing"
21 | "time"
22 | )
23 |
24 | func TestParseExp(t *testing.T) {
25 | now := time.Unix(time.Now().Unix(), 0)
26 |
27 | claims := map[string]interface{}{
28 | expClaim: float64(now.Unix()),
29 | }
30 | exp, err := parseExp(claims)
31 | if err != nil {
32 | t.Errorf("parseExp: %v", err)
33 | }
34 | if exp != now {
35 | t.Errorf("parseExp float got: %v, want: %v", exp, now)
36 | }
37 |
38 | claims[expClaim] = strconv.FormatInt(time.Now().Unix(), 10)
39 | exp, err = parseExp(claims)
40 | if err != nil {
41 | t.Errorf("parseExp: %v", err)
42 | }
43 | if exp != now {
44 | t.Errorf("parseExp string got: %v, want: %v", exp, now)
45 | }
46 |
47 | claims[expClaim] = "badexp"
48 | _, err = parseExp(claims)
49 | if err == nil {
50 | t.Error("parseExp should have gotten an error")
51 | }
52 | }
53 |
54 | func TestSetClaims(t *testing.T) {
55 | c := Context{}
56 | now := time.Unix(time.Now().Unix(), 0)
57 | claims := map[string]interface{}{
58 | apiProductListClaim: time.Now(),
59 | audienceClaim: "aud",
60 | //clientIDClaim: nil,
61 | applicationNameClaim: "app",
62 | scopesClaim: nil,
63 | expClaim: float64(now.Unix()),
64 | developerEmailClaim: "email",
65 | }
66 | err := c.setClaims(claims)
67 | if err == nil {
68 | t.Errorf("setClaims without client_id should get error")
69 | }
70 |
71 | claims[clientIDClaim] = "clientID"
72 | err = c.setClaims(claims)
73 | if err == nil {
74 | t.Errorf("bad product list should error")
75 | }
76 |
77 | productsWant := []string{"product 1", "product 2"}
78 | claims[apiProductListClaim] = `["product 1", "product 2"]`
79 | err = c.setClaims(claims)
80 | if err != nil {
81 | t.Fatalf("valid setClaims, got: %v", err)
82 | }
83 | if !reflect.DeepEqual(c.APIProducts, productsWant) {
84 | t.Errorf("apiProducts want: %s, got: %v", productsWant, c.APIProducts)
85 | }
86 |
87 | claimsWant := []string{"scope1", "scope2"}
88 | claims[scopesClaim] = []interface{}{"scope1", "scope2"}
89 | err = c.setClaims(claims)
90 | if err != nil {
91 | t.Fatalf("valid setClaims, got: %v", err)
92 | }
93 | if !reflect.DeepEqual(claimsWant, c.Scopes) {
94 | t.Errorf("claims want: %s, got: %v", claimsWant, claims[scopesClaim])
95 | }
96 |
97 | //if c.A != "" {
98 | // t.Errorf("nil ClientID should be empty, got: %v", c.ClientID)
99 | //}
100 |
101 | //ClientID string
102 | //AccessToken string
103 | //Application string
104 | //APIProducts []string
105 | //Expires time.Time
106 | //DeveloperEmail string
107 | //Scopes []string
108 | //APIKey string
109 | }
110 |
--------------------------------------------------------------------------------
/adapter/auth/jwt.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package auth
16 |
17 | import (
18 | "context"
19 | "encoding/json"
20 | "path"
21 | "sync"
22 | "time"
23 |
24 | adapterContext "github.com/apigee/istio-mixer-adapter/adapter/context"
25 | "github.com/apigee/istio-mixer-adapter/adapter/util"
26 | "github.com/lestrrat/go-jwx/jwk"
27 | "github.com/lestrrat/go-jwx/jws"
28 | "github.com/lestrrat/go-jwx/jwt"
29 | "github.com/pkg/errors"
30 | "istio.io/istio/mixer/pkg/adapter"
31 | )
32 |
33 | const (
34 | certsPath = "/certs"
35 | acceptableSkew = 10 * time.Second
36 | )
37 |
38 | func newJWTManager(pollInterval time.Duration) *jwtManager {
39 | return &jwtManager{
40 | jwkSets: sync.Map{},
41 | pollInterval: pollInterval,
42 | }
43 | }
44 |
45 | // An jwtManager handles all of the various JWT authentication functionality.
46 | type jwtManager struct {
47 | jwkSets sync.Map
48 | pollInterval time.Duration
49 | cancelPolling context.CancelFunc
50 | }
51 |
52 | func (a *jwtManager) start(env adapter.Env) {
53 | if a.pollInterval > 0 {
54 | env.Logger().Debugf("starting cert polling")
55 | looper := util.Looper{
56 | Env: env,
57 | Backoff: util.NewExponentialBackoff(200*time.Millisecond, a.pollInterval, 2, true),
58 | }
59 | ctx, cancel := context.WithCancel(context.Background())
60 | a.cancelPolling = cancel
61 | looper.Start(ctx, a.refresh, a.pollInterval, func(err error) error {
62 | env.Logger().Errorf("Error refreshing cert set: %s", err)
63 | return nil
64 | })
65 | }
66 | }
67 |
68 | func (a *jwtManager) stop() {
69 | if a != nil && a.cancelPolling != nil {
70 | a.cancelPolling()
71 | }
72 | }
73 |
74 | func (a *jwtManager) ensureSet(url string) error {
75 | set, err := jwk.FetchHTTP(url)
76 | if err != nil {
77 | return err
78 | }
79 | a.jwkSets.Store(url, set)
80 | return nil
81 | }
82 |
83 | func (a *jwtManager) refresh(ctx context.Context) error {
84 | var errRet error
85 | a.jwkSets.Range(func(urlI interface{}, setI interface{}) bool {
86 | if err := a.ensureSet(urlI.(string)); err != nil {
87 | errRet = err
88 | }
89 | return ctx.Err() == nil // if not canceled, keep going
90 | })
91 | return errRet
92 | }
93 |
94 | func (a *jwtManager) jwkSet(ctx adapterContext.Context) (*jwk.Set, error) {
95 | jwksURL := *ctx.CustomerBase()
96 | jwksURL.Path = path.Join(jwksURL.Path, certsPath)
97 | url := jwksURL.String()
98 | if _, ok := a.jwkSets.Load(url); !ok {
99 | if err := a.ensureSet(url); err != nil {
100 | return nil, err
101 | }
102 | }
103 | set, _ := a.jwkSets.Load(url)
104 | return set.(*jwk.Set), nil
105 | }
106 |
107 | func (a *jwtManager) parseJWT(ctx adapterContext.Context, raw string, verify bool) (map[string]interface{}, error) {
108 |
109 | if verify {
110 | set, err := a.jwkSet(ctx)
111 | if err != nil {
112 | return nil, err
113 | }
114 |
115 | // verify against public keys
116 | _, err = jws.VerifyWithJWKSet([]byte(raw), set, nil)
117 | if err != nil {
118 | return nil, err
119 | }
120 | }
121 |
122 | if verify {
123 | // verify fields
124 | token, err := jwt.ParseString(raw)
125 | if err != nil {
126 | return nil, errors.Wrap(err, "invalid jws message")
127 | }
128 |
129 | err = token.Verify(jwt.WithAcceptableSkew(acceptableSkew))
130 | if err != nil {
131 | return nil, errors.Wrap(err, "invalid jws message")
132 | }
133 | }
134 |
135 | // get claims
136 | m, err := jws.ParseString(raw)
137 | if err != nil {
138 | return nil, errors.Wrap(err, "invalid jws message")
139 | }
140 |
141 | var claims map[string]interface{}
142 | if err := json.Unmarshal(m.Payload(), &claims); err != nil {
143 | return nil, errors.Wrap(err, "failed to parse claims")
144 | }
145 |
146 | return claims, nil
147 | }
148 |
--------------------------------------------------------------------------------
/adapter/auth/structs.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package auth
16 |
17 | // APIKeyRequest is the request to Apigee's verifyAPIKey API
18 | type APIKeyRequest struct {
19 | APIKey string `json:"apiKey"`
20 | }
21 |
22 | // APIKeyResponse is the response from Apigee's verifyAPIKey API
23 | type APIKeyResponse struct {
24 | Token string `json:"token"`
25 | }
26 |
--------------------------------------------------------------------------------
/adapter/authtest/context.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package authtest
16 |
17 | import (
18 | "fmt"
19 | "net/url"
20 |
21 | "istio.io/istio/mixer/pkg/adapter"
22 | )
23 |
24 | // Context implements the context.Context interface and is to be used in tests.
25 | type Context struct {
26 | apigeeBase *url.URL
27 | customerBase *url.URL
28 | orgName string
29 | envName string
30 | key string
31 | secret string
32 | log adapter.Logger
33 | }
34 |
35 | // NewContext constructs a new test context.
36 | func NewContext(base string, log adapter.Logger) *Context {
37 | u, err := url.Parse(base)
38 | if err != nil {
39 | panic(fmt.Sprintf("Could not parse URL: %s", base))
40 | }
41 | return &Context{
42 | apigeeBase: u,
43 | customerBase: u,
44 | log: log,
45 | }
46 | }
47 |
48 | // Log gets a logger for the test context.
49 | func (c *Context) Log() adapter.Logger { return c.log }
50 |
51 | // ApigeeBase gets a URL base to send HTTP requests to.
52 | func (c *Context) ApigeeBase() *url.URL { return c.apigeeBase }
53 |
54 | // CustomerBase gets a URL base to send HTTP requests to.
55 | func (c *Context) CustomerBase() *url.URL { return c.customerBase }
56 |
57 | // Organization gets this context's organization.
58 | func (c *Context) Organization() string { return c.orgName }
59 |
60 | // Environment gets this context's environment.
61 | func (c *Context) Environment() string { return c.envName }
62 |
63 | // Key gets this context's API key.
64 | func (c *Context) Key() string { return c.key }
65 |
66 | // Secret gets this context's API secret.
67 | func (c *Context) Secret() string { return c.secret }
68 |
69 | // SetOrganization sets this context's organization.
70 | func (c *Context) SetOrganization(o string) { c.orgName = o }
71 |
72 | // SetEnvironment sets this context's environment.
73 | func (c *Context) SetEnvironment(e string) { c.envName = e }
74 |
--------------------------------------------------------------------------------
/adapter/config/config.proto_descriptor:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apigee/istio-mixer-adapter/dcd54df6a221d07eebff6e82195b5caf3316b42b/adapter/config/config.proto_descriptor
--------------------------------------------------------------------------------
/adapter/context/context.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package context
16 |
17 | import (
18 | "net/url"
19 |
20 | "istio.io/istio/mixer/pkg/adapter"
21 | )
22 |
23 | // A Context contains all the information needed to communicate with Apigee
24 | // home servers.
25 | type Context interface {
26 | Log() adapter.Logger
27 | Organization() string
28 | Environment() string
29 | Key() string
30 | Secret() string
31 |
32 | ApigeeBase() *url.URL
33 | CustomerBase() *url.URL
34 | }
35 |
--------------------------------------------------------------------------------
/adapter/product/products.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package product
16 |
17 | import (
18 | "fmt"
19 | "net/http"
20 | "net/url"
21 | "time"
22 |
23 | "istio.io/istio/mixer/pkg/adapter"
24 | )
25 |
26 | // ServicesAttr is the name of the Product attribute that lists the Istio services it binds to (comma delim)
27 | const ServicesAttr = "istio-services"
28 |
29 | // NewManager creates a new product.Manager. Call Close() when done.
30 | func NewManager(env adapter.Env, options Options) (*Manager, error) {
31 | if err := options.validate(); err != nil {
32 | return nil, err
33 | }
34 | pm := createManager(options, env.Logger())
35 | pm.start(env)
36 | return pm, nil
37 | }
38 |
39 | // Options allows us to specify options for how this product manager will run.
40 | type Options struct {
41 | // Client is a configured HTTPClient
42 | Client *http.Client
43 | // BaseURL of the Apigee customer proxy
44 | BaseURL *url.URL
45 | // RefreshRate determines how often the products are refreshed
46 | RefreshRate time.Duration
47 | // Key is provisioning key
48 | Key string
49 | // Secret is provisioning secret
50 | Secret string
51 | }
52 |
53 | func (o *Options) validate() error {
54 | if o.Client == nil ||
55 | o.BaseURL == nil ||
56 | o.RefreshRate <= 0 ||
57 | o.Key == "" ||
58 | o.Secret == "" {
59 | return fmt.Errorf("all products options are required")
60 | }
61 | if o.RefreshRate < time.Minute {
62 | return fmt.Errorf("products refresh_rate must be >= 1 minute")
63 | }
64 | return nil
65 | }
66 |
--------------------------------------------------------------------------------
/adapter/product/structs.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package product
16 |
17 | import "regexp"
18 |
19 | // APIResponse is the response from the Apigee products API
20 | type APIResponse struct {
21 | APIProducts []APIProduct `json:"apiProduct"`
22 | }
23 |
24 | // An APIProduct is an Apigee API product. See the Apigee docs for details:
25 | // https://docs.apigee.com/api-platform/publish/what-api-product
26 | type APIProduct struct {
27 | Attributes []Attribute `json:"attributes,omitempty"`
28 | // CreatedAt int64 `json:"createdAt,omitempty"`
29 | CreatedBy string `json:"createdBy,omitempty"`
30 | Description string `json:"description,omitempty"`
31 | DisplayName string `json:"displayName,omitempty"`
32 | Environments []string `json:"environments,omitempty"`
33 | // LastModifiedAt int64 `json:"lastModifiedAt,omitempty"`
34 | LastModifiedBy string `json:"lastModifiedBy,omitempty"`
35 | Name string `json:"name,omitempty"`
36 | QuotaLimit string `json:"quota,omitempty"`
37 | QuotaInterval string `json:"quotaInterval,omitempty"`
38 | QuotaTimeUnit string `json:"quotaTimeUnit,omitempty"`
39 | Resources []string `json:"apiResources"`
40 | Scopes []string `json:"scopes"`
41 | Targets []string
42 | QuotaLimitInt int64
43 | QuotaIntervalInt int64
44 | resourceRegexps []*regexp.Regexp
45 | }
46 |
47 | // An Attribute is a name-value-pair attribute of an API product.
48 | type Attribute struct {
49 | Name string `json:"name,omitempty"`
50 | Value string `json:"value,omitempty"`
51 | }
52 |
--------------------------------------------------------------------------------
/adapter/quota/result_cache.go:
--------------------------------------------------------------------------------
1 | package quota
2 |
3 | import (
4 | "container/list"
5 | "sync"
6 | )
7 |
8 | // ResultCache is a structure to track Results by ID, bounded by size
9 | type ResultCache struct {
10 | size int
11 | lookup map[string]*Result
12 | buffer list.List
13 | lock sync.Mutex
14 | }
15 |
16 | // Add a Result to the cache
17 | func (d *ResultCache) Add(id string, result *Result) {
18 | d.lock.Lock()
19 | defer d.lock.Unlock()
20 | _, ok := d.lookup[id]
21 | if ok {
22 | return
23 | }
24 | if d.lookup == nil {
25 | d.lookup = make(map[string]*Result)
26 | }
27 | d.lookup[id] = result
28 | d.buffer.PushBack(id)
29 | if d.buffer.Len() > d.size {
30 | e := d.buffer.Front()
31 | d.buffer.Remove(e)
32 | delete(d.lookup, e.Value.(string))
33 | }
34 | return
35 | }
36 |
37 | // Get a Result from the cache, nil if none
38 | func (d *ResultCache) Get(id string) *Result {
39 | d.lock.Lock()
40 | defer d.lock.Unlock()
41 | result, ok := d.lookup[id]
42 | if ok {
43 | return result
44 | }
45 | return nil
46 | }
47 |
--------------------------------------------------------------------------------
/adapter/quota/result_cache_test.go:
--------------------------------------------------------------------------------
1 | package quota
2 |
3 | import "testing"
4 |
5 | func TestResultCache(t *testing.T) {
6 | results := ResultCache{
7 | size: 2,
8 | }
9 |
10 | tests := []struct {
11 | add string
12 | exists []string
13 | notExists []string
14 | }{
15 | {"test1", []string{"test1"}, []string{""}},
16 | {"test2", []string{"test1", "test2"}, []string{""}},
17 | {"test3", []string{"test2", "test3"}, []string{"test1"}},
18 | {"test1", []string{"test1", "test3"}, []string{"test2"}},
19 | {"test2", []string{"test1", "test2"}, []string{"test3"}},
20 | }
21 |
22 | for i, test := range tests {
23 | results.Add(test.add, &Result{})
24 | for _, id := range test.exists {
25 | if results.Get(id) == nil {
26 | t.Errorf("test[%d] %s value %s should exist", i, test.add, id)
27 | }
28 | }
29 | for _, id := range test.notExists {
30 | if results.Get(id) != nil {
31 | t.Errorf("test[%d] %s value %s should not exist", i, test.add, id)
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/adapter/quota/structs.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package quota
16 |
17 | import "time"
18 |
19 | // A Request is sent to Apigee's quota server to allocate quota.
20 | type Request struct {
21 | Identifier string `json:"identifier"`
22 | Weight int64 `json:"weight"`
23 | Interval int64 `json:"interval"`
24 | Allow int64 `json:"allow"`
25 | TimeUnit string `json:"timeUnit"`
26 | }
27 |
28 | // A Result is a response from Apigee's quota server that gives information
29 | // about how much quota is available. Note that Used will never exceed Allowed,
30 | // but Exceeded will be positive in that case.
31 | type Result struct {
32 | Allowed int64 `json:"allowed"`
33 | Used int64 `json:"used"`
34 | Exceeded int64 `json:"exceeded"`
35 | ExpiryTime int64 `json:"expiryTime"`
36 | Timestamp int64 `json:"timestamp"`
37 | }
38 |
39 | func (r *Result) expiredAt(tm time.Time) bool {
40 | return time.Unix(r.ExpiryTime, 0).After(tm)
41 | }
42 |
--------------------------------------------------------------------------------
/adapter/util/atomic.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package util
16 |
17 | import "sync/atomic"
18 |
19 | // AtomicBool is a threadsafe bool
20 | type AtomicBool struct {
21 | boolInt *int32
22 | }
23 |
24 | // NewAtomicBool creates an AtomicBool
25 | func NewAtomicBool(flag bool) *AtomicBool {
26 | boolInt := int32(0)
27 | if flag {
28 | boolInt = int32(1)
29 | }
30 | return &AtomicBool{
31 | boolInt: &boolInt,
32 | }
33 | }
34 |
35 | // IsTrue returns true if true
36 | func (a *AtomicBool) IsTrue() bool {
37 | return atomic.LoadInt32(a.boolInt) == int32(1)
38 | }
39 |
40 | // IsFalse returns false if false
41 | func (a *AtomicBool) IsFalse() bool {
42 | return atomic.LoadInt32(a.boolInt) != int32(1)
43 | }
44 |
45 | // SetTrue sets the bool to true, returns true if unchanged
46 | func (a *AtomicBool) SetTrue() bool {
47 | return atomic.SwapInt32(a.boolInt, 1) == int32(1)
48 | }
49 |
50 | // SetFalse sets the bool to false, returns true if unchanged
51 | func (a *AtomicBool) SetFalse() bool {
52 | return atomic.SwapInt32(a.boolInt, 0) == int32(0)
53 | }
54 |
--------------------------------------------------------------------------------
/adapter/util/atomic_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package util
16 |
17 | import (
18 | "testing"
19 | )
20 |
21 | func TestAtomicBool(t *testing.T) {
22 | ab := NewAtomicBool(true)
23 | if !ab.IsTrue() {
24 | t.Error("should be true")
25 | }
26 | if ab.IsFalse() {
27 | t.Error("should not be false")
28 | }
29 |
30 | if ab.SetFalse() {
31 | t.Errorf("should have changed to false")
32 | }
33 | if !ab.SetFalse() {
34 | t.Errorf("should not have changed to false")
35 | }
36 | if ab.IsTrue() {
37 | t.Error("should not be true")
38 | }
39 | if !ab.IsFalse() {
40 | t.Error("should be false")
41 | }
42 |
43 | if ab.SetTrue() {
44 | t.Errorf("should have changed to true")
45 | }
46 | if !ab.SetTrue() {
47 | t.Errorf("should not have changed to true")
48 | }
49 | if !ab.IsTrue() {
50 | t.Error("should be true")
51 | }
52 | if ab.IsFalse() {
53 | t.Error("should not be false")
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/adapter/util/backoff.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package util
16 |
17 | import (
18 | "math"
19 | "math/rand"
20 | "time"
21 | )
22 |
23 | const (
24 | defaultInitial = 200 * time.Millisecond
25 | defaultMax = 10 * time.Second
26 | defaultFactor float64 = 2
27 | defaultJitter = false
28 | )
29 |
30 | // Backoff defines functions for RPC Backoff strategy.
31 | type Backoff interface {
32 | Duration() time.Duration
33 | Attempt() int
34 | Reset()
35 | Clone() Backoff
36 | }
37 |
38 | // ExponentialBackoff is a backoff strategy that backs off exponentially.
39 | type ExponentialBackoff struct {
40 | attempt int
41 | initial, max time.Duration
42 | jitter bool
43 | backoffStrategy func() time.Duration
44 | factor float64
45 | }
46 |
47 | // DefaultExponentialBackoff constructs a new ExponentialBackoff with defaults.
48 | func DefaultExponentialBackoff() Backoff {
49 | return NewExponentialBackoff(0, 0, 0, defaultJitter)
50 | }
51 |
52 | // NewExponentialBackoff constructs a new ExponentialBackoff.
53 | func NewExponentialBackoff(initial, max time.Duration, factor float64, jitter bool) Backoff {
54 | backoff := &ExponentialBackoff{}
55 |
56 | if initial <= 0 {
57 | initial = defaultInitial
58 | }
59 | if max <= 0 {
60 | max = defaultMax
61 | }
62 |
63 | if factor <= 0 {
64 | factor = defaultFactor
65 | }
66 |
67 | backoff.initial = initial
68 | backoff.max = max
69 | backoff.attempt = 0
70 | backoff.factor = factor
71 | backoff.jitter = jitter
72 | backoff.backoffStrategy = backoff.exponentialBackoffStrategy
73 |
74 | return backoff
75 | }
76 |
77 | // Duration calculates how long should be waited before attempting again. Note
78 | // that this method is stateful - each call counts as an "attempt".
79 | func (b *ExponentialBackoff) Duration() time.Duration {
80 | d := b.backoffStrategy()
81 | b.attempt++
82 | return d
83 | }
84 |
85 | func (b *ExponentialBackoff) exponentialBackoffStrategy() time.Duration {
86 |
87 | initial := float64(b.initial)
88 | attempt := float64(b.attempt)
89 | duration := initial * math.Pow(b.factor, attempt)
90 |
91 | if b.jitter {
92 | duration = rand.Float64()*(duration-initial) + initial
93 | }
94 |
95 | if duration > math.MaxInt64 {
96 | return b.max
97 | }
98 |
99 | dur := time.Duration(duration)
100 | if dur > b.max {
101 | return b.max
102 | }
103 |
104 | return dur
105 | }
106 |
107 | // Reset clears any state that the backoff strategy has.
108 | func (b *ExponentialBackoff) Reset() {
109 | b.attempt = 0
110 | }
111 |
112 | // Attempt returns how many attempts have been made.
113 | func (b *ExponentialBackoff) Attempt() int {
114 | return b.attempt
115 | }
116 |
117 | // Clone returns a copy
118 | func (b *ExponentialBackoff) Clone() Backoff {
119 | return &ExponentialBackoff{
120 | attempt: b.attempt,
121 | initial: b.initial,
122 | max: b.max,
123 | jitter: b.jitter,
124 | backoffStrategy: b.backoffStrategy,
125 | factor: b.factor,
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/adapter/util/backoff_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package util
16 |
17 | import (
18 | "testing"
19 | "time"
20 | )
21 |
22 | func TestExponentialBackoff(t *testing.T) {
23 | initial := 200 * time.Millisecond
24 | max := 2 * time.Second
25 | factor := float64(2)
26 | jitter := false
27 | b := NewExponentialBackoff(initial, max, factor, jitter)
28 |
29 | for i := 0; i < 2; i++ {
30 | durations := []time.Duration{
31 | 200 * time.Millisecond,
32 | 400 * time.Millisecond,
33 | 800 * time.Millisecond,
34 | 1600 * time.Millisecond,
35 | 2000 * time.Millisecond,
36 | }
37 |
38 | for i, want := range durations {
39 | got := b.Duration()
40 | if want != got {
41 | t.Errorf("duration want: %d, got: %d", want, got)
42 | }
43 | if i+1 != b.Attempt() {
44 | t.Errorf("attempt want: %d, got: %d", i+1, b.Attempt())
45 | }
46 | }
47 |
48 | b.Reset()
49 | }
50 | }
51 |
52 | func TestBackoffWithJitter(t *testing.T) {
53 | initial := 200 * time.Millisecond
54 | max := 2 * time.Second
55 | factor := float64(2)
56 | jitter := true
57 | b := NewExponentialBackoff(initial, max, factor, jitter)
58 |
59 | durations := []time.Duration{
60 | 200 * time.Millisecond,
61 | 400 * time.Millisecond,
62 | 800 * time.Millisecond,
63 | 1600 * time.Millisecond,
64 | 2000 * time.Millisecond,
65 | }
66 |
67 | for i, want := range durations {
68 | got := b.Duration()
69 | if got < initial || got > want {
70 | t.Errorf("duration out of bounds. got: %v, iter: %d", got, i)
71 | }
72 | }
73 |
74 | b.Reset()
75 | }
76 |
77 | func TestDefaultBackoff(t *testing.T) {
78 | backoff := DefaultExponentialBackoff()
79 | eb, ok := backoff.(*ExponentialBackoff)
80 | if !ok {
81 | t.Errorf("not an *ExponentialBackoff")
82 | }
83 | if eb.initial != defaultInitial {
84 | t.Errorf("want: %v, got: %v", defaultInitial, eb.initial)
85 | }
86 | if defaultInitial != eb.initial {
87 | t.Errorf("want: %v, got: %v", defaultInitial, eb.initial)
88 | }
89 | if defaultMax != eb.max {
90 | t.Errorf("want: %v, got: %v", defaultMax, eb.max)
91 | }
92 | if defaultFactor != eb.factor {
93 | t.Errorf("want: %v, got: %v", defaultFactor, eb.factor)
94 | }
95 | if defaultJitter != eb.jitter {
96 | t.Errorf("want: %v, got: %v", defaultJitter, eb.jitter)
97 | }
98 | }
99 |
100 | func TestCloneExponentialBackoff(t *testing.T) {
101 | backoff1 := DefaultExponentialBackoff()
102 | backoff2 := backoff1.Clone()
103 |
104 | if &backoff1 == &backoff2 {
105 | t.Errorf("must not be the same object!")
106 | }
107 |
108 | if backoff1.Attempt() != 0 {
109 | t.Errorf("want 0, got %d", backoff1.Attempt())
110 | }
111 | backoff1.Duration()
112 | if backoff1.Attempt() != 1 {
113 | t.Errorf("want 1, got %d", backoff1.Attempt())
114 | }
115 |
116 | if backoff2.Attempt() != 0 {
117 | t.Errorf("want 0, got %d", backoff2.Attempt())
118 | }
119 |
120 | backoff3 := backoff1.Clone()
121 | if backoff3.Attempt() != 1 {
122 | t.Errorf("want 1, got %d", backoff3.Attempt())
123 | }
124 |
125 | if backoff2.Duration() != backoff3.Duration() {
126 | t.Errorf("durations not equal")
127 | }
128 |
129 | if backoff1.Duration() == backoff3.Duration() {
130 | t.Errorf("durations should not be equal")
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/adapter/util/looper_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package util_test
16 |
17 | import (
18 | "context"
19 | "fmt"
20 | "testing"
21 | "time"
22 |
23 | "github.com/apigee/istio-mixer-adapter/adapter/util"
24 | "github.com/pkg/errors"
25 | "istio.io/istio/mixer/pkg/adapter/test"
26 | )
27 |
28 | func TestPoller(t *testing.T) {
29 | poller := util.Looper{
30 | Env: test.NewEnv(t),
31 | Backoff: util.NewExponentialBackoff(time.Millisecond, time.Millisecond, 2, true),
32 | }
33 |
34 | wait := make(chan struct{})
35 |
36 | called := 0
37 | f := func(ctx context.Context) error {
38 | called++
39 | <-wait
40 | return nil
41 | }
42 |
43 | ctx, cancel := context.WithCancel(context.Background())
44 | poller.Start(ctx, f, time.Millisecond, func(err error) error {
45 | t.Error("should not reach")
46 | return nil
47 | })
48 | defer cancel()
49 |
50 | if called != 0 {
51 | t.Error("called should be 0")
52 | }
53 | wait <- struct{}{}
54 | if called != 1 {
55 | t.Error("called should be 1")
56 | }
57 | }
58 |
59 | func TestPollerQuit(t *testing.T) {
60 | poller := util.Looper{
61 | Env: test.NewEnv(t),
62 | Backoff: util.NewExponentialBackoff(time.Millisecond, time.Millisecond, 2, true),
63 | }
64 |
65 | wait := make(chan struct{})
66 | f := func(ctx context.Context) error {
67 | <-wait
68 | return errors.Errorf("yup")
69 | }
70 |
71 | called := 0
72 | waitErr := make(chan struct{})
73 | ctx, cancel := context.WithCancel(context.Background())
74 | poller.Start(ctx, f, time.Millisecond, func(err error) error {
75 | called++
76 | waitErr <- struct{}{}
77 | return nil
78 | })
79 | defer cancel()
80 |
81 | if called != 0 {
82 | t.Error("called should be 0")
83 | }
84 | wait <- struct{}{}
85 | <-waitErr
86 | if called != 1 {
87 | t.Error("called should be 1")
88 | }
89 | }
90 |
91 | func TestPollerCancel(t *testing.T) {
92 | poller := util.Looper{
93 | Env: test.NewEnv(t),
94 | Backoff: util.NewExponentialBackoff(time.Millisecond, time.Millisecond, 2, true),
95 | }
96 |
97 | wait := make(chan struct{})
98 | f := func(ctx context.Context) error {
99 | t.Log("running func")
100 | wait <- struct{}{}
101 | select {
102 | case <-time.After(5 * time.Millisecond):
103 | t.Error("cancel not called")
104 | case <-ctx.Done():
105 | t.Log("cancel called")
106 | }
107 | t.Log("func done")
108 | wait <- struct{}{}
109 | return nil
110 | }
111 |
112 | ctx, cancel := context.WithCancel(context.Background())
113 | poller.Start(ctx, f, time.Millisecond, func(err error) error {
114 | t.Logf("error: %#v", err)
115 | return nil
116 | })
117 | <-wait
118 | cancel()
119 | <-wait
120 | }
121 |
122 | func TestNewChannelWithWorkerPool(t *testing.T) {
123 | env := test.NewEnv(t)
124 | backoff := util.NewExponentialBackoff(time.Millisecond, time.Millisecond, 2, true)
125 | ctx := context.Background()
126 | errH := func(error) error {
127 | return nil
128 | }
129 | channel := util.NewChannelWithWorkerPool(ctx, 2, 2, env, errH, backoff)
130 | var i = 0
131 | ip := &i
132 |
133 | work := func(ctx context.Context) error {
134 | *ip++
135 | return nil
136 | }
137 | work2 := func(ctx context.Context) error {
138 | return fmt.Errorf("error")
139 | }
140 | channel <- work
141 | time.Sleep(5 * time.Millisecond)
142 |
143 | if *ip != 1 {
144 | t.Errorf("want: 1, got: %d", *ip)
145 | }
146 |
147 | channel <- work2
148 | time.Sleep(5 * time.Millisecond)
149 | if *ip != 1 {
150 | t.Errorf("want: 1, got: %d", *ip)
151 | }
152 |
153 | channel <- work
154 | time.Sleep(5 * time.Millisecond)
155 | if *ip != 2 {
156 | t.Errorf("want: 2, got: %d", *ip)
157 | }
158 |
159 | close(channel)
160 | }
161 |
--------------------------------------------------------------------------------
/adapter/util/reservoir.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package util
16 |
17 | import (
18 | "istio.io/istio/mixer/pkg/adapter"
19 | )
20 |
21 | // NewReservoir sends from one channel to another without blocking until closed.
22 | // Once "in" channel is closed, "out" will continue to drain before closing.
23 | // if buffer limit is reached, new messages (LIFO) are sent to overflow: non-blocking, can be overrun.
24 | func NewReservoir(env adapter.Env, limit int) (chan<- interface{}, <-chan interface{}, <-chan interface{}) {
25 | in := make(chan interface{})
26 | out := make(chan interface{})
27 | overflow := make(chan interface{}, 1)
28 | log := env.Logger()
29 | env.ScheduleDaemon(func() {
30 | var reservoir []interface{}
31 |
32 | outChan := func() chan<- interface{} {
33 | if len(reservoir) == 0 {
34 | return nil // block if empty
35 | }
36 | return out
37 | }
38 |
39 | next := func() interface{} {
40 | if len(reservoir) == 0 {
41 | return nil // block if empty
42 | }
43 | return reservoir[0]
44 | }
45 |
46 | for len(reservoir) > 0 || in != nil {
47 | select {
48 | case v, ok := <-in:
49 | if ok {
50 | if len(reservoir) < limit {
51 | // log.Debugf("queue: %v (%d)", v, len(reservoir))
52 | reservoir = append(reservoir, v)
53 | } else {
54 | // no stopping here
55 | select {
56 | case overflow <- v:
57 | // log.Debugf("overflow: %v", v)
58 | default:
59 | log.Warningf("dropped: %v", v)
60 | }
61 | }
62 | } else {
63 | in = nil
64 | }
65 | case outChan() <- next():
66 | // log.Debugf("dequeue: %v", reservoir[0])
67 | reservoir = reservoir[1:]
68 | }
69 | }
70 | close(out)
71 | close(overflow)
72 | })
73 |
74 | return in, out, overflow
75 | }
76 |
--------------------------------------------------------------------------------
/adapter/util/reservoir_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package util_test
16 |
17 | import (
18 | "sync"
19 | "testing"
20 |
21 | "github.com/apigee/istio-mixer-adapter/adapter/util"
22 | "istio.io/istio/mixer/pkg/adapter/test"
23 | )
24 |
25 | func TestReservoirStream(t *testing.T) {
26 | const limit = 50
27 | env := test.NewEnv(t)
28 |
29 | in, out, _ := util.NewReservoir(env, limit)
30 |
31 | lastVal := -1
32 | wg := sync.WaitGroup{}
33 | wg.Add(1)
34 | go func() {
35 | for v := range out {
36 | vi := v.(int)
37 | t.Logf("receive: %d", vi)
38 | if lastVal+1 != vi {
39 | t.Errorf("want %d, got %d", lastVal+1, vi)
40 | }
41 | lastVal = vi
42 | }
43 | wg.Done()
44 | }()
45 |
46 | for i := 0; i < 20; i++ {
47 | t.Logf("send: %d", i)
48 | in <- i
49 | }
50 | close(in)
51 | wg.Wait()
52 |
53 | if lastVal != 19 {
54 | t.Errorf("lastVal: %d, got %d", lastVal, 19)
55 | }
56 | }
57 |
58 | func TestReservoirLimit(t *testing.T) {
59 | const limit = 2
60 | env := test.NewEnv(t)
61 |
62 | in, out, overflow := util.NewReservoir(env, limit)
63 |
64 | for i := 0; i < 10; i++ {
65 | t.Logf("send: %d", i)
66 | in <- i
67 | }
68 | i := (<-out).(int)
69 | if i != 0 {
70 | t.Errorf("want %d, got %d", 0, i)
71 | }
72 | i = (<-overflow).(int)
73 | if i != 2 {
74 | t.Errorf("want %d, got %d", 2, i)
75 | }
76 |
77 | close(in)
78 |
79 | i = (<-out).(int)
80 | if i != 1 {
81 | t.Errorf("want %d, got %d", 1, i)
82 | }
83 |
84 | _, ok := <-out
85 | if ok {
86 | t.Errorf("channel should be closed")
87 | }
88 |
89 | i, ok = (<-overflow).(int)
90 | if ok {
91 | t.Errorf("channel should be closed")
92 | }
93 | if i != 0 {
94 | t.Errorf("want %d, got %d", 0, i)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/adapter/util/util.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package util
16 |
17 | import (
18 | "bufio"
19 | "fmt"
20 | "net"
21 | "os"
22 | "strings"
23 | )
24 |
25 | // SprintfRedacts truncates secret strings to len(5)
26 | func SprintfRedacts(redacts []interface{}, format string, a ...interface{}) string {
27 | s := fmt.Sprintf(format, a...)
28 | for _, r := range redacts {
29 | if r, ok := r.(string); ok {
30 | truncated := Truncate(r, 5)
31 | s = strings.Replace(s, r, truncated, -1)
32 | }
33 | }
34 | return s
35 | }
36 |
37 | // Truncate truncates secret strings to arbitrary length and adds "..." as indication
38 | func Truncate(in string, end int) string {
39 | out := in
40 | if len(out) > end {
41 | out = fmt.Sprintf("%s...", out[0:end])
42 | }
43 | return out
44 | }
45 |
46 | // ReadPropertiesFile Parses a simple properties file (xx=xx format)
47 | func ReadPropertiesFile(fileName string) (map[string]string, error) {
48 | config := map[string]string{}
49 | f, err := os.Open(fileName)
50 | if err != nil {
51 | return nil, err
52 | }
53 | defer f.Close()
54 |
55 | scan := bufio.NewScanner(f)
56 | for scan.Scan() {
57 | text := scan.Text()
58 | if eq := strings.Index(text, "="); eq >= 0 {
59 | if key := strings.TrimSpace(text[:eq]); len(key) > 0 {
60 | value := ""
61 | if len(text) > eq {
62 | value = strings.TrimSpace(text[eq+1:])
63 | }
64 | config[key] = value
65 | }
66 | }
67 | }
68 |
69 | if err := scan.Err(); err != nil {
70 | return nil, err
71 | }
72 |
73 | return config, nil
74 | }
75 |
76 | // FreePort returns a free port number
77 | func FreePort() (int, error) {
78 | listener, err := net.Listen("tcp", ":0")
79 | if err != nil {
80 | return 0, err
81 | }
82 |
83 | port := listener.Addr().(*net.TCPAddr).Port
84 | if err := listener.Close(); err != nil {
85 | return 0, err
86 | }
87 | return port, nil
88 | }
89 |
--------------------------------------------------------------------------------
/adapter/util/util_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package util
16 |
17 | import (
18 | "fmt"
19 | "io/ioutil"
20 | "log"
21 | "net"
22 | "os"
23 | "strings"
24 | "testing"
25 |
26 | "istio.io/istio/mixer/template/authorization"
27 | )
28 |
29 | func TestSprintfRedacted(t *testing.T) {
30 |
31 | superman := "Clark Kent"
32 | batman := "Bruce Wayne"
33 | ironman := "Tony Stark"
34 |
35 | inst := &authorization.Instance{
36 | Subject: &authorization.Subject{
37 | Properties: map[string]interface{}{
38 | "superman": superman,
39 | "ironman": ironman,
40 | "batman": batman,
41 | },
42 | },
43 | }
44 |
45 | redacts := []interface{}{superman, batman}
46 | result := SprintfRedacts(redacts, "%#v", *inst.Subject)
47 |
48 | if strings.Contains(result, superman) {
49 | t.Errorf("should not contain %s, got: %s", superman, result)
50 | }
51 | if strings.Contains(result, batman) {
52 | t.Errorf("should not contain %s, got: %s", batman, result)
53 | }
54 | if !strings.Contains(result, ironman) {
55 | t.Errorf("should contain %s, got: %s", ironman, result)
56 | }
57 | }
58 |
59 | func TestTruncate(t *testing.T) {
60 | for _, ea := range []struct {
61 | in string
62 | end int
63 | want string
64 | }{
65 | {"hello world", 5, "hello..."},
66 | {"hello", 5, "hello"},
67 | {"he", 5, "he"},
68 | } {
69 | t.Logf("in: '%s' end: %d", ea.in, ea.end)
70 | got := Truncate(ea.in, 5)
71 | if got != ea.want {
72 | t.Errorf("want: '%s', got: '%s'", ea.want, got)
73 | }
74 | }
75 | }
76 |
77 | func TestReadPropertiesFile(t *testing.T) {
78 | tf, err := ioutil.TempFile("", "properties")
79 | if err != nil {
80 | t.Fatalf("TempFile: %v", err)
81 | }
82 | defer os.Remove(tf.Name())
83 |
84 | sourceMap := map[string]string{
85 | "a.valid.port": "apigee-udca-theganyo-apigee-test.apigee.svc.cluster.local:20001",
86 | "a.valid.url": "https://apigee-synchronizer-theganyo-apigee-test.apigee.svc.cluster.local:8843/v1/versions/active/zip",
87 | }
88 | for k, v := range sourceMap {
89 | line := fmt.Sprintf("%s=%s\n", k, v)
90 | if _, err := tf.WriteString(line); err != nil {
91 | log.Fatal(err)
92 | }
93 | }
94 | if err := tf.Close(); err != nil {
95 | log.Fatal(err)
96 | }
97 | props, err := ReadPropertiesFile(tf.Name())
98 | if err != nil {
99 | t.Fatalf("ReadPropertiesFile: %v", err)
100 | }
101 |
102 | for k, v := range sourceMap {
103 | if props[k] != v {
104 | t.Errorf("expected: %s at key: %s, got: %s", v, k, props[k])
105 | }
106 | }
107 | }
108 |
109 | func TestFreeport(t *testing.T) {
110 | p, err := FreePort()
111 | if err != nil {
112 | t.Errorf("shouldn't get error: %v", err)
113 | }
114 |
115 | addr := fmt.Sprintf("localhost:%d", p)
116 | l, err := net.Listen("tcp", addr)
117 | if err != nil {
118 | t.Errorf("shouldn't get error: %v", err)
119 | }
120 |
121 | l.Close()
122 | }
123 |
--------------------------------------------------------------------------------
/apigee-istio/apigee/kvm.go:
--------------------------------------------------------------------------------
1 | package apigee
2 |
3 | import (
4 | "path"
5 | )
6 |
7 | const kvmPath = "keyvaluemaps"
8 |
9 | // KVMService is an interface for interfacing with the Apigee Edge Admin API
10 | // dealing with kvm.
11 | type KVMService interface {
12 | Get(mapname string) (*KVM, *Response, error)
13 | Create(kvm KVM) (*Response, error)
14 | UpdateEntry(kvmName string, entry Entry) (*Response, error)
15 | AddEntry(kvmName string, entry Entry) (*Response, error)
16 | }
17 |
18 | // Entry is an entry in the KVM
19 | type Entry struct {
20 | Name string `json:"name,omitempty"`
21 | Value string `json:"value,omitempty"`
22 | }
23 |
24 | // KVM represents an Apigee KVM
25 | type KVM struct {
26 | Name string `json:"name,omitempty"`
27 | Encrypted bool `json:"encrypted,omitempty"`
28 | Entries []Entry `json:"entry,omitempty"`
29 | }
30 |
31 | // GetValue returns a value from the KVM
32 | func (k *KVM) GetValue(name string) (v string, ok bool) {
33 | for _, e := range k.Entries {
34 | if e.Name == name {
35 | return e.Value, true
36 | }
37 | }
38 | return
39 | }
40 |
41 | // KVMServiceOp represents a KVM service operation
42 | type KVMServiceOp struct {
43 | client *EdgeClient
44 | }
45 |
46 | var _ KVMService = &KVMServiceOp{}
47 |
48 | // Get returns a response given a KVM map name
49 | func (s *KVMServiceOp) Get(mapname string) (*KVM, *Response, error) {
50 | path := path.Join(kvmPath, mapname)
51 | req, e := s.client.NewRequest("GET", path, nil)
52 | if e != nil {
53 | return nil, nil, e
54 | }
55 | returnedKVM := KVM{}
56 | resp, e := s.client.Do(req, &returnedKVM)
57 | if e != nil {
58 | return nil, resp, e
59 | }
60 | return &returnedKVM, resp, e
61 | }
62 |
63 | // Create creates a KVM and returns a response
64 | func (s *KVMServiceOp) Create(kvm KVM) (*Response, error) {
65 | path := path.Join(kvmPath)
66 | req, e := s.client.NewRequest("POST", path, kvm)
67 | if e != nil {
68 | return nil, e
69 | }
70 | resp, e := s.client.Do(req, &kvm)
71 | return resp, e
72 | }
73 |
74 | // UpdateEntry updates a KVM entry
75 | func (s *KVMServiceOp) UpdateEntry(kvmName string, entry Entry) (*Response, error) {
76 | path := path.Join(kvmPath, kvmName, "entries", entry.Name)
77 | req, e := s.client.NewRequest("POST", path, entry)
78 | if e != nil {
79 | return nil, e
80 | }
81 | resp, e := s.client.Do(req, &entry)
82 | return resp, e
83 | }
84 |
85 | // AddEntry add an entry to the KVM
86 | func (s *KVMServiceOp) AddEntry(kvmName string, entry Entry) (*Response, error) {
87 | path := path.Join(kvmPath, kvmName, "entries")
88 | req, e := s.client.NewRequest("POST", path, entry)
89 | if e != nil {
90 | return nil, e
91 | }
92 | resp, e := s.client.Do(req, &entry)
93 | return resp, e
94 | }
95 |
--------------------------------------------------------------------------------
/apigee-istio/apigee/revision.go:
--------------------------------------------------------------------------------
1 | package apigee
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | // Revision represents a revision number. Edge returns rev numbers in string form.
10 | // This marshals and unmarshals between that format and int.
11 | type Revision int
12 |
13 | // MarshalJSON implements the json.Marshaler interface. It marshals from
14 | // a Revision holding an integer value like 2, into a string like "2".
15 | func (r *Revision) MarshalJSON() ([]byte, error) {
16 | rev := fmt.Sprintf("%d", r)
17 | return []byte(rev), nil
18 | }
19 |
20 | // UnmarshalJSON implements the json.Unmarshaler interface. It unmarshals from
21 | // a string like "2" (including the quotes), into an integer 2.
22 | func (r *Revision) UnmarshalJSON(b []byte) error {
23 | rev, e := strconv.ParseInt(strings.TrimSuffix(strings.TrimPrefix(string(b), "\""), "\""), 10, 32)
24 | if e != nil {
25 | return e
26 | }
27 |
28 | *r = Revision(rev)
29 | return nil
30 | }
31 |
32 | func (r Revision) String() string {
33 | return fmt.Sprintf("%d", r)
34 | }
35 |
36 | // RevisionSlice is for sorting
37 | type RevisionSlice []Revision
38 |
39 | func (p RevisionSlice) Len() int { return len(p) }
40 | func (p RevisionSlice) Less(i, j int) bool { return p[i] < p[j] }
41 | func (p RevisionSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
42 |
--------------------------------------------------------------------------------
/apigee-istio/apigee/timestamp.go:
--------------------------------------------------------------------------------
1 | package apigee
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | "time"
8 | )
9 |
10 | // Timestamp represents a time that can be unmarshalled from a JSON string
11 | // formatted as "java time" = milliseconds-since-unix-epoch.
12 | type Timestamp struct {
13 | time.Time
14 | }
15 |
16 | // MarshalJSON creates a JSON representation of this Timestamp
17 | func (t Timestamp) MarshalJSON() ([]byte, error) {
18 | ms := t.Time.UnixNano() / 1000000
19 | stamp := fmt.Sprintf("%d", ms)
20 | return []byte(stamp), nil
21 | }
22 |
23 | // UnmarshalJSON implements the json.Unmarshaler interface.
24 | // Time is expected in RFC3339 or Unix format.
25 | func (t *Timestamp) UnmarshalJSON(b []byte) error {
26 | ms, err := strconv.ParseInt(strings.TrimSuffix(strings.TrimPrefix(string(b), "\""), "\""), 10, 64)
27 | if err != nil {
28 | return err
29 | }
30 | t.Time = time.Unix(int64(ms/1000), (ms-int64(ms/1000)*1000)*1000000)
31 | return nil
32 | }
33 |
34 | func (t Timestamp) String() string {
35 | return fmt.Sprintf("%d", int64(t.Time.UnixNano())/1000000)
36 | }
37 |
38 | // Equal reports whether t and u are equal based on time.Equal
39 | func (t Timestamp) Equal(u Timestamp) bool {
40 | return t.Time.Equal(u.Time)
41 | }
42 |
--------------------------------------------------------------------------------
/apigee-istio/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "os"
19 |
20 | "github.com/apigee/istio-mixer-adapter/apigee-istio/cmd"
21 | "github.com/apigee/istio-mixer-adapter/apigee-istio/shared"
22 | )
23 |
24 | // populated via ldflags
25 | var (
26 | version = "dev"
27 | commit = "unknown"
28 | date = "unknown"
29 | )
30 |
31 | func init() {
32 | shared.BuildInfo = shared.BuildInfoType{
33 | Version: version,
34 | Commit: commit,
35 | Date: date,
36 | }
37 | }
38 |
39 | func main() {
40 | rootCmd := cmd.GetRootCmd(os.Args[1:], shared.Printf, shared.Fatalf)
41 |
42 | if err := rootCmd.Execute(); err != nil {
43 | os.Exit(-1)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/bin/build_grpc_definitions.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script will build the samples/apigee/definitions.yaml file.
4 | # Run this if any of the proto files (config, authorization, analytics) are changed.
5 | # See RELEASING.md for documentation of full release process.
6 |
7 | ISTIO_ROOT="${GOPATH-$HOME/go}/src/github.com/apigee/istio-mixer-adapter"
8 | MIXGEN=$ISTIO_ROOT/vendor/istio.io/istio/mixer/tools/mixgen/main.go
9 | DEFINITIONS_FILE="${ISTIO_ROOT}/samples/apigee/definitions.yaml"
10 |
11 | read -r -d '' DEFINITIONS_BASE <<"EOT"
12 | # This file generated via bin/build_grpc_definitions.sh. Regenerate if
13 | # any of the proto files (config, authorization, analytics) are changed.
14 | #
15 | # Defines the base structures and data map for the Apigee mixer adapter.
16 | # In general, these are static and should not need to be modified.
17 | # However, certain specific behaviors such as where to retrieve an API Key
18 | # could be changed here.
19 | ---
20 | # instance for GRPC template authorization
21 | apiVersion: "config.istio.io/v1alpha2"
22 | kind: instance
23 | metadata:
24 | name: apigee-authorization
25 | namespace: istio-system
26 | spec:
27 | template: apigee-authorization
28 | params:
29 | subject:
30 | properties:
31 | api_key: request.api_key | request.headers["x-api-key"] | ""
32 | json_claims: request.auth.raw_claims | ""
33 | action:
34 | namespace: destination.namespace | "default"
35 | service: api.service | destination.service.host | ""
36 | path: api.operation | request.path | ""
37 | method: request.method | ""
38 | ---
39 | # instance for GRPC template analytics
40 | apiVersion: "config.istio.io/v1alpha2"
41 | kind: instance
42 | metadata:
43 | name: apigee-analytics
44 | namespace: istio-system
45 | spec:
46 | template: apigee-analytics
47 | params:
48 | api_key: request.api_key | request.headers["x-api-key"] | ""
49 | api_proxy: api.service | destination.service.host | ""
50 | response_status_code: response.code | 0
51 | client_ip: source.ip | ip("0.0.0.0")
52 | request_verb: request.method | ""
53 | request_uri: request.path | ""
54 | useragent: request.useragent | ""
55 | client_received_start_timestamp: request.time
56 | client_received_end_timestamp: request.time
57 | target_sent_start_timestamp: request.time
58 | target_sent_end_timestamp: request.time
59 | target_received_start_timestamp: response.time
60 | target_received_end_timestamp: response.time
61 | client_sent_start_timestamp: response.time
62 | client_sent_end_timestamp: response.time
63 | api_claims: # from jwt
64 | json_claims: request.auth.raw_claims | ""
65 | ---
66 | EOT
67 |
68 |
69 | templateDS=$GOPATH/src/istio.io/istio/mixer/template/authorization/template_handler_service.descriptor_set
70 | AUTHORIZATION=$(go run $MIXGEN template -d $templateDS -n apigee-authorization)
71 |
72 | templateDS=$GOPATH/src/github.com/apigee/istio-mixer-adapter/template/analytics/template_handler_service.descriptor_set
73 | ANALYTICS=$(go run $MIXGEN template -d $templateDS -n apigee-analytics)
74 |
75 | templateDS=$GOPATH/src/github.com/apigee/istio-mixer-adapter/adapter/config/config.proto_descriptor
76 | APIGEE=$(go run $MIXGEN adapter -c $templateDS -s=false -t apigee-authorization -t apigee-analytics -n apigee)
77 |
78 | NEWLINE=$'\n'
79 | echo "$DEFINITIONS_BASE $NEWLINE $AUTHORIZATION $NEWLINE $ANALYTICS $NEWLINE $APIGEE" > $DEFINITIONS_FILE
80 |
81 | echo "Generated new file: $DEFINITIONS_FILE"
82 |
--------------------------------------------------------------------------------
/bin/build_proxy_resources.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | #
4 | # If you change the proxies, you must run this and check in the generated proxies.go.
5 | # Remember to update the returned proxy version(s).
6 | #
7 |
8 | if [[ "${GOPATH}" == "" ]]; then
9 | echo "GOPATH not set, please set it."
10 | exit 1
11 | fi
12 |
13 | if [[ `command -v go-bindata` == "" ]]; then
14 | echo "go-bindata not installed, installing..."
15 | go get -u github.com/go-bindata/go-bindata/...
16 | fi
17 |
18 | ADAPTER_DIR="${GOPATH}/src/github.com/apigee/istio-mixer-adapter"
19 | DIST_DIR="${ADAPTER_DIR}/dist"
20 | PROXIES_ZIP_DIR="${DIST_DIR}/proxies"
21 | PROXIES_SOURCE_DIR="${ADAPTER_DIR}/proxies"
22 |
23 | LEGACY_AUTH_PROXY_SRC="${PROXIES_SOURCE_DIR}/auth-proxy-legacy"
24 | INTERNAL_PROXY_SRC="${PROXIES_SOURCE_DIR}/internal-proxy"
25 | HYBRID_AUTH_PROXY_SRC="${PROXIES_SOURCE_DIR}/auth-proxy-hybrid"
26 |
27 | if [ ! -d "${ADAPTER_DIR}" ]; then
28 | echo "could not find istio-mixer-adapter repo, please put it in:"
29 | echo "${ADAPTER_DIR}"
30 | exit 1
31 | fi
32 |
33 | if [ ! -d "${PROXIES_ZIP_DIR}" ]; then
34 | mkdir -p "${PROXIES_ZIP_DIR}"
35 | fi
36 |
37 | # legacy saas auth proxy
38 | ZIP=${PROXIES_ZIP_DIR}/istio-auth-legacy.zip
39 | echo "building ${ZIP}"
40 | rm -f "${ZIP}"
41 | cd "${LEGACY_AUTH_PROXY_SRC}"
42 | zip -qr "${ZIP}" apiproxy
43 |
44 | # hybrid auth proxy
45 | ZIP=${PROXIES_ZIP_DIR}/istio-auth-hybrid.zip
46 | echo "building ${ZIP}"
47 | rm -f "${ZIP}"
48 | cd "${HYBRID_AUTH_PROXY_SRC}"
49 | zip -qr "${ZIP}" apiproxy
50 |
51 | # internal proxy
52 | ZIP=${PROXIES_ZIP_DIR}/istio-internal.zip
53 | echo "building ${ZIP}"
54 | rm -f "${ZIP}"
55 | cd "${INTERNAL_PROXY_SRC}"
56 | zip -qr "${ZIP}" apiproxy
57 |
58 | # create resource
59 | RESOURCE_FILE="${ADAPTER_DIR}/apigee-istio/proxies/proxies.go"
60 | echo "building ${RESOURCE_FILE}"
61 | cd "${DIST_DIR}"
62 | go-bindata -nomemcopy -pkg "proxies" -prefix "proxies" -o "${RESOURCE_FILE}" proxies
63 |
64 | echo "done"
65 |
--------------------------------------------------------------------------------
/bin/build_release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This script will build a new draft release on Github.
4 | # See RELEASING.md for documentation of full release process.
5 |
6 | # use DRYRUN=1 to test build
7 |
8 | if [[ "${GOPATH}" == "" ]]; then
9 | echo "GOPATH not set, please set it."
10 | exit 1
11 | fi
12 |
13 | if [[ `command -v goreleaser` == "" ]]; then
14 | echo "goreleaser not installed, installing..."
15 | cd "${GOPATH}/bin/"
16 | wget https://github.com/goreleaser/goreleaser/releases/download/v0.117.1/goreleaser_Linux_x86_64.tar.gz
17 | tar xfz goreleaser_Linux_x86_64.tar.gz goreleaser
18 | rm goreleaser_Linux_x86_64.tar.gz
19 | fi
20 |
21 | ADAPTER_DIR="${GOPATH}/src/github.com/apigee/istio-mixer-adapter"
22 |
23 | if [ ! -d "${ADAPTER_DIR}" ]; then
24 | echo "could not find istio-mixer-adapter repo, please put it in:"
25 | echo "${ADAPTER_DIR}"
26 | exit 1
27 | fi
28 |
29 | DRYRUN_ARGS=""
30 | if [[ "${DRYRUN}" == "1" ]]; then
31 | echo "Dry run, will not label or push to Github"
32 | DRYRUN_ARGS="--snapshot"
33 | fi
34 |
35 |
36 | cd "${ADAPTER_DIR}"
37 | goreleaser --rm-dist ${DRYRUN_ARGS}
38 |
--------------------------------------------------------------------------------
/bin/install_docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script will install Docker on the local machine. Not recommended for use
4 | # on development machines, it is mainly used for containers in CI.
5 |
6 | if [[ `command -v docker` != "" ]]; then
7 | echo "Docker already installed."
8 | exit 0
9 | fi
10 |
11 | echo "Installing docker client..."
12 | VER=17.12.1
13 | wget -O /tmp/docker-$VER.tgz https://download.docker.com/linux/static/stable/x86_64/docker-$VER-ce.tgz || exit 1
14 | tar -zx -C /tmp -f /tmp/docker-$VER.tgz
15 | mv /tmp/docker/* /usr/bin/
16 |
--------------------------------------------------------------------------------
/bin/install_gcloud.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script will install gcloud on the local machine. Not recommended for use
4 | # on development machines, it is mainly used for containers in CI.
5 |
6 | if [[ `command -v gcloud` != "" ]]; then
7 | echo "gcloud already installed."
8 | exit 0
9 | fi
10 |
11 | echo "Installing gcloud..."
12 | wget -O /tmp/gcloud.tar.gz https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-234.0.0-linux-x86_64.tar.gz || exit 1
13 | sudo tar -zx -C /opt -f /tmp/gcloud.tar.gz
14 |
15 | # Need to ln so that `sudo gcloud` works
16 | sudo ln -s /opt/google-cloud-sdk/bin/gcloud /usr/bin/gcloud
17 |
--------------------------------------------------------------------------------
/bin/install_protoc.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if [[ `command -v protoc` == "" ]]; then
4 | mkdir /tmp/protoc
5 | wget -O /tmp/protoc/protoc.zip https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-linux-x86_64.zip
6 | unzip /tmp/protoc/protoc.zip -d /tmp/protoc
7 | sudo mv -f /tmp/protoc/bin/protoc /usr/bin/
8 | sudo mv -f /tmp/protoc/include/google /usr/local/include/
9 | rm -rf /tmp/protoc
10 | fi
11 |
--------------------------------------------------------------------------------
/bin/lint_and_vet.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | golint ./adapter/... || exit 1
3 | go vet ./apigee-istio/... ./adapter/... || exit 1
4 |
--------------------------------------------------------------------------------
/grpc-server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM scratch
2 | ADD ca-certificates.crt /etc/ssl/certs/
3 | ADD grpc_health_probe /
4 | ADD apigee-adapter /
5 | ENTRYPOINT ["/apigee-adapter"]
6 | CMD ["--address=:5000", "--log_output_level=adapters:info"]
7 |
--------------------------------------------------------------------------------
/grpc-server/Dockerfile_debug:
--------------------------------------------------------------------------------
1 | FROM ubuntu:xenial
2 | ADD ca-certificates.crt /etc/ssl/certs/
3 | ADD grpc_health_probe /
4 | ADD apigee-adapter /
5 | ENTRYPOINT ["/apigee-adapter"]
6 | CMD ["--address=:5000", "--log_output_level=adapters:debug"]
7 |
--------------------------------------------------------------------------------
/grpc-server/README.md:
--------------------------------------------------------------------------------
1 | Build binary and docker image:
2 |
3 | bin/build_adapter_docker.sh
4 |
5 | Deploy docker image into Kubernetes
6 |
7 | kubectl apply -f samples/apigee/adapter.yaml
8 |
9 | FYI: If needed, root certs file is created via:
10 |
11 | curl -o ca-certificates.crt https://curl.haxx.se/ca/cacert.pem
12 |
--------------------------------------------------------------------------------
/grpc-server/grpc_health_probe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apigee/istio-mixer-adapter/dcd54df6a221d07eebff6e82195b5caf3316b42b/grpc-server/grpc_health_probe
--------------------------------------------------------------------------------
/grpc-server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | coreLog "log"
6 | "os"
7 |
8 | "github.com/apigee/istio-mixer-adapter/adapter"
9 | "github.com/spf13/cobra"
10 | "istio.io/istio/pkg/log"
11 | )
12 |
13 | var address string
14 |
15 | func main() {
16 | options := log.DefaultOptions()
17 |
18 | rootCmd := &cobra.Command{
19 | Run: func(cmd *cobra.Command, args []string) {
20 |
21 | if err := log.Configure(options); err != nil {
22 | coreLog.Fatal(err)
23 | }
24 |
25 | s, err := adapter.NewGRPCAdapter(address)
26 | if err != nil {
27 | fmt.Printf("unable to start server: %v", err)
28 | os.Exit(-1)
29 | }
30 |
31 | shutdown := make(chan error, 1)
32 | go func() {
33 | s.Run(shutdown)
34 | }()
35 | _ = <-shutdown
36 | },
37 | }
38 | rootCmd.Flags().StringVarP(&address, "address", "a", ":5000", `Address to use for Adapter's gRPC API`)
39 |
40 | options.AttachCobraFlags(rootCmd)
41 | rootCmd.SetArgs(os.Args[1:])
42 | rootCmd.Execute()
43 | }
44 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/istio-auth.xml:
--------------------------------------------------------------------------------
1 |
2 | istio-auth
3 | istio-auth
4 | 1576711605435
5 | 1576711605435
6 | /istio-auth
7 |
8 | AccessTokenRequest
9 | Create-OAuth-Request
10 | Create-Refresh-Request
11 | DistributedQuota
12 | Eval-Quota-Result
13 | Extract-API-Key
14 | Extract-OAuth-Params
15 | Extract-Refresh-Params
16 | Extract-Revoke-Params
17 | Extract-Rotate-Variables
18 | Generate-Access-Token
19 | Generate-JWK
20 | Generate-VerifyKey-Token
21 | Get-Private-Key
22 | Get-Public-Keys
23 | JavaCallout
24 | Raise-Fault-Unknown-Request
25 | RefreshAccessToken
26 | Retrieve-Cert
27 | RevokeRefreshToken
28 | Send-JWK-Message
29 | Send-Version
30 | Set-JWT-Variables
31 | Set-Quota-Response
32 | Set-Quota-Variables
33 | Set-Response
34 | Update-Keys
35 | Verify-API-Key
36 | Decode-Basic-Authentication
37 | Clear-API-Key
38 | Access-App-Info
39 | Products-to-JSON
40 |
41 |
42 | default
43 |
44 |
45 | java://istio-products-javacallout-2.0.0.jar
46 | jsc://eval-quota-result.js
47 | jsc://generate-jwk.js
48 | jsc://jsrsasign-all-min.js
49 | jsc://jwt-initialization.js
50 | jsc://send-jwk-response.js
51 | jsc://set-jwt-variables.js
52 | jsc://set-quota-variables.js
53 | jsc://set-response.js
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Access-App-Info.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Access App Info
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/AccessTokenRequest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | AccessTokenRequest
4 |
5 |
6 |
7 |
8 | FORM_PARAM
9 |
10 | 300000
11 | false
12 | GenerateAccessToken
13 | 3600000
14 |
15 | FORM_PARAM
16 |
17 | true
18 |
19 | password
20 | client_credentials
21 |
22 |
23 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Clear-API-Key.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Clear API Key
4 |
5 |
6 | apikey
7 |
8 |
9 |
10 | true
11 |
12 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Create-OAuth-Request.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Create OAuth Request
4 |
5 |
6 |
7 |
8 | {client_id}
9 | {client_secret}
10 | {grant_type}
11 | {username}
12 | {password}
13 | {scp}
14 |
15 | /token
16 |
17 |
18 | token_expiry
19 | 3000
20 |
21 |
22 | refresh_token_expiry
23 | 3600000
24 |
25 | true
26 |
27 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Create-Refresh-Request.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Create Refresh Request
4 |
5 |
6 |
7 |
8 | {client_id}
9 | {client_secret}
10 | {refresh_token}
11 | {grant_type}
12 |
13 | /token
14 |
15 |
16 | token_expiry
17 | 300
18 |
19 |
20 | refresh_token_expiry
21 | 3600
22 |
23 | true
24 |
25 |
26 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Decode-Basic-Authentication.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Decode Basic Authentication
4 | Decode
5 | true
6 |
7 |
8 |
9 | request.header.Authorization
10 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/DistributedQuota.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | DistributedQuota
4 |
5 |
6 |
7 |
8 |
9 | true
10 | true
11 | 2019-01-01 00:00:00
12 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Eval-Quota-Result.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Eval Quota Result
4 |
5 | jsc://eval-quota-result.js
6 |
7 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Extract-API-Key.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Extract API Key
4 |
5 |
6 |
9 |
12 | true
13 |
14 |
15 | $.apiKey
16 |
17 |
18 | request
19 |
20 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Extract-OAuth-Params.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Extract OAuth Params
4 |
5 |
6 |
9 |
12 | true
13 |
14 |
15 | $.client_id
16 |
17 |
18 | $.client_id
19 |
20 |
21 | $.client_secret
22 |
23 |
24 | $.grant_type
25 |
26 |
27 | $.username
28 |
29 |
30 | $.password
31 |
32 |
33 | $.scope
34 |
35 |
36 | request
37 |
38 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Extract-Refresh-Params.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Extract Refresh Params
4 |
5 |
6 |
9 |
12 | true
13 |
14 |
15 | $.client_id
16 |
17 |
18 | $.client_id
19 |
20 |
21 | $.client_secret
22 |
23 |
24 | $.refresh_token
25 |
26 |
27 | $.grant_type
28 |
29 |
30 | request
31 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Extract-Revoke-Params.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Extract Revoke Params
4 |
5 |
6 |
7 | $.client_id
8 |
9 |
10 | $.client_secret
11 |
12 |
13 | $.token
14 |
15 |
16 | $.token_type_hint
17 |
18 |
19 | request
20 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Extract-Rotate-Variables.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Extract Rotate Variables
4 |
5 | true
6 |
7 |
8 | $.kid
9 |
10 |
11 | $.certificate
12 |
13 |
14 | $.private_key
15 |
16 |
17 | request
18 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Generate-Access-Token.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Generate Access Token
4 | RS256
5 |
6 |
7 |
8 |
9 |
10 | istio
11 | 15m
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | jwtmessage
22 |
23 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Generate-JWK.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Generate JWK
4 |
5 | jsc://jwt-initialization.js
6 | jsc://jsrsasign-all-min.js
7 | jsc://generate-jwk.js
8 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Generate-VerifyKey-Token.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Generate VerifyKey Token
4 | RS256
5 |
6 |
7 |
8 |
9 |
10 | istio
11 | 15m
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | jwtmessage
20 |
21 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Get-Private-Key.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Get Private Key
4 |
5 |
6 | false
7 | 86400
8 |
9 |
10 |
11 | private_key
12 |
13 |
14 |
15 |
16 | certificate1_kid
17 |
18 |
19 | environment
20 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Get-Public-Keys.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Get Public Keys
4 |
5 |
6 | false
7 | 86400
8 |
9 |
10 |
11 | certificate1
12 |
13 |
14 |
15 |
16 | certificate2
17 |
18 |
19 |
20 |
21 | certificate1_kid
22 |
23 |
24 |
25 |
26 | certificate2_kid
27 |
28 |
29 | environment
30 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/JavaCallout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | JavaCallout
4 |
5 |
6 | io.apigee.microgateway.javacallout.Callout
7 | java://istio-products-javacallout-2.0.0.jar
8 |
9 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Products-to-JSON.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Products to JSON
4 |
5 |
6 | apiCredential
7 | AccessEntity.ChildNodes.Access-App-Info.App.Credentials
8 |
9 |
10 | Credentials/Credential
11 | Credentials/Credential/ApiProducts/ApiProduct
12 |
13 |
14 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Raise-Fault-Unknown-Request.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Raise Fault-Unknown-Request
4 |
5 |
6 |
7 |
8 |
9 |
10 | {
11 | "error":"invalid_request",
12 | "error_description": "invalid request"
13 | }
14 |
15 | 400
16 | Bad Request
17 |
18 |
19 | true
20 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/RefreshAccessToken.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | RefreshAccessToken
4 |
5 |
6 |
7 |
8 | FORM_PARAM
9 |
10 | 300000
11 | false
12 | RefreshAccessToken
13 |
14 | FORM_PARAM
15 |
16 | true
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Retrieve-Cert.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Retrieve Cert
4 |
5 | false
6 | 2
7 |
8 |
9 | certificate1
10 |
11 |
12 |
13 |
14 | certificate1_kid
15 |
16 |
17 | environment
18 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/RevokeRefreshToken.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | RevokeRefreshToken
4 |
5 | false
6 | InvalidateToken
7 |
8 |
9 | token
10 |
11 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Send-JWK-Message.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Send JWK Message
4 |
5 | jsc://send-jwk-response.js
6 |
7 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Send-Version.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Send Version
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {"version":"1.4.1"}
12 |
13 |
14 | true
15 |
16 |
17 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Set-JWT-Variables.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Set JWT Variables
4 |
5 |
6 | jsc://set-jwt-variables.js
7 |
8 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Set-Quota-Response.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Set Quota Response
4 |
5 |
6 |
7 |
8 | {
9 | "allowed": @quota.allow#,
10 | "used": @quota.used#,
11 | "exceeded": @quota.exceeded#,
12 | "expiryTime": @ratelimit.DistributedQuota.expiry.time#,
13 | "timestamp": @system.timestamp#
14 | }
15 |
16 | true
17 |
18 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Set-Quota-Variables.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Set Quota Variables
4 |
5 | jsc://set-quota-variables.js
6 |
7 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Set-Response.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Set Response
4 |
5 |
6 | jsc://set-response.js
7 |
8 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Update-Keys.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Update Keys
4 |
5 | false
6 | 2
7 |
8 |
9 | certificate2
10 |
11 |
12 |
13 |
14 |
15 | certificate2_kid
16 |
17 |
18 |
19 |
20 |
21 | certificate1
22 |
23 |
24 |
25 |
26 |
27 | certificate1_kid
28 |
29 |
30 |
31 |
32 |
33 | private_key
34 |
35 |
36 |
37 | environment
38 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/policies/Verify-API-Key.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Verify API Key
4 |
5 |
6 |
7 | (fault.name="InvalidApiKey")
8 |
9 | fault_invalid_key
10 |
11 |
12 | (fault.name="FailedToResolveAPIKey")
13 |
14 | fault_missing_key
15 |
16 |
17 | (fault.name="InvalidApiKeyForGivenResource")
18 |
19 | fault_insufficient_key_permissions
20 |
21 |
22 | (fault.name="ApiKeyNotApproved")
23 |
24 | fault_key_not_approved
25 |
26 |
27 | (fault.name="invalid_client-app_not_approved")
28 |
29 | fault_invalid_client_app
30 |
31 |
32 | (fault.name="DeveloperStatusNotActive")
33 |
34 | fault_developer_inactive
35 |
36 |
37 | (fault.name="CompanyStatusNotActive")
38 |
39 | fault_company_inactive
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/resources/java/istio-products-javacallout-2.0.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apigee/istio-mixer-adapter/dcd54df6a221d07eebff6e82195b5caf3316b42b/proxies/auth-proxy-hybrid/apiproxy/resources/java/istio-products-javacallout-2.0.0.jar
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/resources/jsc/eval-quota-result.js:
--------------------------------------------------------------------------------
1 | // ensures that in the response, used <= allowed
2 | // and exceeded is a count of the excess of used > allow
3 | // assumes that allow is set arbitrarily high in the actual policy
4 | var used = context.getVariable("ratelimit.DistributedQuota.used.count")
5 | var allowed = context.getVariable("quota.allow")
6 | if (used > allowed) {
7 | var exceeded = used - allowed
8 | context.setVariable("quota.used", allowed)
9 | context.setVariable("quota.exceeded", exceeded.toFixed(0))
10 | } else {
11 | var exceeded = 0
12 | context.setVariable("quota.used", used)
13 | context.setVariable("quota.exceeded", exceeded.toFixed(0))
14 | }
15 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/resources/jsc/generate-jwk.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | const alg = "RS256";
16 | const use = "sig";
17 | var certificate1 = context.getVariable("private.certificate1");
18 | var certificate2 = context.getVariable("private.certificate2");
19 | var certificatelist = {};
20 |
21 | certificatelist.keys = [];
22 |
23 | if (!certificate1) {
24 | throw Error("No certificate found");
25 | }
26 |
27 | var key1 = KEYUTIL.getKey(certificate1);
28 | var jwk1 = KEYUTIL.getJWKFromKey(key1);
29 | var cert1_kid = context.getVariable("private.certificate1_kid") || null;
30 |
31 | if (cert1_kid !== null) {
32 | jwk1.kid = cert1_kid;
33 | jwk1.alg = alg;
34 | jwk1.use = use;
35 | }
36 | certificatelist.keys.push(jwk1);
37 |
38 | if (certificate2) {
39 | var key2 = KEYUTIL.getKey(certificate2);
40 | var jwk2 = KEYUTIL.getJWKFromKey(key2);
41 | var cert2_kid = context.getVariable("private.certificate2_kid") || null;
42 | if (cert2_kid !== null) {
43 | jwk2.kid = cert2_kid;
44 | jwk2.alg = alg;
45 | jwk2.use = use;
46 | }
47 | certificatelist.keys.push(jwk2);
48 | }
49 |
50 | context.setVariable("jwkmessage", JSON.stringify(certificatelist));
51 |
52 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/resources/jsc/jwt-initialization.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | /*
16 | *
17 | * A dummy navigator object - jsrasign expects to be running in a browser and expects
18 | * these to be in the global namespace
19 | *
20 | */
21 |
22 | var navigator = navigator || {appName : ''};
23 | var window = window || {};
24 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/resources/jsc/send-jwk-response.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | //send response
16 | context.setVariable("response.header.Content-Type","application/json");
17 | context.setVariable("response.header.Cache-Control","no-store");
18 | context.setVariable("response.content", context.getVariable("jwkmessage"));
19 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/resources/jsc/set-jwt-variables.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | var apiCredential = JSON.parse(context.getVariable('apiCredential'));
16 | var apiKey = context.getVariable('apikey');
17 | //{"Credentials":{"Credential":[{"Attributes":{},"ConsumerKey":"xxx","ConsumerSecret":"xx","ExpiresAt":"-1","IssuedAt":"1530046158362","ApiProducts":{"ApiProduct":{"Name":"details product","Status":"approved"}},"Scopes":{},"Status":"approved"}]}}
18 | var credentials = apiCredential.Credentials.Credential;
19 |
20 | var apiProductsList = [];
21 | try {
22 | credentials.forEach(function(credential) {
23 | if (credential.ConsumerKey == apiKey) {
24 | credential.ApiProducts.ApiProduct.forEach(function(apiProduct){
25 | apiProductsList.push(apiProduct.Name);
26 | });
27 | }
28 | });
29 | } catch (err) {
30 | print(err);
31 | }
32 |
33 | var scope = context.getVariable("oauthv2accesstoken.AccessTokenRequest.scope");
34 | if (scope) {
35 | var scopearr = scope.split(" ");
36 | context.setVariable("scope", scopearr.join());
37 | } else {
38 | context.setVariable("scope", "");
39 | }
40 |
41 | context.setVariable("apiProductList", apiProductsList.join());
42 | context.setVariable("nbf", new Date().toUTCString());
43 | context.setVariable("iss", context.getVariable("proxyProto") + "://" + context.getVariable("proxyHost") + context.getVariable("proxy.basepath") + context.getVariable("proxy.pathsuffix"));
44 | context.setVariable("jti", 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
45 | var r = Math.random() * 16 | 0,
46 | v = c == 'x' ? r : (r & 0x3 | 0x8);
47 | return v.toString(16);
48 | }));
49 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/resources/jsc/set-quota-variables.js:
--------------------------------------------------------------------------------
1 | context.setVariable('quota.identifier', request.body.asJSON.identifier);
2 | context.setVariable("quota.allow", request.body.asJSON.allow);
3 | context.setVariable("quota.interval", request.body.asJSON.interval);
4 | context.setVariable("quota.unit", request.body.asJSON.timeUnit);
5 | context.setVariable("quota.weight", request.body.asJSON.weight);
6 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-hybrid/apiproxy/resources/jsc/set-response.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | //prepare response object
16 |
17 | var jws = {
18 | token: context.getVariable('jwtmessage')
19 | };
20 | //if refresh token exists, add it to response
21 | if (context.getVariable('grant_type') === "password") {
22 | jws.refresh_token = context.getVariable("oauthv2accesstoken.AccessTokenRequest.refresh_token");
23 | jws.refresh_token_expires_in = context.getVariable("oauthv2accesstoken.AccessTokenRequest.refresh_token_expires_in");
24 | jws.refresh_token_issued_at = context.getVariable("oauthv2accesstoken.AccessTokenRequest.refresh_token_issued_at") ;
25 | jws.refresh_token_status = context.getVariable("oauthv2accesstoken.AccessTokenRequest.refresh_token_status");
26 | }
27 | //send response
28 | context.setVariable("response.header.Content-Type","application/json");
29 | context.setVariable("response.header.Cache-Control","no-store");
30 | context.setVariable("response.content", JSON.stringify(jws));
31 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/README.md:
--------------------------------------------------------------------------------
1 | # istio-auth
2 |
3 | An Apigee Edge proxy to support generating, refreshing and revoking access tokens for istio-mixer-adapter.
4 |
5 | ## Development
6 |
7 | IMPORTANT: If you change the proxy source, you must run `bin/build_proxy_resources.sh` and rebuild the
8 | `apigee-istio` CLI to include it.
9 |
10 | ## Description
11 |
12 | The istio-auth proxy acts as an auth server and provides several functions:
13 |
14 | * Provides a list of all products in the org (/products)
15 | * Provides a signed JWT if the API Key is valid (/verifyApiKey)
16 | * Generates an access token, which is a signed JWT. Supports client_credentials grant type (/token)
17 | * Refresh an access token (/refresh)
18 | * Revoke a refresh token (/revoke)
19 | * Manage quotas (/quotas)
20 |
21 | ### Installation
22 |
23 | This proxy will automatically be installed during provisioning with the apigee-istio CLI.
24 |
25 | ### Customizations
26 |
27 | #### How do I set custom expiry?
28 |
29 | In the flow named 'Obtain Access Token' you'll find an Assign Message Policy called 'Create OAuth Request'.
30 | Change the value here:
31 |
32 |
33 | token_expiry
34 | 300000
35 |
36 |
37 |
38 | #### How can I get refresh tokens?
39 |
40 | The OAuth v2 policy supports password grant. Send a request as below:
41 |
42 | POST /token
43 | {
44 | "client_id":"foo",
45 | "client_secret":"foo",
46 | "grant_type":"password",
47 | "username":"blah",
48 | "password": "blah"
49 | }
50 |
51 | If valid, the response will contain a refresh token.
52 |
53 | #### How do I refresh an access_token?
54 |
55 | Send a request as below:
56 |
57 | POST /refresh
58 | {
59 | "grant_type": "refresh_token",
60 | "refresh_token": "foo",
61 | "client_id":"blah",
62 | "client_secret":"blah"
63 | }
64 |
65 | If valid, the response will contain a new access_token.
66 |
67 | #### What grant types are supported?
68 |
69 | * client_credentials
70 | * password
71 | * refresh_token
72 |
73 | Users may extend the Apigee OAuth v2 policy to add support for additional grant types.
74 |
75 | #### Support for JSON Web Keys
76 |
77 | istio-mixer-adapter stores private keys and public keys in an encrypted kvm on Apigee Edge.
78 | The proxy exposes an endpoint '/certs' to return public keys as JWK Set.
79 |
80 | #### Support for JWT "kid" - Key Identifiers.
81 |
82 | If the KVM includes a field called 'certificate1_kid' (value can be any string), the JWT header will include the "kid".
83 |
84 | {
85 | "alg": "RS256",
86 | "typ": "JWT",
87 | "kid": "1"
88 | }
89 |
90 | The "kid" will be leveraged during validation of JWTs.
91 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/istio-auth.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | /istio-auth
4 |
5 | 1529864810874
6 | srinandans@google.com
7 | istio-auth
8 | istio-auth
9 | 1544596340972
10 | defaultUser
11 | SHA-512:35f8d4ae9a0f1a3ae63cda4936914fb71ede499a50e6f0e7746e11cafd32e4991d804524468928ebd15f1af698f52239bc26ebedaa78f7be4b8271ee0fa97ece
12 |
13 | Access-App-Info-2
14 | Access-App-Info
15 | AccessTokenRequest
16 | Authenticate-Call
17 | AuthenticationError
18 | Create-OAuth-Request
19 | Create-Refresh-Request
20 | DistributedQuota
21 | Extract-API-Key
22 | Extract-OAuth-Params
23 | Extract-Refresh-Params
24 | Extract-Revoke-Params
25 | Extract-Rotate-Variables
26 | Generate-Access-Token
27 | Generate-JWK
28 | Generate-VerifyKey-Token
29 | Get-Private-Key
30 | Get-Public-Keys
31 | JavaCallout
32 | Products-to-JSON-2
33 | Products-to-JSON
34 | Raise-Fault-Unknown-Request
35 | RefreshAccessToken
36 | Retrieve-Cert
37 | RevokeRefreshToken
38 | Send-JWK-Message
39 | Send-Version
40 | Set-JWT-Variables
41 | Set-Quota-Variables
42 | Set-Response
43 | Update-Keys
44 | Verify-API-Key
45 |
46 |
47 | default
48 |
49 |
50 | java://istio-products-javacallout-2.0.0.jar
51 | jsc://generate-jwk.js
52 | jsc://jsrsasign-all-min.js
53 | jsc://jwt-initialization.js
54 | jsc://send-jwk-response.js
55 | jsc://set-jwt-variables.js
56 | jsc://set-quota-variables.js
57 | jsc://set-response.js
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Access-App-Info-2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Access App Info 2
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Access-App-Info.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Access App Info
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/AccessTokenRequest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | AccessTokenRequest
4 |
5 |
6 |
7 |
8 | FORM_PARAM
9 |
10 | 300000
11 | false
12 | GenerateAccessToken
13 | 3600000
14 |
15 | FORM_PARAM
16 |
17 | true
18 |
19 | password
20 | client_credentials
21 |
22 |
23 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Authenticate-Call.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Authenticate Call
4 |
5 |
6 | false
7 |
8 |
9 | {request.header.Authorization}
10 |
11 | GET
12 | /edgemicro/bootstrap/organization/{organization.name}/environment/{environment.name}
13 |
14 |
15 | calloutResponse
16 |
17 |
18 | https://edgemicroservices.apigee.net
19 |
20 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/AuthenticationError.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | AuthenticationError
4 |
5 |
6 |
7 |
8 |
9 | {
10 | "error":"unauthorized",
11 | "error_description": "authentication failed"
12 | }
13 |
14 | 401
15 | Unauthorized
16 |
17 |
18 | true
19 |
20 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Create-OAuth-Request.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Create OAuth Request
4 |
5 |
6 |
7 |
8 | {client_id}
9 | {client_secret}
10 | {grant_type}
11 | {username}
12 | {password}
13 | {scp}
14 |
15 | /token
16 |
17 |
18 | token_expiry
19 | 3000
20 |
21 |
22 | refresh_token_expiry
23 | 3600000
24 |
25 | true
26 |
27 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Create-Refresh-Request.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Create Refresh Request
4 |
5 |
6 |
7 |
8 | {client_id}
9 | {client_secret}
10 | {refresh_token}
11 | {grant_type}
12 |
13 | /token
14 |
15 |
16 | token_expiry
17 | 300
18 |
19 |
20 | refresh_token_expiry
21 | 3600
22 |
23 | true
24 |
25 |
26 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/DistributedQuota.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | DistributedQuota
4 |
5 |
6 |
7 |
8 |
9 |
10 | true
11 | true
12 | 2019-01-01 00:00:00
13 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Eval-Quota-Result.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Eval Quota Result
4 |
5 | jsc://eval-quota-result.js
6 |
7 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Extract-API-Key.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Extract API Key
4 |
5 |
6 |
9 |
12 | true
13 |
14 |
15 | $.apiKey
16 |
17 |
18 | request
19 |
20 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Extract-OAuth-Params.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Extract OAuth Params
4 |
5 |
6 |
9 |
12 | true
13 |
14 |
15 | $.client_id
16 |
17 |
18 | $.client_id
19 |
20 |
21 | $.client_secret
22 |
23 |
24 | $.grant_type
25 |
26 |
27 | $.username
28 |
29 |
30 | $.password
31 |
32 |
33 | $.scope
34 |
35 |
36 | request
37 |
38 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Extract-Refresh-Params.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Extract Refresh Params
4 |
5 |
6 |
9 |
12 | true
13 |
14 |
15 | $.client_id
16 |
17 |
18 | $.client_secret
19 |
20 |
21 | $.refresh_token
22 |
23 |
24 | $.grant_type
25 |
26 |
27 | request
28 |
29 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Extract-Revoke-Params.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Extract Revoke Params
4 |
5 |
6 |
7 | $.client_id
8 |
9 |
10 | $.client_secret
11 |
12 |
13 | $.token
14 |
15 |
16 | $.token_type_hint
17 |
18 |
19 | request
20 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Extract-Rotate-Variables.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Extract Rotate Variables
4 |
5 | true
6 |
7 |
8 | $.kid
9 |
10 |
11 | $.certificate
12 |
13 |
14 | $.private_key
15 |
16 |
17 | request
18 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Generate-Access-Token.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Generate Access Token
4 | RS256
5 |
6 |
7 |
8 |
9 |
10 | istio
11 | 15m
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | jwtmessage
22 |
23 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Generate-JWK.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Generate JWK
4 |
5 | jsc://jwt-initialization.js
6 | jsc://jsrsasign-all-min.js
7 | jsc://generate-jwk.js
8 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Generate-VerifyKey-Token.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Generate VerifyKey Token
4 | RS256
5 |
6 |
7 |
8 |
9 |
10 | istio
11 | 15m
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | jwtmessage
20 |
21 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Get-Private-Key.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Get Private Key
4 |
5 |
6 | false
7 | 86400
8 |
9 |
10 |
11 | private_key
12 |
13 |
14 |
15 |
16 | certificate1_kid
17 |
18 |
19 | environment
20 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Get-Public-Keys.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Get Public Keys
4 |
5 |
6 | false
7 | 86400
8 |
9 |
10 |
11 | certificate1
12 |
13 |
14 |
15 |
16 | certificate2
17 |
18 |
19 |
20 |
21 | certificate1_kid
22 |
23 |
24 |
25 |
26 | certificate2_kid
27 |
28 |
29 | environment
30 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/JavaCallout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | JavaCallout
4 |
5 |
6 | io.apigee.microgateway.javacallout.Callout
7 | java://istio-products-javacallout-2.0.0.jar
8 |
9 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Products-to-JSON-2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Products to JSON 2
4 |
5 |
6 | apiCredential
7 | AccessEntity.ChildNodes.Access-App-Info-2.App.Credentials
8 |
9 |
10 | Credentials/Credential
11 | Credentials/Credential/ApiProducts/ApiProduct
12 |
13 |
14 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Products-to-JSON.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Products to JSON
4 |
5 |
6 | apiCredential
7 | AccessEntity.ChildNodes.Access-App-Info.App.Credentials
8 |
9 |
10 | Credentials/Credential
11 | Credentials/Credential/ApiProducts/ApiProduct
12 |
13 |
14 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Raise-Fault-Unknown-Request.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Raise Fault-Unknown-Request
4 |
5 |
6 |
7 |
8 |
9 |
10 | {
11 | "error":"invalid_request",
12 | "error_description": "invalid request"
13 | }
14 |
15 | 400
16 | Bad Request
17 |
18 |
19 | true
20 |
21 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/RefreshAccessToken.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | RefreshAccessToken
4 |
5 |
6 |
7 |
8 | FORM_PARAM
9 |
10 | 300000
11 | false
12 | RefreshAccessToken
13 |
14 | FORM_PARAM
15 |
16 | true
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Retrieve-Cert.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Retrieve Cert
4 |
5 | false
6 | 2
7 |
8 |
9 | certificate1
10 |
11 |
12 |
13 |
14 | certificate1_kid
15 |
16 |
17 | environment
18 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/RevokeRefreshToken.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | RevokeRefreshToken
4 |
5 | false
6 | InvalidateToken
7 |
8 |
9 | token
10 |
11 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Send-JWK-Message.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Send JWK Message
4 |
5 | jsc://send-jwk-response.js
6 |
7 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Send-Version.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Send Version
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {"version":"1.4.1"}
12 |
13 |
14 | true
15 |
16 |
17 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Set-JWT-Variables.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Set JWT Variables
4 |
5 |
6 | jsc://set-jwt-variables.js
7 |
8 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Set-Quota-Response.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Set Quota Response
4 |
5 |
6 |
7 |
8 | {
9 | "allowed": @quota.allow#,
10 | "used": @quota.used#,
11 | "exceeded": @quota.exceeded#,
12 | "expiryTime": @ratelimit.DistributedQuota.expiry.time#,
13 | "timestamp": @system.timestamp#
14 | }
15 |
16 | true
17 |
18 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Set-Quota-Variables.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Set Quota Variables
4 |
5 | jsc://set-quota-variables.js
6 |
7 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Set-Response.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Set Response
4 |
5 |
6 | jsc://set-response.js
7 |
8 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Update-Keys.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Update Keys
4 |
5 | false
6 | 2
7 |
8 |
9 | certificate2
10 |
11 |
12 |
13 |
14 |
15 | certificate2_kid
16 |
17 |
18 |
19 |
20 |
21 | certificate1
22 |
23 |
24 |
25 |
26 |
27 | certificate1_kid
28 |
29 |
30 |
31 |
32 |
33 | private_key
34 |
35 |
36 |
37 | environment
38 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/policies/Verify-API-Key.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Verify API Key
4 |
5 |
6 |
7 | (fault.name="InvalidApiKey")
8 |
9 | fault_invalid_key
10 |
11 |
12 | (fault.name="FailedToResolveAPIKey")
13 |
14 | fault_missing_key
15 |
16 |
17 | (fault.name="InvalidApiKeyForGivenResource")
18 |
19 | fault_insufficient_key_permissions
20 |
21 |
22 | (fault.name="ApiKeyNotApproved")
23 |
24 | fault_key_not_approved
25 |
26 |
27 | (fault.name="invalid_client-app_not_approved")
28 |
29 | fault_invalid_client_app
30 |
31 |
32 | (fault.name="DeveloperStatusNotActive")
33 |
34 | fault_developer_inactive
35 |
36 |
37 | (fault.name="CompanyStatusNotActive")
38 |
39 | fault_company_inactive
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/resources/java/istio-products-javacallout-2.0.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apigee/istio-mixer-adapter/dcd54df6a221d07eebff6e82195b5caf3316b42b/proxies/auth-proxy-legacy/apiproxy/resources/java/istio-products-javacallout-2.0.0.jar
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/resources/jsc/eval-quota-result.js:
--------------------------------------------------------------------------------
1 | // ensures that in the response, used <= allowed
2 | // and exceeded is a count of the excess of used > allow
3 | // assumes that allow is set arbitrarily high in the actual policy
4 | var used = context.getVariable("ratelimit.DistributedQuota.used.count")
5 | var allowed = context.getVariable("quota.allow")
6 | if (used > allowed) {
7 | var exceeded = used - allowed
8 | context.setVariable("quota.used", allowed)
9 | context.setVariable("quota.exceeded", exceeded.toFixed(0))
10 | } else {
11 | var exceeded = 0
12 | context.setVariable("quota.used", used)
13 | context.setVariable("quota.exceeded", exceeded.toFixed(0))
14 | }
15 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/resources/jsc/generate-jwk.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | const alg = "RS256";
16 | const use = "sig";
17 | var certificate1 = context.getVariable("private.certificate1");
18 | var certificate2 = context.getVariable("private.certificate2");
19 | var certificatelist = {};
20 |
21 | certificatelist.keys = [];
22 |
23 | if (!certificate1) {
24 | throw Error("No certificate found");
25 | }
26 |
27 | var key1 = KEYUTIL.getKey(certificate1);
28 | var jwk1 = KEYUTIL.getJWKFromKey(key1);
29 | var cert1_kid = context.getVariable("private.certificate1_kid") || null;
30 |
31 | if (cert1_kid !== null) {
32 | jwk1.kid = cert1_kid;
33 | jwk1.alg = alg;
34 | jwk1.use = use;
35 | }
36 | certificatelist.keys.push(jwk1);
37 |
38 | if (certificate2) {
39 | var key2 = KEYUTIL.getKey(certificate2);
40 | var jwk2 = KEYUTIL.getJWKFromKey(key2);
41 | var cert2_kid = context.getVariable("private.certificate2_kid") || null;
42 | if (cert2_kid !== null) {
43 | jwk2.kid = cert2_kid;
44 | jwk2.alg = alg;
45 | jwk2.use = use;
46 | }
47 | certificatelist.keys.push(jwk2);
48 | }
49 |
50 | context.setVariable("jwkmessage", JSON.stringify(certificatelist));
51 |
52 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/resources/jsc/jwt-initialization.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | /*
16 | *
17 | * A dummy navigator object - jsrasign expects to be running in a browser and expects
18 | * these to be in the global namespace
19 | *
20 | */
21 |
22 | var navigator = navigator || {appName : ''};
23 | var window = window || {};
24 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/resources/jsc/send-jwk-response.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | //send response
16 | context.setVariable("response.header.Content-Type","application/json");
17 | context.setVariable("response.header.Cache-Control","no-store");
18 | context.setVariable("response.content", context.getVariable("jwkmessage"));
19 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/resources/jsc/set-jwt-variables.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | var apiCredential = JSON.parse(context.getVariable('apiCredential'));
16 | var apiKey = context.getVariable('apikey');
17 | //{"Credentials":{"Credential":[{"Attributes":{},"ConsumerKey":"xxx","ConsumerSecret":"xx","ExpiresAt":"-1","IssuedAt":"1530046158362","ApiProducts":{"ApiProduct":{"Name":"details product","Status":"approved"}},"Scopes":{},"Status":"approved"}]}}
18 | var credentials = apiCredential.Credentials.Credential;
19 |
20 | var apiProductsList = [];
21 | try {
22 | credentials.forEach(function(credential) {
23 | if (credential.ConsumerKey == apiKey) {
24 | credential.ApiProducts.ApiProduct.forEach(function(apiProduct){
25 | apiProductsList.push(apiProduct.Name);
26 | });
27 | }
28 | });
29 | } catch (err) {
30 | print(err);
31 | }
32 |
33 | var scope = context.getVariable("oauthv2accesstoken.AccessTokenRequest.scope");
34 | if (scope) {
35 | var scopearr = scope.split(" ");
36 | context.setVariable("scope", scopearr.join());
37 | }
38 |
39 | context.setVariable("apiProductList", apiProductsList.join());
40 | context.setVariable("nbf", new Date().toUTCString());
41 | context.setVariable("iss", context.getVariable("proxyProto") + "://" + context.getVariable("proxyHost") + context.getVariable("proxy.basepath") + context.getVariable("proxy.pathsuffix"));
42 | context.setVariable("jti", 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
43 | var r = Math.random() * 16 | 0,
44 | v = c == 'x' ? r : (r & 0x3 | 0x8);
45 | return v.toString(16);
46 | }));
47 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/resources/jsc/set-quota-variables.js:
--------------------------------------------------------------------------------
1 | context.setVariable('quota.identifier', request.body.asJSON.identifier);
2 | context.setVariable("quota.allow", request.body.asJSON.allow);
3 | context.setVariable("quota.interval", request.body.asJSON.interval);
4 | context.setVariable("quota.unit", request.body.asJSON.timeUnit);
5 | context.setVariable("quota.weight", request.body.asJSON.weight);
6 |
--------------------------------------------------------------------------------
/proxies/auth-proxy-legacy/apiproxy/resources/jsc/set-response.js:
--------------------------------------------------------------------------------
1 | // Copyright 2018 Google LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | //prepare response object
16 |
17 | var jws = {
18 | token: context.getVariable('jwtmessage')
19 | };
20 | //if refresh token exists, add it to response
21 | if (context.getVariable('grant_type') === "password") {
22 | jws.refresh_token = context.getVariable("oauthv2accesstoken.AccessTokenRequest.refresh_token");
23 | jws.refresh_token_expires_in = context.getVariable("oauthv2accesstoken.AccessTokenRequest.refresh_token_expires_in");
24 | jws.refresh_token_issued_at = context.getVariable("oauthv2accesstoken.AccessTokenRequest.refresh_token_issued_at") ;
25 | jws.refresh_token_status = context.getVariable("oauthv2accesstoken.AccessTokenRequest.refresh_token_status");
26 | }
27 | //send response
28 | context.setVariable("response.header.Content-Type","application/json");
29 | context.setVariable("response.header.Cache-Control","no-store");
30 | context.setVariable("response.content", JSON.stringify(jws));
31 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/README.md:
--------------------------------------------------------------------------------
1 | # edgemicro-internal
2 |
3 | An Apigee Edge proxy to support analytics and quota.
4 |
5 | ## Development
6 |
7 | IMPORTANT: If you change the proxy source, you must run `bin/build_proxy_sources.sh` and rebuild the
8 | `apigee-istio` CLI to include it.
9 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/EdgeMicro.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1437159946043
5 | adminui@apigee.com
6 |
7 | EdgeMicro
8 | 1437163826987
9 | adminui@apigee.com
10 |
11 | Authenticate
12 | Callout
13 | Credential
14 | DistributedQuota
15 | JSSetupVariables
16 | NoOrgOrEnv
17 | Return200
18 | Return401
19 | Return404
20 | SetQuotaResponse
21 |
22 |
23 | default
24 |
25 |
26 | jsc://JSSetupVariables.js
27 | java://edge-micro-javacallout-1.0.0.jar
28 |
29 |
30 |
31 | false
32 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/policies/Authenticate.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Authenticate
4 |
5 | com.apigee.edgemicro.javacallout.Authenticate
6 | java://edge-micro-javacallout-1.0.0.jar
7 |
8 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/policies/Callout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Callout
4 |
5 | edgemicro_
6 | DN=http://23.23.5.244:9001
7 | http://23.23.5.244:8080
8 |
9 | com.apigee.edgemicro.javacallout.Callout
10 | java://edge-micro-javacallout-1.0.0.jar
11 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/policies/DistributedQuota.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | DistributedQuota
4 |
5 |
6 |
7 |
8 |
9 | true
10 | false
11 |
12 | 5
13 | 100
14 |
15 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/policies/JSSetupVariables.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | JSSetupVariables
4 |
5 | jsc://JSSetupVariables.js
6 |
7 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/policies/NoOrgOrEnv.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | NoOrgOrEnv
4 |
5 |
6 |
7 |
8 |
9 | 404
10 | No organization or environment specified
11 |
12 |
13 | true
14 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/policies/Return401.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Return401
4 |
5 |
6 |
7 |
8 |
9 | 401
10 |
11 |
12 | true
13 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/policies/Return404.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Return404
4 |
5 |
6 |
7 |
8 |
9 | 404
10 | Not Found
11 |
12 |
13 | true
14 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/policies/ReturnVersion.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | ReturnVersion
4 |
5 |
6 | response.status.code
7 | 200
8 |
9 |
10 | response.content
11 | 1.1.0
12 |
13 |
14 | response.header.Content-Type
15 | text/plain
16 |
17 | true
18 |
19 |
20 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/policies/SetQuotaResponse.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | SetQuotaResponse
4 |
5 |
6 |
7 |
8 | {
9 | "allowed": @ratelimit.DistributedQuota.allowed.count#,
10 | "used": @ratelimit.DistributedQuota.used.count#,
11 | "exceeded": @ratelimit.DistributedQuota.exceed.count#,
12 | "available": @ratelimit.DistributedQuota.available.count#,
13 | "expiryTime": @ratelimit.DistributedQuota.expiry.time#,
14 | "timestamp": @system.timestamp#
15 | }
16 |
17 | true
18 |
19 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/proxies/default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Authenticate
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ReturnVersion
17 |
18 |
19 | (proxy.pathsuffix MatchesPath "**/v2/version") and (request.verb = "GET")
20 |
21 |
22 |
23 |
24 |
25 | NoOrgOrEnv
26 |
27 |
28 | (apigee.edgemicro.organization = null or apigee.edgemicro.environment = null)
29 |
30 |
31 |
32 |
33 | JSSetupVariables
34 |
35 |
36 | DistributedQuota
37 |
38 |
39 |
40 |
41 | SetQuotaResponse
42 |
43 |
44 | (proxy.pathsuffix MatchesPath "/quotas**") and (request.verb = "POST")
45 |
46 |
47 |
48 |
49 |
50 |
51 | Callout
52 |
53 |
54 | (proxy.pathsuffix MatchesPath "/credential/**")
55 |
56 |
57 |
58 |
59 | Callout
60 |
61 |
62 | (proxy.pathsuffix MatchesPath "/bootstrap/**" and apigee.edgemicro.authenicate = "true" and (request.verb = "GET"))
63 |
64 |
65 |
66 |
67 | Callout
68 |
69 |
70 | (proxy.pathsuffix MatchesPath "/region/**" and apigee.edgemicro.authenicate = "true" and (request.verb = "GET"))
71 |
72 |
73 |
74 |
75 | Callout
76 |
77 |
78 | ( proxy.pathsuffix MatchesPath "/axpublisher/**" and (request.verb = "POST")
79 |
80 |
81 |
82 |
83 | Return401
84 |
85 |
86 | apigee.edgemicro.authenicate = "false"
87 |
88 |
89 |
90 |
91 | Return404
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | /edgemicro
102 | default
103 |
104 |
105 |
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/resources/java/edge-micro-javacallout-1.0.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apigee/istio-mixer-adapter/dcd54df6a221d07eebff6e82195b5caf3316b42b/proxies/internal-proxy/apiproxy/resources/java/edge-micro-javacallout-1.0.0.jar
--------------------------------------------------------------------------------
/proxies/internal-proxy/apiproxy/resources/jsc/JSSetupVariables.js:
--------------------------------------------------------------------------------
1 |
2 | var orgName = context.getVariable('apigee.edgemicro.organization');
3 |
4 | context.setVariable('quota.identifier', orgName + '.' + request.body.asJSON.identifier);
5 | context.setVariable("quota.allow",request.body.asJSON.allow);
6 | context.setVariable("quota.interval",request.body.asJSON.interval);
7 | context.setVariable("quota.unit",request.body.asJSON.timeUnit);
8 | context.setVariable("quota.weight",request.body.asJSON.weight);
9 |
10 |
--------------------------------------------------------------------------------
/samples/apigee/adapter.yaml:
--------------------------------------------------------------------------------
1 | # Example deployment for Apigee Adapter.
2 | # This will work without modiciation for SaaS.
3 | # For Hybrid, you must uncomment and properly configure the secret volumes.
4 | apiVersion: apps/v1
5 | kind: Deployment
6 | metadata:
7 | name: apigee-adapter
8 | namespace: istio-system
9 | spec:
10 | replicas: 1
11 | selector:
12 | matchLabels:
13 | app: apigee-adapter
14 | template:
15 | metadata:
16 | labels:
17 | app: apigee-adapter
18 | version: v1
19 | spec:
20 | containers:
21 | - name: apigee-adapter
22 | image: "gcr.io/apigee-api-management-istio/apigee-adapter:1.4.1"
23 | imagePullPolicy: IfNotPresent #Always
24 | env:
25 | - name: GODEBUG # value must be 0, as apigee does not support http 2
26 | value: http2client=0
27 | ports:
28 | - containerPort: 5000
29 | readinessProbe:
30 | exec:
31 | command: ["/grpc_health_probe", "-addr=:5000"]
32 | initialDelaySeconds: 5
33 | livenessProbe:
34 | exec:
35 | command: ["/grpc_health_probe", "-addr=:5000"]
36 | initialDelaySeconds: 10
37 | args:
38 | - --address=:5000
39 | - --log_output_level=default:warn,adapters:info
40 | resources:
41 | limits:
42 | cpu: 100m
43 | memory: 100Mi
44 | requests:
45 | cpu: 10m
46 | memory: 100Mi
47 | # volumeMounts:
48 | # - mountPath: /opt/apigee/customer
49 | # name: cwc-volume
50 | # readOnly: true
51 | # - mountPath: /opt/apigee/tls
52 | # name: tls-volume
53 | # readOnly: true
54 | # volumes:
55 | # - name: cwc-volume
56 | # secret:
57 | # defaultMode: 420
58 | # secretName: REPLACE ME
59 | # - name: tls-volume
60 | # secret:
61 | # defaultMode: 420
62 | # secretName: REPLACE ME
63 | ---
64 | apiVersion: v1
65 | kind: Service
66 | metadata:
67 | name: apigee-adapter
68 | namespace: istio-system
69 | labels:
70 | app: apigee-adapter
71 | spec:
72 | ports:
73 | - port: 5000
74 | name: http
75 | selector:
76 | app: apigee-adapter
77 |
--------------------------------------------------------------------------------
/samples/apigee/authentication-policy.yaml:
--------------------------------------------------------------------------------
1 | # Creates an Authentication policy and binds it to service.
2 | # The example forces requests to helloworld or httpbin services
3 | # to have a valid JWT.
4 | # Configure issuer, jwks_uri, and services as appropriate.
5 | ---
6 | # Define an Istio Auth Policy
7 | apiVersion: "authentication.istio.io/v1alpha1"
8 | kind: Policy
9 | metadata:
10 | name: auth-spec
11 | namespace: default
12 | spec:
13 | targets:
14 | - name: helloworld
15 | - name: httpbin
16 | peers:
17 | # - mtls: {} # uncomment if you're using mTLS between services in your mesh
18 | origins:
19 | - jwt:
20 | issuer: REPLACE ME
21 | jwks_uri: REPLACE ME
22 | principalBinding: USE_ORIGIN
23 |
--------------------------------------------------------------------------------
/samples/apigee/handler.yaml:
--------------------------------------------------------------------------------
1 | # Example Istio handler configuration for Apigee adapter for Mixer.
2 | # use `apigee-istio provision` to generate your own.
3 | apiVersion: config.istio.io/v1alpha2
4 | kind: handler
5 | metadata:
6 | name: apigee-handler
7 | namespace: istio-system
8 | spec:
9 | adapter: apigee
10 | connection:
11 | address: apigee-adapter:5000
12 | params:
13 | apigee_base: https://istioservices.apigee.net/edgemicro
14 | customer_base: REPLACE_ME
15 | hybrid_config:
16 | org_name: REPLACE_ME
17 | env_name: REPLACE_ME
18 | key: REPLACE_ME
19 | secret: REPLACE_ME
20 |
--------------------------------------------------------------------------------
/samples/apigee/rule.yaml:
--------------------------------------------------------------------------------
1 | # Defines rules to apply the Apigee mixer adapter to requests.
2 | # In the rule below, we apply Apigee authorization and analytics
3 | # as defined in the apigee-handler (handler.yaml) to all requests
4 | # to the default namespace.
5 | ---
6 | apiVersion: config.istio.io/v1alpha2
7 | kind: rule
8 | metadata:
9 | name: apigee-rule
10 | namespace: istio-system
11 | spec:
12 | match: context.reporter.kind == "inbound" && destination.namespace == "default"
13 | actions:
14 | - handler: apigee-handler
15 | instances:
16 | - apigee-authorization
17 | - apigee-analytics
18 |
--------------------------------------------------------------------------------