├── .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 |
7 | {proxyProto} 8 |
9 |
10 | {proxyHost} 11 |
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 |
7 | {proxyProto} 8 |
9 |
10 | {proxyHost} 11 |
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 |
7 | {proxyProto} 8 |
9 |
10 | {proxyHost} 11 |
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 |
public, max-age=604800
8 |
application/json
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 |
7 | {proxyProto} 8 |
9 |
10 | {proxyHost} 11 |
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 |
7 | {proxyProto} 8 |
9 |
10 | {proxyHost} 11 |
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 |
7 | {proxyProto} 8 |
9 |
10 | {proxyHost} 11 |
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 |
public, max-age=604800
8 |
application/json
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 | --------------------------------------------------------------------------------