From 61fea4c885899fbc3a10f8da4860d6c9c2882e0d Mon Sep 17 00:00:00 2001 From: Ignasi Barrera Date: Mon, 22 Apr 2024 13:26:43 +0200 Subject: [PATCH] Replace contents with link and archive notice Signed-off-by: Ignasi Barrera --- CONTRIBUTING.md | 29 - DEVELOPMENT.md | 122 -- Dockerfile | 39 - LICENSE | 201 -- Makefile | 232 --- README.md | 44 +- cmd/main.go | 78 - config/Makefile | 35 - config/buf.gen.yaml | 30 - config/buf.lock | 8 - config/buf.yaml | 27 - config/gen/go/v1/config.pb.go | 943 --------- config/gen/go/v1/config.pb.validate.go | 1227 ------------ config/gen/go/v1/mock/config.pb.go | 177 -- config/gen/go/v1/mock/config.pb.validate.go | 137 -- config/gen/go/v1/oidc/config.pb.go | 1059 ---------- config/gen/go/v1/oidc/config.pb.validate.go | 1117 ----------- config/v1/config.proto | 189 -- config/v1/mock/config.proto | 25 - config/v1/oidc/config.proto | 258 --- e2e/Makefile | 35 - e2e/README.md | 4 - e2e/docker.go | 134 -- e2e/istio/Makefile | 30 - e2e/istio/README.md | 52 - e2e/istio/cluster/istiod-config.yaml | 41 - e2e/istio/cluster/istiogw-config.yaml | 37 - e2e/istio/cluster/kind-config.yaml | 27 - e2e/istio/cluster/manifests/authservice.yaml | 172 -- e2e/istio/cluster/manifests/authz-policy.yaml | 30 - e2e/istio/cluster/manifests/http-echo.yaml | 70 - .../cluster/manifests/ingress-gateway.yaml | 57 - e2e/istio/cluster/manifests/keycloak.yaml | 157 -- e2e/istio/cluster/manifests/redis.yaml | 73 - e2e/istio/cluster/manifests/telemetry.yaml | 23 - e2e/istio/istio_test.go | 75 - e2e/istio/suite_test.go | 106 - e2e/k8s_suite.go | 206 -- e2e/keycloak/Makefile | 25 - e2e/keycloak/README.md | 35 - e2e/keycloak/authz-config.json | 38 - e2e/keycloak/docker-compose.yaml | 128 -- e2e/keycloak/envoy-config.yaml | 104 - e2e/keycloak/idp-proxy-config.yaml | 69 - e2e/keycloak/keycloak_test.go | 239 --- e2e/keycloak/setup-keycloak.sh | 58 - e2e/legacy/Makefile | 38 - e2e/legacy/README.md | 38 - e2e/legacy/authz-config.json | 42 - e2e/legacy/docker-compose.yaml | 108 -- e2e/legacy/envoy-config.yaml | 104 - e2e/legacy/legacy_test.go | 207 -- e2e/legacy/setup-keycloak.sh | 58 - e2e/mock/Makefile | 15 - e2e/mock/README.md | 7 - e2e/mock/authz-config.json | 55 - e2e/mock/docker-compose.yaml | 39 - e2e/mock/envoy-config.yaml | 75 - e2e/mock/mock_test.go | 84 - e2e/redis/Makefile | 15 - e2e/redis/README.md | 5 - e2e/redis/docker-compose.yaml | 22 - e2e/redis/store_test.go | 146 -- e2e/suite-certs.mk | 42 - e2e/suite-docker.mk | 58 - e2e/suite-k8s.mk | 81 - e2e/testclient.go | 392 ---- env.mk | 61 - go.mod | 94 - go.sum | 265 --- internal/authz/handler.go | 27 - internal/authz/mock.go | 57 - internal/authz/mock_test.go | 50 - internal/authz/oidc.go | 841 -------- internal/authz/oidc_test.go | 1712 ----------------- internal/boolstr.go | 34 - internal/boolstr_test.go | 43 - internal/config.go | 279 --- internal/config_test.go | 269 --- internal/file.go | 164 -- internal/file_test.go | 340 ---- internal/fips_disabled.go | 22 - internal/fips_enabled.go | 24 - internal/http/headers.go | 37 - internal/http/http.go | 85 - internal/http/http_test.go | 134 -- internal/k8s/secret_controller.go | 231 --- .../k8s/secret_controller_lifecycle_test.go | 191 -- .../k8s/secret_controller_reconcile_test.go | 134 -- .../oidc-with-cross-ns-secret-ref-in.json | 43 - .../oidc-with-cross-ns-secret-ref-out.json | 43 - .../oidc-with-multiple-secret-refs-in.json | 121 -- .../oidc-with-multiple-secret-refs-out.json | 116 -- .../oidc-with-secret-ref-deleting-in.json | 43 - .../oidc-with-secret-ref-deleting-out.json | 43 - .../oidc-with-secret-ref-not-found-in.json | 43 - .../oidc-with-secret-ref-not-found-out.json | 43 - .../oidc-with-secret-ref-without-data-in.json | 43 - ...oidc-with-secret-ref-without-data-out.json | 43 - .../k8s/testdata/oidc-with-secret-ref.json | 43 - .../testdata/oidc-without-secret-ref-in.json | 40 - .../testdata/oidc-without-secret-ref-out.json | 40 - internal/logging.go | 188 -- internal/logging_test.go | 81 - internal/logr.go | 86 - internal/logr_test.go | 132 -- internal/oidc/discovery.go | 64 - internal/oidc/discovery_test.go | 118 -- internal/oidc/jwks.go | 168 -- internal/oidc/jwks_test.go | 309 --- internal/oidc/memory.go | 191 -- internal/oidc/memory_test.go | 129 -- internal/oidc/redis.go | 304 --- internal/oidc/redis_test.go | 221 --- internal/oidc/session.go | 207 -- internal/oidc/session_test.go | 144 -- internal/oidc/state.go | 22 - internal/oidc/time.go | 31 - internal/oidc/time_test.go | 37 - internal/oidc/token.go | 37 - internal/oidc/token_test.go | 52 - internal/server/authz.go | 231 --- internal/server/authz_test.go | 367 ---- internal/server/health.go | 120 -- internal/server/health_test.go | 104 - internal/server/logging.go | 66 - internal/server/requestid.go | 45 - internal/server/requestid_test.go | 47 - internal/server/server.go | 106 - internal/server/server_test.go | 101 - internal/testdata/duplicate-oidc.json | 40 - .../testdata/invalid-callback-logout.json | 29 - internal/testdata/invalid-callback.json | 26 - internal/testdata/invalid-config.json | 3 - internal/testdata/invalid-health-port.json | 4 - internal/testdata/invalid-logout.json | 29 - .../invalid-oidc-client-secret-ref.json | 29 - .../testdata/invalid-oidc-client-secret.json | 27 - internal/testdata/invalid-oidc-override.json | 27 - internal/testdata/invalid-oidc-uris.json | 27 - internal/testdata/invalid-redis.json | 30 - internal/testdata/invalid-values.json | 17 - internal/testdata/mock.json | 17 - internal/testdata/multiple-oidc.json | 42 - internal/testdata/oidc-dynamic.json | 30 - internal/testdata/oidc-override.json | 43 - internal/testdata/oidc.json | 39 - .../valid-logout-override-default.json | 68 - internal/tls.go | 213 -- internal/tls_test.go | 353 ---- run-in-docker.sh | 40 - 151 files changed, 2 insertions(+), 20270 deletions(-) delete mode 100644 CONTRIBUTING.md delete mode 100644 DEVELOPMENT.md delete mode 100644 Dockerfile delete mode 100644 LICENSE delete mode 100644 Makefile delete mode 100644 cmd/main.go delete mode 100644 config/Makefile delete mode 100644 config/buf.gen.yaml delete mode 100644 config/buf.lock delete mode 100644 config/buf.yaml delete mode 100644 config/gen/go/v1/config.pb.go delete mode 100644 config/gen/go/v1/config.pb.validate.go delete mode 100644 config/gen/go/v1/mock/config.pb.go delete mode 100644 config/gen/go/v1/mock/config.pb.validate.go delete mode 100644 config/gen/go/v1/oidc/config.pb.go delete mode 100644 config/gen/go/v1/oidc/config.pb.validate.go delete mode 100644 config/v1/config.proto delete mode 100644 config/v1/mock/config.proto delete mode 100644 config/v1/oidc/config.proto delete mode 100644 e2e/Makefile delete mode 100644 e2e/README.md delete mode 100644 e2e/docker.go delete mode 100644 e2e/istio/Makefile delete mode 100644 e2e/istio/README.md delete mode 100644 e2e/istio/cluster/istiod-config.yaml delete mode 100644 e2e/istio/cluster/istiogw-config.yaml delete mode 100644 e2e/istio/cluster/kind-config.yaml delete mode 100644 e2e/istio/cluster/manifests/authservice.yaml delete mode 100644 e2e/istio/cluster/manifests/authz-policy.yaml delete mode 100644 e2e/istio/cluster/manifests/http-echo.yaml delete mode 100644 e2e/istio/cluster/manifests/ingress-gateway.yaml delete mode 100644 e2e/istio/cluster/manifests/keycloak.yaml delete mode 100644 e2e/istio/cluster/manifests/redis.yaml delete mode 100644 e2e/istio/cluster/manifests/telemetry.yaml delete mode 100644 e2e/istio/istio_test.go delete mode 100644 e2e/istio/suite_test.go delete mode 100644 e2e/k8s_suite.go delete mode 100644 e2e/keycloak/Makefile delete mode 100644 e2e/keycloak/README.md delete mode 100644 e2e/keycloak/authz-config.json delete mode 100644 e2e/keycloak/docker-compose.yaml delete mode 100644 e2e/keycloak/envoy-config.yaml delete mode 100644 e2e/keycloak/idp-proxy-config.yaml delete mode 100644 e2e/keycloak/keycloak_test.go delete mode 100755 e2e/keycloak/setup-keycloak.sh delete mode 100644 e2e/legacy/Makefile delete mode 100644 e2e/legacy/README.md delete mode 100644 e2e/legacy/authz-config.json delete mode 100644 e2e/legacy/docker-compose.yaml delete mode 100644 e2e/legacy/envoy-config.yaml delete mode 100644 e2e/legacy/legacy_test.go delete mode 100755 e2e/legacy/setup-keycloak.sh delete mode 100644 e2e/mock/Makefile delete mode 100644 e2e/mock/README.md delete mode 100644 e2e/mock/authz-config.json delete mode 100644 e2e/mock/docker-compose.yaml delete mode 100644 e2e/mock/envoy-config.yaml delete mode 100644 e2e/mock/mock_test.go delete mode 100644 e2e/redis/Makefile delete mode 100644 e2e/redis/README.md delete mode 100644 e2e/redis/docker-compose.yaml delete mode 100644 e2e/redis/store_test.go delete mode 100644 e2e/suite-certs.mk delete mode 100644 e2e/suite-docker.mk delete mode 100644 e2e/suite-k8s.mk delete mode 100644 e2e/testclient.go delete mode 100644 env.mk delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 internal/authz/handler.go delete mode 100644 internal/authz/mock.go delete mode 100644 internal/authz/mock_test.go delete mode 100644 internal/authz/oidc.go delete mode 100644 internal/authz/oidc_test.go delete mode 100644 internal/boolstr.go delete mode 100644 internal/boolstr_test.go delete mode 100644 internal/config.go delete mode 100644 internal/config_test.go delete mode 100644 internal/file.go delete mode 100644 internal/file_test.go delete mode 100644 internal/fips_disabled.go delete mode 100644 internal/fips_enabled.go delete mode 100644 internal/http/headers.go delete mode 100644 internal/http/http.go delete mode 100644 internal/http/http_test.go delete mode 100644 internal/k8s/secret_controller.go delete mode 100644 internal/k8s/secret_controller_lifecycle_test.go delete mode 100644 internal/k8s/secret_controller_reconcile_test.go delete mode 100644 internal/k8s/testdata/oidc-with-cross-ns-secret-ref-in.json delete mode 100644 internal/k8s/testdata/oidc-with-cross-ns-secret-ref-out.json delete mode 100644 internal/k8s/testdata/oidc-with-multiple-secret-refs-in.json delete mode 100644 internal/k8s/testdata/oidc-with-multiple-secret-refs-out.json delete mode 100644 internal/k8s/testdata/oidc-with-secret-ref-deleting-in.json delete mode 100644 internal/k8s/testdata/oidc-with-secret-ref-deleting-out.json delete mode 100644 internal/k8s/testdata/oidc-with-secret-ref-not-found-in.json delete mode 100644 internal/k8s/testdata/oidc-with-secret-ref-not-found-out.json delete mode 100644 internal/k8s/testdata/oidc-with-secret-ref-without-data-in.json delete mode 100644 internal/k8s/testdata/oidc-with-secret-ref-without-data-out.json delete mode 100644 internal/k8s/testdata/oidc-with-secret-ref.json delete mode 100644 internal/k8s/testdata/oidc-without-secret-ref-in.json delete mode 100644 internal/k8s/testdata/oidc-without-secret-ref-out.json delete mode 100644 internal/logging.go delete mode 100644 internal/logging_test.go delete mode 100644 internal/logr.go delete mode 100644 internal/logr_test.go delete mode 100644 internal/oidc/discovery.go delete mode 100644 internal/oidc/discovery_test.go delete mode 100644 internal/oidc/jwks.go delete mode 100644 internal/oidc/jwks_test.go delete mode 100644 internal/oidc/memory.go delete mode 100644 internal/oidc/memory_test.go delete mode 100644 internal/oidc/redis.go delete mode 100644 internal/oidc/redis_test.go delete mode 100644 internal/oidc/session.go delete mode 100644 internal/oidc/session_test.go delete mode 100644 internal/oidc/state.go delete mode 100644 internal/oidc/time.go delete mode 100644 internal/oidc/time_test.go delete mode 100644 internal/oidc/token.go delete mode 100644 internal/oidc/token_test.go delete mode 100644 internal/server/authz.go delete mode 100644 internal/server/authz_test.go delete mode 100644 internal/server/health.go delete mode 100644 internal/server/health_test.go delete mode 100644 internal/server/logging.go delete mode 100644 internal/server/requestid.go delete mode 100644 internal/server/requestid_test.go delete mode 100644 internal/server/server.go delete mode 100644 internal/server/server_test.go delete mode 100644 internal/testdata/duplicate-oidc.json delete mode 100644 internal/testdata/invalid-callback-logout.json delete mode 100644 internal/testdata/invalid-callback.json delete mode 100644 internal/testdata/invalid-config.json delete mode 100644 internal/testdata/invalid-health-port.json delete mode 100644 internal/testdata/invalid-logout.json delete mode 100644 internal/testdata/invalid-oidc-client-secret-ref.json delete mode 100644 internal/testdata/invalid-oidc-client-secret.json delete mode 100644 internal/testdata/invalid-oidc-override.json delete mode 100644 internal/testdata/invalid-oidc-uris.json delete mode 100644 internal/testdata/invalid-redis.json delete mode 100644 internal/testdata/invalid-values.json delete mode 100644 internal/testdata/mock.json delete mode 100644 internal/testdata/multiple-oidc.json delete mode 100644 internal/testdata/oidc-dynamic.json delete mode 100644 internal/testdata/oidc-override.json delete mode 100644 internal/testdata/oidc.json delete mode 100644 internal/testdata/valid-logout-override-default.json delete mode 100644 internal/tls.go delete mode 100644 internal/tls_test.go delete mode 100755 run-in-docker.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 9e37753..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,29 +0,0 @@ -# Contributing - -We welcome contributions from the community. Please read the following guidelines carefully to -maximize the chances of your PR being merged. - -## Coding Style - -* To ensure your change passes format checks, run `make check`. To format your files, you can run `make format`. -* We follow standard Go table-driven tests and use the `testify` library to assert correctness. - To verify all tests pass, you can run `make test`. - -## Code Reviews - -* The pull request title should describe what the change does and not embed issue numbers. - The pull request should only be blank when the change is minor. Any feature should include - a description of the change and what motivated it. If the change or design changes through - review, please keep the title and description updated accordingly. -* A single approval is sufficient to merge. If a reviewer asks for - changes in a PR they should be addressed before the PR is merged, - even if another reviewer has already approved the PR. -* During the review, address the comments and commit the changes - _without_ squashing the commits. This facilitates incremental reviews - since the reviewer does not go through all the code again to find out - what has changed since the last review. When a change goes out of sync with main, - please rebase and force push, keeping the original commits where practical. -* Commits are squashed prior to merging a pull request, using the title - as commit message by default. Maintainers may request contributors to - edit the pull request tite to ensure that it remains descriptive as a - commit message. Alternatively, maintainers may change the commit message directly. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index 7e007fb..0000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,122 +0,0 @@ -# Developer guide - -All the build targets are self-explanatory and can be listed with: - -```bash -$ make help -``` - -The following software and tools are needed to build the project and run the tests: - -* [Go](https://golang.org/dl/) -* [GNU make](https://www.gnu.org/software/make/) -* [Docker](https://docs.docker.com/get-docker/) -* [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) (needed to run the Istio e2e test suite) -* [Helm](https://helm.sh/docs/intro/install/) (needed to run the Istio e2e test suite) - - -## Building the binary - -To build the binary simply run: - -```bash -$ make build # Builds a dynamically linked binary -$ make static # Builds a statically linked binary -``` - -The resulting binaries will be in the `bin/` directory. You can play with the -`TARGETS` environment variable to control the operating systems and architectures you want -to build for. - - -## Docker image - -To build the Docker image, run: - -```bash -$ make docker # Build a single-arch Docker image tagged with "-latest-$arch" -$ make docker-push # Build and push the multi-arch Docker images to the registry -``` - -This will automatically build the required binaries and create a Docker image with them. - -The `make docker` target will produce images that are suitable to be used in the `e2e` tests. -The `make docker-push` target will produce multi-arch images and push them to the registry. -You can use the `DOCKER_TARGETS` environment variable to control the operating systems and architectures -you want to build the Docker images for. - - -## Generating the API code - -The configuration options are defined in the [config](config/) directory using [Protocol Buffers](https://protobuf.dev/). -To generate the configuration API code after doing changes to the `.proto` files, run: - -```bash -$ make generate -``` - -There is no need to run `generate` after checking out the code; it's only needed when changes are made to -the `.proto` files. - - -## Testing - -The main testing targets are: - -```bash -$ make test # Run the unit tests -$ make lint # Run the linters -$ make e2e # Run the end-to-end tests -``` - -### e2e tests - -The end-to-end tests are found in the [e2e](e2e/) directory. Each subdirectory contains a test suite -that can be run independently. The `make e2e` target will run all the test suites by default. To run -individual suites, simply run `make e2e/`. For example: - -```bash -$ make e2e # Run all the e2e suites -$ make e2e/keycloak # Run the 'keycloak' e2e suite - -# Examples with custom test options -$ E2E_TEST_OPTS="-v -count=1" make e2e # Run all the e2e suites with verbose output and no caching -$ E2E_PRESERVE_LOGS=true make e2e # Preserve the container logs even if tests succeed -``` - -> [!Note] -> The end-to-end tests use the `authservice` Docker image, and it **must be up-to-date**. -> Make sure you run `make clean docker` before running the tests - -The end-to-end tests use Docker Compose or [KinD](https://kind.sigs.k8s.io/) to set up the required -infrastructure before running the tests. Once the tests are done, the infrastructure is automatically -torn down if tests pass, or left running if tests fail, to facilitate troubleshooting. Container logs -are also captured upon test failure, to aid in debugging. - -#### Running tests from your IDE - -Sometimes it is useful to run the tests from your IDE. To do so, you can start the test infrastructure by -running the `e2e-pre` target from the e2e test suite folder. For example: - -``` -$ make -C e2e/keycloak e2e-pre -``` - -Once the infra is up, you can run the tests from your IDE or with a normal `go test` command. After the tests -are done, you can tear down the infrastructure by running the `e2e-post` target. - - -#### Backward-compatibility tests - -The [e2e/legacy](e2e/legacy/) suite directory contains a set of tests that are designed to verify the -backward compatibility of the Auth Service with the older C++ based version. This suite can be run with -the current image and the old image as follows: - -```bash -$ E2E_SUITE_MODE=current make e2e/legacy # Run the suite with the current image -$ E2E_SUITE_MODE=legacy make e2e/legacy # Run the suite with the old authservice image - -# Run the suite with a custom image -$ export E2E_LEGACY_IMAGE=ghcr.io/istio-ecosystem/authservice/authservice:0.5.3 -$ E2E_SUITE_MODE=legacy make e2e/legacy -``` diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0a399c3..0000000 --- a/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Builder image used to create a non-root user and to pick the SSL CA certs from -FROM alpine:3.18.0 as builder -RUN apk --update add ca-certificates -RUN adduser --disabled-password --gecos "" --uid 65532 nonroot - - -FROM scratch - -ARG TARGETARCH -ARG TARGETOS -ARG REPO -ARG FLAVOR - -# Copy the user info so we can run the container as a non-root user -COPY --from=builder /etc/passwd /etc/passwd -COPY --from=builder /etc/group /etc/group -# Copy the base SSL CA certs so we can make HTTPS requests -COPY --from=builder /etc/ssl/cert.pem /etc/ssl/cert.pem - -# Run as non-root. We can't use nonroot:nonroot here since in K8s: -# https://github.com/kubernetes/kubernetes/blob/98eff192802a87c613091223f774a6c789543e74/pkg/kubelet/kuberuntime/security_context_others.go#L49. -USER 65532:65532 - -ADD bin/authservice-${FLAVOR}-${TARGETOS}-${TARGETARCH} /usr/local/bin/authservice -ENTRYPOINT ["/usr/local/bin/authservice"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/Makefile b/Makefile deleted file mode 100644 index 4d8f219..0000000 --- a/Makefile +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -PKG ?= ./cmd -BUILD_OPTS ?= -TEST_OPTS ?= -TEST_PKGS ?= $(shell go list ./... | grep -v /e2e) -OUTDIR ?= bin - -include env.mk # Load common variables - - -##@ Build targets - -.PHONY: all -all: build - -.PHONY: build -build: $(TARGETS:%=$(OUTDIR)/$(NAME)-%) ## Build all the binaries - -.PHONY: static -static: $(TARGETS:%=$(OUTDIR)/$(NAME)-static-%) ## Build all the static binaries - -.PHONY: fips -fips: $(FIPS_TARGETS:%=$(OUTDIR)/$(NAME)-fips-%) ## Build all the FIPS static binaries - -$(OUTDIR)/$(NAME)-%: GOOS=$(word 1,$(subst -, ,$(subst $(NAME)-,,$(@F)))) -$(OUTDIR)/$(NAME)-%: GOARCH=$(word 2,$(subst -, ,$(subst $(NAME)-,,$(@F)))) -$(OUTDIR)/$(NAME)-%: - @echo "Build $(@F)" - @GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(BUILD_OPTS) -o $@ $(PKG) - -$(OUTDIR)/$(NAME)-static-%: GOOS=$(word 1,$(subst -, ,$(subst $(NAME)-static-,,$(@F)))) -$(OUTDIR)/$(NAME)-static-%: GOARCH=$(word 2,$(subst -, ,$(subst $(NAME)-static-,,$(@F)))) -$(OUTDIR)/$(NAME)-static-%: - @echo "Build $(@F)" - @CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(BUILD_OPTS) \ - -ldflags '-s -w -extldflags "-static"' -tags "netgo" \ - -o $@ $(PKG) - -$(OUTDIR)/$(NAME)-fips-%: GOOS=$(word 1,$(subst -, ,$(subst $(NAME)-fips-,,$(@F)))) -$(OUTDIR)/$(NAME)-fips-%: GOARCH=$(word 2,$(subst -, ,$(subst $(NAME)-fips-,,$(@F)))) -$(OUTDIR)/$(NAME)-fips-%: -ifneq ($(BUILD_FIPS_IN_DOCKER),true) - @echo "Build $(@F)" - @GOEXPERIMENT=boringcrypto CGO_ENABLED=1 GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(BUILD_OPTS) \ - -ldflags '-linkmode=external -s -w -extldflags "-static"' -tags "netgo" \ - -o $@ $(PKG) - @echo "Verifying FIPS symbols are present" - @strings $@ | grep -q _Cfunc__goboringcrypto_ || (echo "FIPS symbols not found" && exit 1) -else -# Run the FIPS build in a Linux container if the host OS is not Linux - @$(ROOT)/run-in-docker.sh $(GOOS)/$(GOARCH) make $@ -endif - -.PHONY: clean -clean: clean/e2e ## Clean the build artifacts - @rm -rf $(OUTDIR) - -.PHONY: clean/coverage -clean/coverage: ## Clean the coverage report - @rm -rf $(OUTDIR)/coverage - -.PHONY: clean/all -clean/all: clean config/clean ## Clean everything - @rm -rf $(OUTDIR) - -.PHONY: clean/e2e -clean/e2e: ## Clean the e2e test artifacts - @$(MAKE) -C $(@F) $(@D) - - -##@ Config Proto targets - -.PHONY: config/build -config/build: ## Build the API - @$(MAKE) -C $(@D) $(@F) - -.PHONY: config/clean -config/clean: ## Clean the Config Proto generated code - @$(MAKE) -C $(@D) $(@F) - -.PHONY: config/lint -config/lint: ## Lint the Config Proto generated code - @$(MAKE) -C $(@D) $(@F) - - -##@ Test targets - -.PHONY: test -test: ## Run all the tests - @KUBEBUILDER_ASSETS="$(shell go run $(ENVTEST) use -p path)" \ - go test $(TEST_OPTS) $(TEST_PKGS) - -COVERAGE_OPTS ?= -.PHONY: coverage -coverage: ## Creates coverage report for all projects - @echo "Running test coverage" - @mkdir -p $(OUTDIR)/$@ - @KUBEBUILDER_ASSETS="$(shell go run $(ENVTEST) use -p path)" \ - go test $(COVERAGE_OPTS) \ - -timeout 30s \ - -coverprofile $(OUTDIR)/$@/coverage.out \ - -covermode atomic \ - $(TEST_PKGS) - @go tool cover -html="$(OUTDIR)/$@/coverage.out" -o "$(OUTDIR)/$@/coverage.html" - -.PHONY: e2e -e2e: ## Runt he e2e tests - @$(MAKE) -C e2e e2e - -e2e/%: force-e2e - @$(MAKE) -C e2e $(@) - -.PHONY: force-e2e -force-e2e: - -##@ Docker targets - -.PHONY: docker-pre -docker-pre: - @docker buildx inspect $(DOCKER_BUILDER_NAME) || \ - docker buildx create --name $(DOCKER_BUILDER_NAME) \ - --driver docker-container --driver-opt network=host \ - --buildkitd-flags '--allow-insecure-entitlement network.host' --use - -comma := , -space := $(empty) $(empty) -PLATFORMS := $(subst -,/,$(subst $(space),$(comma),$(DOCKER_TARGETS))) -INSECURE_REGISTRY_ARG := --output=type=registry,registry.insecure=true - -.PHONY: docker -docker: docker-pre $(DOCKER_TARGETS:%=docker/static/%) ## Build the Docker images - -.PHONY: docker-fips -docker-fips: docker-pre $(DOCKER_TARGETS:%=docker/fips/%) ## Build the FIPS Docker images - -.SECONDEXPANSION: -docker/%: PLATFORM=$(subst -,/,$(notdir $(*))) -docker/%: DOCKER_ARCH=$(notdir $(subst -,/,$(PLATFORM))) -docker/%: FLAVOR=$(subst /,,$(dir $(*))) -docker/%: TAG_SUFFIX=$(if $(subst static,,${FLAVOR}),-fips) -docker/%: $(OUTDIR)/$(NAME)-$$(FLAVOR)-$$(notdir %) - @echo "Building Docker image $(DOCKER_HUB)/$(NAME):$(DOCKER_TAG)-$(DOCKER_ARCH)$(TAG_SUFFIX)" - @docker buildx build \ - $(DOCKER_BUILD_ARGS) \ - --builder $(DOCKER_BUILDER_NAME) \ - --load \ - -f Dockerfile \ - --platform $(PLATFORM) \ - --build-arg REPO=https://$(GO_MODULE) \ - --build-arg FLAVOR=$(FLAVOR) \ - $(subst org.,--label org.,$(DOCKER_METADATA)) \ - -t $(DOCKER_HUB)/$(NAME):latest-$(DOCKER_ARCH)$(TAG_SUFFIX) \ - -t $(DOCKER_HUB)/$(NAME):$(DOCKER_TAG)-$(DOCKER_ARCH)$(TAG_SUFFIX) \ - . - -.PHONY: docker-push -docker-push: docker-pre $(DOCKER_TARGETS:%=$(OUTDIR)/$(NAME)-static-%) docker-push/static ## Build and push the multi-arch Docker images - -.PHONY: docker-push-fips -docker-push-fips: docker-pre $(DOCKER_TARGETS:%=$(OUTDIR)/$(NAME)-fips-%) docker-push/fips ## Build and push the multi-arch FIPS Docker images - -docker-push/%: TAG_SUFFIX=$(if $(subst static,,$(*)),-fips) -docker-push/%: - @echo "Pushing Docker image $(DOCKER_HUB)/$(NAME):$(DOCKER_TAG)$(TAG_SUFFIX)" - @docker buildx build \ - $(DOCKER_BUILD_ARGS) \ - --builder $(DOCKER_BUILDER_NAME) \ - $(if $(USE_INSECURE_REGISTRY),$(INSECURE_REGISTRY_ARG),--push) \ - -f Dockerfile \ - --platform $(PLATFORMS) \ - --build-arg REPO=https://$(GO_MODULE) \ - --build-arg FLAVOR=$(@F) \ - $(subst org.,--label org.,$(DOCKER_METADATA)) \ - $(subst org.,--annotation index:org.,$(DOCKER_METADATA)) \ - -t $(DOCKER_HUB)/$(NAME):$(DOCKER_TAG)$(TAG_SUFFIX) \ - . - -##@ Other targets - -.PHONY: generate -generate: config/build ## Run code generation targets - -LINT_OPTS ?= --timeout 5m -GOLANGCI_LINT_CONFIG ?= .golangci.yml -.PHONY: lint -lint: $(GOLANGCI_LINT_CONFIG) config/lint ## Lint checks for all Go code - @echo "Linting Go code" - @go run $(GOLANGCI_LINT) run $(LINT_OPTS) --build-tags "$(TEST_TAGS)" --config $(GOLANGCI_LINT_CONFIG) - -.PHONY: format -format: go.mod ## Format all Go code - @echo "Formatting code" - @go run $(LICENSER) apply -r "Tetrate" - @go run $(GOSIMPORTS) -local $(GO_MODULE) -w . - @gofmt -w . - -.PHONY: check -check: ## CI blocks merge until this passes. If this fails, run "make check" locally and commit the difference. - @echo "Running CI checks" - @$(MAKE) clean/all generate - @$(MAKE) format - @if [ ! -z "`git status -s`" ]; then \ - echo "The following differences will fail CI until committed:"; \ - git diff; \ - exit 1; \ - fi - -.PHONY: dist -dist: ## Package the release binaries - @mkdir -p $(OUTDIR)/dist - @cd $(OUTDIR) && for f in $(NAME)-*; do \ - tar cvzf dist/$$f.tar.gz $$f; \ - done - -.PHONY: help -help: ## Display this help - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} \ - /^[.a-zA-Z0-9\/_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } \ - /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST) diff --git a/README.md b/README.md index 6d92671..660aba2 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,2 @@ -# authservice-go - -[![CI](https://github.com/tetrateio/authservice-go/actions/workflows/ci.yaml/badge.svg)](https://github.com/tetrateio/authservice-go/actions/workflows/ci.yaml) -[![codecov](https://codecov.io/gh/tetrateio/authservice-go/graph/badge.svg?token=JTLsQloZo9)](https://codecov.io/gh/tetrateio/authservice-go) - -An implementation of [Envoy](https://envoyproxy.io) [External Authorization](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter), -focused on delivering authN/Z solutions for [Istio](https://istio.io) and [Kubernetes](https://kubernetes.io). - -This project is a port of the [istio-ecosystem/authservice](https://github.com/istio-ecosystem/authservice) -project from C++ to Go. - -## Introduction - -`authservice-go` helps delegate the [OIDC Authorization Code Grant Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) -to the Istio mesh. `authservice-go` is compatible with any standard OIDC Provider as well as other Istio End-user Auth features, -including [Authentication Policy](https://istio.io/docs/tasks/security/authn-policy/) and [RBAC](https://istio.io/docs/tasks/security/rbac-groups/). -Together, they allow developers to protect their APIs and web apps without any application code required. - -Some of the features it provides: -* Transparent login and logout - * Retrieves OAuth2 Access tokens, ID tokens, and refresh tokens -* Fine-grained control over which url paths are protected -* Session management - * Configuration of session lifetime and idle timeouts - * Refreshes expired tokens automatically -* Compatible with any standard OIDC Provider -* Supports multiple OIDC Providers for same application -* Trusts custom CA certs when talking to OIDC Providers -* Works either at the sidecar or gateway level - - -## How does authservice work? - -[This flowchart](https://miro.com/app/board/o9J_kvus6b4=/) explains how `authservice-go` -makes decisions at different points in the login lifecycle. - -## Contributing - -Contributions are very welcome! Please read the [Contributing guidelines](CONTRIBUTING.md) -to get started. - -Detailed development instructions can be found in the [Development guide](DEVELOPMENT.md). +This reposiroty has been merged upstream and has been archived. +Please refer to: https://github.com/istio-ecosystem/authservice diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 3ba7f62..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package main - -import ( - "fmt" - "os" - - "github.com/tetratelabs/log" - "github.com/tetratelabs/run" - "github.com/tetratelabs/run/pkg/signal" - "github.com/tetratelabs/telemetry" - - "github.com/tetrateio/authservice-go/internal" - "github.com/tetrateio/authservice-go/internal/k8s" - "github.com/tetrateio/authservice-go/internal/oidc" - "github.com/tetrateio/authservice-go/internal/server" -) - -func main() { - var ( - lifecycle = run.NewLifecycle() - configFile = &internal.LocalConfigFile{} - logging = internal.NewLogSystem(log.New(), &configFile.Config) - tlsPool = internal.NewTLSConfigPool(lifecycle.Context()) - jwks = oidc.NewJWKSProvider(&configFile.Config, tlsPool) - sessions = oidc.NewSessionStoreFactory(&configFile.Config) - envoyAuthz = server.NewExtAuthZFilter(&configFile.Config, tlsPool, jwks, sessions) - authzServer = server.New(&configFile.Config, envoyAuthz.Register) - healthz = server.NewHealthServer(&configFile.Config) - secretCtrl = k8s.NewSecretController(&configFile.Config) - ) - - configLog := run.NewPreRunner("config-log", func() error { - cfgLog := internal.Logger(internal.Config) - if cfgLog.Level() == telemetry.LevelDebug { - cfgLog.Debug("configuration loaded", "config", internal.ConfigToJSONString(&configFile.Config)) - } - return nil - }) - fipsLog := run.NewPreRunner("fips", func() error { - internal.LogFIPS() - return nil - }) - - g := run.Group{Logger: internal.Logger(internal.Default)} - - g.Register( - lifecycle, // manage the lifecycle of the run.Services - configFile, // load the configuration - logging, // Set up the logging system - secretCtrl, // watch for secret updates and update the configuration - configLog, // log the configuration - fipsLog, // log whether FIPS is enabled - jwks, // start the JWKS provider - sessions, // start the session store - authzServer, // start the server - healthz, // start the health server - &signal.Handler{}, // handle graceful termination - ) - - if err := g.Run(); err != nil { - fmt.Printf("Unexpected exit: %v\n", err) - os.Exit(-1) - } -} diff --git a/config/Makefile b/config/Makefile deleted file mode 100644 index cd7d4ca..0000000 --- a/config/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -BUF ?= github.com/bufbuild/buf/cmd/buf@v1.17.0 - -PROTO_SOURCES := $(shell find . -name '*.proto') - -.PHONY: build -build: $(PROTO_SOURCES) ## Generate the Go code from the protobuf definitions - @echo "Generating Go code from protobuf definitions" - @go run $(BUF) mod update - @go run $(BUF) build - @go run $(BUF) generate - @go mod tidy - -.PHONY: lint -lint: ## Lint the protobuf definitions - @echo "Linting protobuf definitions" - @go run $(BUF) lint - -.PHONY: clean -clean: ## Clean all generated code - @echo "Cleaning generated code" - @rm -rf gen diff --git a/config/buf.gen.yaml b/config/buf.gen.yaml deleted file mode 100644 index d7ef5a8..0000000 --- a/config/buf.gen.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -version: v1 -managed: - enabled: true - go_package_prefix: - default: github.com/tetrateio/authservice-go/config/gen/go - except: - - buf.build/envoyproxy/protoc-gen-validate -plugins: - - plugin: buf.build/protocolbuffers/go - out: gen/go - opt: - - paths=source_relative - - plugin: buf.build/bufbuild/validate-go - out: gen/go - opt: - - paths=source_relative diff --git a/config/buf.lock b/config/buf.lock deleted file mode 100644 index 8585935..0000000 --- a/config/buf.lock +++ /dev/null @@ -1,8 +0,0 @@ -# Generated by buf. DO NOT EDIT. -version: v1 -deps: - - remote: buf.build - owner: envoyproxy - repository: protoc-gen-validate - commit: 6607b10f00ed4a3d98f906807131c44a - digest: shake256:acc7b2ededb2f88d296862943a003b157bdb68ec93ed13dcd8566b2d06e47993ea6daf12013b9655658aaf6bbdb141cf65bfe400ce2870f4654b0a5b45e57c09 diff --git a/config/buf.yaml b/config/buf.yaml deleted file mode 100644 index d9ea53c..0000000 --- a/config/buf.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -version: v1 -name: buf.build/authservice/config -deps: - - buf.build/envoyproxy/protoc-gen-validate:6607b10f00ed4a3d98f906807131c44a -lint: - use: - - DEFAULT - except: - - PACKAGE_DIRECTORY_MATCH - - PACKAGE_VERSION_SUFFIX -breaking: - use: - - FILE diff --git a/config/gen/go/v1/config.pb.go b/config/gen/go/v1/config.pb.go deleted file mode 100644 index e43a029..0000000 --- a/config/gen/go/v1/config.pb.go +++ /dev/null @@ -1,943 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.33.0 -// protoc (unknown) -// source: v1/config.proto - -package configv1 - -import ( - reflect "reflect" - sync "sync" - - _ "github.com/envoyproxy/protoc-gen-validate/validate" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - - mock "github.com/tetrateio/authservice-go/config/gen/go/v1/mock" - oidc "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// Specifies how a request can be matched to a filter chain. -type Match struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The name of the http header used to match against. - // Required. - Header string `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` - // The criteria by which to match. - // Must be one of `prefix` or `equality`. - // Required. - // - // Types that are assignable to Criteria: - // - // *Match_Prefix - // *Match_Equality - Criteria isMatch_Criteria `protobuf_oneof:"criteria"` -} - -func (x *Match) Reset() { - *x = Match{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Match) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Match) ProtoMessage() {} - -func (x *Match) ProtoReflect() protoreflect.Message { - mi := &file_v1_config_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Match.ProtoReflect.Descriptor instead. -func (*Match) Descriptor() ([]byte, []int) { - return file_v1_config_proto_rawDescGZIP(), []int{0} -} - -func (x *Match) GetHeader() string { - if x != nil { - return x.Header - } - return "" -} - -func (m *Match) GetCriteria() isMatch_Criteria { - if m != nil { - return m.Criteria - } - return nil -} - -func (x *Match) GetPrefix() string { - if x, ok := x.GetCriteria().(*Match_Prefix); ok { - return x.Prefix - } - return "" -} - -func (x *Match) GetEquality() string { - if x, ok := x.GetCriteria().(*Match_Equality); ok { - return x.Equality - } - return "" -} - -type isMatch_Criteria interface { - isMatch_Criteria() -} - -type Match_Prefix struct { - // The expected prefix. If the actual value of the header starts with this prefix, - // then it will be considered a match. - Prefix string `protobuf:"bytes,2,opt,name=prefix,proto3,oneof"` -} - -type Match_Equality struct { - // The expected value. If the actual value of the header exactly equals this value, - // then it will be considered a match. - Equality string `protobuf:"bytes,3,opt,name=equality,proto3,oneof"` -} - -func (*Match_Prefix) isMatch_Criteria() {} - -func (*Match_Equality) isMatch_Criteria() {} - -// A filter configuration. -type Filter struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The type of filter. Currently, the only valid types are `oidc` - // and `mock`. Required. - // - // Types that are assignable to Type: - // - // *Filter_Oidc - // *Filter_OidcOverride - // *Filter_Mock - Type isFilter_Type `protobuf_oneof:"type"` -} - -func (x *Filter) Reset() { - *x = Filter{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_config_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Filter) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Filter) ProtoMessage() {} - -func (x *Filter) ProtoReflect() protoreflect.Message { - mi := &file_v1_config_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Filter.ProtoReflect.Descriptor instead. -func (*Filter) Descriptor() ([]byte, []int) { - return file_v1_config_proto_rawDescGZIP(), []int{1} -} - -func (m *Filter) GetType() isFilter_Type { - if m != nil { - return m.Type - } - return nil -} - -func (x *Filter) GetOidc() *oidc.OIDCConfig { - if x, ok := x.GetType().(*Filter_Oidc); ok { - return x.Oidc - } - return nil -} - -func (x *Filter) GetOidcOverride() *oidc.OIDCConfig { - if x, ok := x.GetType().(*Filter_OidcOverride); ok { - return x.OidcOverride - } - return nil -} - -func (x *Filter) GetMock() *mock.MockConfig { - if x, ok := x.GetType().(*Filter_Mock); ok { - return x.Mock - } - return nil -} - -type isFilter_Type interface { - isFilter_Type() -} - -type Filter_Oidc struct { - // An OpenID Connect filter configuration. - Oidc *oidc.OIDCConfig `protobuf:"bytes,1,opt,name=oidc,proto3,oneof"` -} - -type Filter_OidcOverride struct { - // This value will be used when `default_oidc_config` exists. - // It will override values of them. If that doesn't exist, - // this configuration will be rejected. - OidcOverride *oidc.OIDCConfig `protobuf:"bytes,2,opt,name=oidc_override,json=oidcOverride,proto3,oneof"` -} - -type Filter_Mock struct { - // Mock filter configuration for testing and letting - // AuthService run even if no OIDC providers are configured. - Mock *mock.MockConfig `protobuf:"bytes,3,opt,name=mock,proto3,oneof"` -} - -func (*Filter_Oidc) isFilter_Type() {} - -func (*Filter_OidcOverride) isFilter_Type() {} - -func (*Filter_Mock) isFilter_Type() {} - -// A chain of one or more filters that will sequentially process an HTTP request. -type FilterChain struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // A user-defined identifier for the processing chain used in log messages. - // Required. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // A rule to determine whether an HTTP request should be processed by the filter chain. - // If not defined, the filter chain will match every request. - // Optional. - Match *Match `protobuf:"bytes,2,opt,name=match,proto3" json:"match,omitempty"` - // The configuration of one of more filters in the filter chain. When the filter chain - // matches an incoming request, then this list of filters will be applied to the request - // in the order that they are declared. - // All filters are evaluated until one of them returns a non-OK response. - // If all filters return OK, the envoy proxy is notified that the request may continue. - // The first filter that returns a non-OK response causes the request to be rejected with - // the filter's returned status and any remaining filters are skipped. - // At least one `Filter` is required in this array. - Filters []*Filter `protobuf:"bytes,3,rep,name=filters,proto3" json:"filters,omitempty"` -} - -func (x *FilterChain) Reset() { - *x = FilterChain{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_config_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *FilterChain) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FilterChain) ProtoMessage() {} - -func (x *FilterChain) ProtoReflect() protoreflect.Message { - mi := &file_v1_config_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FilterChain.ProtoReflect.Descriptor instead. -func (*FilterChain) Descriptor() ([]byte, []int) { - return file_v1_config_proto_rawDescGZIP(), []int{2} -} - -func (x *FilterChain) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *FilterChain) GetMatch() *Match { - if x != nil { - return x.Match - } - return nil -} - -func (x *FilterChain) GetFilters() []*Filter { - if x != nil { - return x.Filters - } - return nil -} - -// The top-level configuration object. -// For a simple example, see the [sample JSON in the bookinfo configmap template](https://github.com/istio-ecosystem/authservice/blob/master/bookinfo-example/config/authservice-configmap-template-for-authn-and-authz.yaml). -type Config struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Each incoming http request is matched against the list of filters in the chain, in order, - // until a matching filter is found. The first matching filter is then applied to the request. - // After the first match is made, other filters in the chain are ignored. - // Order of chain declaration is therefore important. - // At least one `FilterChain` is required in this array. - Chains []*FilterChain `protobuf:"bytes,1,rep,name=chains,proto3" json:"chains,omitempty"` - // The IP address for the Authservice to listen for incoming requests to process. - // Required. - ListenAddress string `protobuf:"bytes,2,opt,name=listen_address,json=listenAddress,proto3" json:"listen_address,omitempty"` - // The TCP port for the Authservice to listen for incoming requests to process. - // Required. - ListenPort int32 `protobuf:"varint,3,opt,name=listen_port,json=listenPort,proto3" json:"listen_port,omitempty"` - // The verbosity of logs generated by the Authservice. - // Must be one of `trace`, `debug`, `info', 'error' or 'critical'. - // Required. - LogLevel string `protobuf:"bytes,4,opt,name=log_level,json=logLevel,proto3" json:"log_level,omitempty"` - // The number of threads in the thread pool to use for processing. - // The main thread will be used for accepting connections, before sending them to the thread-pool - // for processing. The total number of running threads, including the main thread, will be N+1. - // Required. - Threads uint32 `protobuf:"varint,5,opt,name=threads,proto3" json:"threads,omitempty"` - // List of trigger rules to decide if the Authservice should be used to authenticate the - // request. The Authservice authentication happens if any one of the rules matched. - // If the list is not empty and none of the rules matched, the request will be allowed - // to proceed without Authservice authentication. - // The format and semantics of `trigger_rules` are the same as the `triggerRules` setting - // on the Istio Authentication Policy - // (see https://istio.io/docs/reference/config/security/istio.authentication.v1alpha1). - // CAUTION: Be sure that your configured `OIDCConfig.callback` and `OIDCConfig.logout` paths - // each satisfies at least one of the trigger rules, or else the Authservice will not be able to - // intercept requests made to those paths to perform the appropriate login/logout behavior. - // Optional. Leave this empty to always trigger authentication for all paths. - TriggerRules []*TriggerRule `protobuf:"bytes,9,rep,name=trigger_rules,json=triggerRules,proto3" json:"trigger_rules,omitempty"` - // Global configuration of OIDC. This value will be applied to all filter definition - // when it defined as `oidc_override`. - // Optional. - DefaultOidcConfig *oidc.OIDCConfig `protobuf:"bytes,10,opt,name=default_oidc_config,json=defaultOidcConfig,proto3" json:"default_oidc_config,omitempty"` - // If true will allow the the requests even no filter chain match is found. Default false. - // Optional. - AllowUnmatchedRequests bool `protobuf:"varint,11,opt,name=allow_unmatched_requests,json=allowUnmatchedRequests,proto3" json:"allow_unmatched_requests,omitempty"` - // The Authservice provides an HTTP server to check the health state. - // This configures the address for the health server to listen for. - // Optional. Defaults to the value of `listen_address`. - HealthListenAddress string `protobuf:"bytes,12,opt,name=health_listen_address,json=healthListenAddress,proto3" json:"health_listen_address,omitempty"` - // The TCP port for the health server to listen for. - // Optional. Defaults 10004. - HealthListenPort int32 `protobuf:"varint,13,opt,name=health_listen_port,json=healthListenPort,proto3" json:"health_listen_port,omitempty"` - // The path for the health server to attend. - // Optional. Defaults to "/healthz". - HealthListenPath string `protobuf:"bytes,14,opt,name=health_listen_path,json=healthListenPath,proto3" json:"health_listen_path,omitempty"` -} - -func (x *Config) Reset() { - *x = Config{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_config_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Config) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Config) ProtoMessage() {} - -func (x *Config) ProtoReflect() protoreflect.Message { - mi := &file_v1_config_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Config.ProtoReflect.Descriptor instead. -func (*Config) Descriptor() ([]byte, []int) { - return file_v1_config_proto_rawDescGZIP(), []int{3} -} - -func (x *Config) GetChains() []*FilterChain { - if x != nil { - return x.Chains - } - return nil -} - -func (x *Config) GetListenAddress() string { - if x != nil { - return x.ListenAddress - } - return "" -} - -func (x *Config) GetListenPort() int32 { - if x != nil { - return x.ListenPort - } - return 0 -} - -func (x *Config) GetLogLevel() string { - if x != nil { - return x.LogLevel - } - return "" -} - -func (x *Config) GetThreads() uint32 { - if x != nil { - return x.Threads - } - return 0 -} - -func (x *Config) GetTriggerRules() []*TriggerRule { - if x != nil { - return x.TriggerRules - } - return nil -} - -func (x *Config) GetDefaultOidcConfig() *oidc.OIDCConfig { - if x != nil { - return x.DefaultOidcConfig - } - return nil -} - -func (x *Config) GetAllowUnmatchedRequests() bool { - if x != nil { - return x.AllowUnmatchedRequests - } - return false -} - -func (x *Config) GetHealthListenAddress() string { - if x != nil { - return x.HealthListenAddress - } - return "" -} - -func (x *Config) GetHealthListenPort() int32 { - if x != nil { - return x.HealthListenPort - } - return 0 -} - -func (x *Config) GetHealthListenPath() string { - if x != nil { - return x.HealthListenPath - } - return "" -} - -// Trigger rule to match against a request. The trigger rule is satisfied if -// and only if both rules, excluded_paths and include_paths are satisfied. -type TriggerRule struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // List of paths to be excluded from the request. The rule is satisfied if - // request path does not match to any of the path in this list. - // Optional. - ExcludedPaths []*StringMatch `protobuf:"bytes,1,rep,name=excluded_paths,json=excludedPaths,proto3" json:"excluded_paths,omitempty"` - // List of paths that the request must include. If the list is not empty, the - // rule is satisfied if request path matches at least one of the path in the list. - // If the list is empty, the rule is ignored, in other words the rule is always satisfied. - // Optional. - IncludedPaths []*StringMatch `protobuf:"bytes,2,rep,name=included_paths,json=includedPaths,proto3" json:"included_paths,omitempty"` -} - -func (x *TriggerRule) Reset() { - *x = TriggerRule{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_config_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *TriggerRule) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TriggerRule) ProtoMessage() {} - -func (x *TriggerRule) ProtoReflect() protoreflect.Message { - mi := &file_v1_config_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TriggerRule.ProtoReflect.Descriptor instead. -func (*TriggerRule) Descriptor() ([]byte, []int) { - return file_v1_config_proto_rawDescGZIP(), []int{4} -} - -func (x *TriggerRule) GetExcludedPaths() []*StringMatch { - if x != nil { - return x.ExcludedPaths - } - return nil -} - -func (x *TriggerRule) GetIncludedPaths() []*StringMatch { - if x != nil { - return x.IncludedPaths - } - return nil -} - -// Describes how to match a given string. Match is case-sensitive. -type StringMatch struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Types that are assignable to MatchType: - // - // *StringMatch_Exact - // *StringMatch_Prefix - // *StringMatch_Suffix - // *StringMatch_Regex - MatchType isStringMatch_MatchType `protobuf_oneof:"match_type"` -} - -func (x *StringMatch) Reset() { - *x = StringMatch{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_config_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *StringMatch) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*StringMatch) ProtoMessage() {} - -func (x *StringMatch) ProtoReflect() protoreflect.Message { - mi := &file_v1_config_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use StringMatch.ProtoReflect.Descriptor instead. -func (*StringMatch) Descriptor() ([]byte, []int) { - return file_v1_config_proto_rawDescGZIP(), []int{5} -} - -func (m *StringMatch) GetMatchType() isStringMatch_MatchType { - if m != nil { - return m.MatchType - } - return nil -} - -func (x *StringMatch) GetExact() string { - if x, ok := x.GetMatchType().(*StringMatch_Exact); ok { - return x.Exact - } - return "" -} - -func (x *StringMatch) GetPrefix() string { - if x, ok := x.GetMatchType().(*StringMatch_Prefix); ok { - return x.Prefix - } - return "" -} - -func (x *StringMatch) GetSuffix() string { - if x, ok := x.GetMatchType().(*StringMatch_Suffix); ok { - return x.Suffix - } - return "" -} - -func (x *StringMatch) GetRegex() string { - if x, ok := x.GetMatchType().(*StringMatch_Regex); ok { - return x.Regex - } - return "" -} - -type isStringMatch_MatchType interface { - isStringMatch_MatchType() -} - -type StringMatch_Exact struct { - // exact string match. - Exact string `protobuf:"bytes,1,opt,name=exact,proto3,oneof"` -} - -type StringMatch_Prefix struct { - // prefix-based match. - Prefix string `protobuf:"bytes,2,opt,name=prefix,proto3,oneof"` -} - -type StringMatch_Suffix struct { - // suffix-based match. - Suffix string `protobuf:"bytes,3,opt,name=suffix,proto3,oneof"` -} - -type StringMatch_Regex struct { - // ECMAscript style regex-based match as defined by [EDCA-262](http://en.cppreference.com/w/cpp/regex/ecmascript). - // Example: "^/pets/(.*?)?" - Regex string `protobuf:"bytes,4,opt,name=regex,proto3,oneof"` -} - -func (*StringMatch_Exact) isStringMatch_MatchType() {} - -func (*StringMatch_Prefix) isStringMatch_MatchType() {} - -func (*StringMatch_Suffix) isStringMatch_MatchType() {} - -func (*StringMatch_Regex) isStringMatch_MatchType() {} - -var File_v1_config_proto protoreflect.FileDescriptor - -var file_v1_config_proto_rawDesc = []byte{ - 0x0a, 0x0f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x15, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x1a, 0x14, 0x76, 0x31, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x14, 0x76, 0x31, 0x2f, 0x6d, 0x6f, 0x63, 0x6b, - 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x83, 0x01, - 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1f, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, - 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, - 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, - 0x01, 0x48, 0x00, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x25, 0x0a, 0x08, 0x65, - 0x71, 0x75, 0x61, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, - 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x08, 0x65, 0x71, 0x75, 0x61, 0x6c, 0x69, - 0x74, 0x79, 0x42, 0x0f, 0x0a, 0x08, 0x63, 0x72, 0x69, 0x74, 0x65, 0x72, 0x69, 0x61, 0x12, 0x03, - 0xf8, 0x42, 0x01, 0x22, 0xe0, 0x01, 0x0a, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x3c, - 0x0a, 0x04, 0x6f, 0x69, 0x64, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x2e, 0x4f, 0x49, 0x44, 0x43, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x04, 0x6f, 0x69, 0x64, 0x63, 0x12, 0x4d, 0x0a, 0x0d, - 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x6f, 0x69, 0x64, 0x63, - 0x2e, 0x4f, 0x49, 0x44, 0x43, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x0c, 0x6f, - 0x69, 0x64, 0x63, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x3c, 0x0a, 0x04, 0x6d, - 0x6f, 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, - 0x31, 0x2e, 0x6d, 0x6f, 0x63, 0x6b, 0x2e, 0x4d, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x48, 0x00, 0x52, 0x04, 0x6d, 0x6f, 0x63, 0x6b, 0x42, 0x0b, 0x0a, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x03, 0xf8, 0x42, 0x01, 0x22, 0xa1, 0x01, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x74, 0x65, - 0x72, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68, - 0x52, 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x41, 0x0a, 0x07, 0x66, 0x69, 0x6c, 0x74, 0x65, - 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, - 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x92, 0x01, 0x02, 0x08, - 0x01, 0x52, 0x07, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x22, 0x8c, 0x05, 0x0a, 0x06, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x44, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x92, 0x01, - 0x02, 0x08, 0x01, 0x52, 0x06, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x0e, 0x6c, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x70, 0x01, 0x52, 0x0d, 0x6c, 0x69, - 0x73, 0x74, 0x65, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2a, 0x0a, 0x0b, 0x6c, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, - 0x42, 0x09, 0xfa, 0x42, 0x06, 0x1a, 0x04, 0x10, 0x80, 0x80, 0x04, 0x52, 0x0a, 0x6c, 0x69, 0x73, - 0x74, 0x65, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x47, 0x0a, 0x09, 0x6c, 0x6f, 0x67, 0x5f, 0x6c, - 0x65, 0x76, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2a, 0xfa, 0x42, 0x27, 0x72, - 0x25, 0x52, 0x05, 0x74, 0x72, 0x61, 0x63, 0x65, 0x52, 0x05, 0x64, 0x65, 0x62, 0x75, 0x67, 0x52, - 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x08, 0x63, 0x72, - 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x52, 0x08, 0x6c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x12, 0x21, 0x0a, 0x07, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x0d, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x2a, 0x02, 0x28, 0x01, 0x52, 0x07, 0x74, 0x68, 0x72, 0x65, - 0x61, 0x64, 0x73, 0x12, 0x47, 0x0a, 0x0d, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x5f, 0x72, - 0x75, 0x6c, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, - 0x76, 0x31, 0x2e, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0c, - 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x13, - 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x75, 0x74, 0x68, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, - 0x31, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x2e, 0x4f, 0x49, 0x44, 0x43, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x11, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x4f, 0x69, 0x64, 0x63, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x38, 0x0a, 0x18, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x75, 0x6e, - 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x55, 0x6e, 0x6d, - 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x32, - 0x0a, 0x15, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x68, - 0x65, 0x61, 0x6c, 0x74, 0x68, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x12, 0x37, 0x0a, 0x12, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x5f, 0x6c, 0x69, 0x73, - 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x05, 0x42, 0x09, - 0xfa, 0x42, 0x06, 0x1a, 0x04, 0x10, 0x80, 0x80, 0x04, 0x52, 0x10, 0x68, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x68, - 0x65, 0x61, 0x6c, 0x74, 0x68, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x5f, 0x70, 0x61, 0x74, - 0x68, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x4c, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x50, 0x61, 0x74, 0x68, 0x22, 0xa3, 0x01, 0x0a, 0x0b, 0x54, 0x72, - 0x69, 0x67, 0x67, 0x65, 0x72, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x49, 0x0a, 0x0e, 0x65, 0x78, 0x63, - 0x6c, 0x75, 0x64, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x22, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x0d, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x50, - 0x61, 0x74, 0x68, 0x73, 0x12, 0x49, 0x0a, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, - 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x61, 0x74, 0x63, 0x68, - 0x52, 0x0d, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x50, 0x61, 0x74, 0x68, 0x73, 0x22, - 0x7f, 0x0a, 0x0b, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, - 0x0a, 0x05, 0x65, 0x78, 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, - 0x05, 0x65, 0x78, 0x61, 0x63, 0x74, 0x12, 0x18, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x12, 0x18, 0x0a, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x00, 0x52, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x05, 0x72, 0x65, - 0x67, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x72, 0x65, 0x67, - 0x65, 0x78, 0x42, 0x0c, 0x0a, 0x0a, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x42, 0xdd, 0x01, 0x0a, 0x19, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x42, 0x0b, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x65, 0x74, 0x72, 0x61, 0x74, - 0x65, 0x69, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2d, - 0x67, 0x6f, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, - 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, - 0x43, 0x58, 0xaa, 0x02, 0x15, 0x41, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x15, 0x41, 0x75, 0x74, - 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5c, - 0x56, 0x31, 0xe2, 0x02, 0x21, 0x41, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x17, 0x41, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x3a, 0x3a, 0x56, 0x31, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_v1_config_proto_rawDescOnce sync.Once - file_v1_config_proto_rawDescData = file_v1_config_proto_rawDesc -) - -func file_v1_config_proto_rawDescGZIP() []byte { - file_v1_config_proto_rawDescOnce.Do(func() { - file_v1_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_config_proto_rawDescData) - }) - return file_v1_config_proto_rawDescData -} - -var file_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6) -var file_v1_config_proto_goTypes = []interface{}{ - (*Match)(nil), // 0: authservice.config.v1.Match - (*Filter)(nil), // 1: authservice.config.v1.Filter - (*FilterChain)(nil), // 2: authservice.config.v1.FilterChain - (*Config)(nil), // 3: authservice.config.v1.Config - (*TriggerRule)(nil), // 4: authservice.config.v1.TriggerRule - (*StringMatch)(nil), // 5: authservice.config.v1.StringMatch - (*oidc.OIDCConfig)(nil), // 6: authservice.config.v1.oidc.OIDCConfig - (*mock.MockConfig)(nil), // 7: authservice.config.v1.mock.MockConfig -} -var file_v1_config_proto_depIdxs = []int32{ - 6, // 0: authservice.config.v1.Filter.oidc:type_name -> authservice.config.v1.oidc.OIDCConfig - 6, // 1: authservice.config.v1.Filter.oidc_override:type_name -> authservice.config.v1.oidc.OIDCConfig - 7, // 2: authservice.config.v1.Filter.mock:type_name -> authservice.config.v1.mock.MockConfig - 0, // 3: authservice.config.v1.FilterChain.match:type_name -> authservice.config.v1.Match - 1, // 4: authservice.config.v1.FilterChain.filters:type_name -> authservice.config.v1.Filter - 2, // 5: authservice.config.v1.Config.chains:type_name -> authservice.config.v1.FilterChain - 4, // 6: authservice.config.v1.Config.trigger_rules:type_name -> authservice.config.v1.TriggerRule - 6, // 7: authservice.config.v1.Config.default_oidc_config:type_name -> authservice.config.v1.oidc.OIDCConfig - 5, // 8: authservice.config.v1.TriggerRule.excluded_paths:type_name -> authservice.config.v1.StringMatch - 5, // 9: authservice.config.v1.TriggerRule.included_paths:type_name -> authservice.config.v1.StringMatch - 10, // [10:10] is the sub-list for method output_type - 10, // [10:10] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name -} - -func init() { file_v1_config_proto_init() } -func file_v1_config_proto_init() { - if File_v1_config_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_v1_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Match); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Filter); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FilterChain); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Config); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TriggerRule); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StringMatch); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_v1_config_proto_msgTypes[0].OneofWrappers = []interface{}{ - (*Match_Prefix)(nil), - (*Match_Equality)(nil), - } - file_v1_config_proto_msgTypes[1].OneofWrappers = []interface{}{ - (*Filter_Oidc)(nil), - (*Filter_OidcOverride)(nil), - (*Filter_Mock)(nil), - } - file_v1_config_proto_msgTypes[5].OneofWrappers = []interface{}{ - (*StringMatch_Exact)(nil), - (*StringMatch_Prefix)(nil), - (*StringMatch_Suffix)(nil), - (*StringMatch_Regex)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_v1_config_proto_rawDesc, - NumEnums: 0, - NumMessages: 6, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_v1_config_proto_goTypes, - DependencyIndexes: file_v1_config_proto_depIdxs, - MessageInfos: file_v1_config_proto_msgTypes, - }.Build() - File_v1_config_proto = out.File - file_v1_config_proto_rawDesc = nil - file_v1_config_proto_goTypes = nil - file_v1_config_proto_depIdxs = nil -} diff --git a/config/gen/go/v1/config.pb.validate.go b/config/gen/go/v1/config.pb.validate.go deleted file mode 100644 index f068ac2..0000000 --- a/config/gen/go/v1/config.pb.validate.go +++ /dev/null @@ -1,1227 +0,0 @@ -// Code generated by protoc-gen-validate. DO NOT EDIT. -// source: v1/config.proto - -package configv1 - -import ( - "bytes" - "errors" - "fmt" - "net" - "net/mail" - "net/url" - "regexp" - "sort" - "strings" - "time" - "unicode/utf8" - - "google.golang.org/protobuf/types/known/anypb" -) - -// ensure the imports are used -var ( - _ = bytes.MinRead - _ = errors.New("") - _ = fmt.Print - _ = utf8.UTFMax - _ = (*regexp.Regexp)(nil) - _ = (*strings.Reader)(nil) - _ = net.IPv4len - _ = time.Duration(0) - _ = (*url.URL)(nil) - _ = (*mail.Address)(nil) - _ = anypb.Any{} - _ = sort.Sort -) - -// Validate checks the field values on Match with the rules defined in the -// proto definition for this message. If any rules are violated, the first -// error encountered is returned, or nil if there are no violations. -func (m *Match) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on Match with the rules defined in the -// proto definition for this message. If any rules are violated, the result is -// a list of violation errors wrapped in MatchMultiError, or nil if none found. -func (m *Match) ValidateAll() error { - return m.validate(true) -} - -func (m *Match) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - if utf8.RuneCountInString(m.GetHeader()) < 1 { - err := MatchValidationError{ - field: "Header", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - oneofCriteriaPresent := false - switch v := m.Criteria.(type) { - case *Match_Prefix: - if v == nil { - err := MatchValidationError{ - field: "Criteria", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - oneofCriteriaPresent = true - - if utf8.RuneCountInString(m.GetPrefix()) < 1 { - err := MatchValidationError{ - field: "Prefix", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - case *Match_Equality: - if v == nil { - err := MatchValidationError{ - field: "Criteria", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - oneofCriteriaPresent = true - - if utf8.RuneCountInString(m.GetEquality()) < 1 { - err := MatchValidationError{ - field: "Equality", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - default: - _ = v // ensures v is used - } - if !oneofCriteriaPresent { - err := MatchValidationError{ - field: "Criteria", - reason: "value is required", - } - if !all { - return err - } - errors = append(errors, err) - } - - if len(errors) > 0 { - return MatchMultiError(errors) - } - - return nil -} - -// MatchMultiError is an error wrapping multiple validation errors returned by -// Match.ValidateAll() if the designated constraints aren't met. -type MatchMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m MatchMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m MatchMultiError) AllErrors() []error { return m } - -// MatchValidationError is the validation error returned by Match.Validate if -// the designated constraints aren't met. -type MatchValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e MatchValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e MatchValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e MatchValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e MatchValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e MatchValidationError) ErrorName() string { return "MatchValidationError" } - -// Error satisfies the builtin error interface -func (e MatchValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sMatch.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = MatchValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = MatchValidationError{} - -// Validate checks the field values on Filter with the rules defined in the -// proto definition for this message. If any rules are violated, the first -// error encountered is returned, or nil if there are no violations. -func (m *Filter) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on Filter with the rules defined in the -// proto definition for this message. If any rules are violated, the result is -// a list of violation errors wrapped in FilterMultiError, or nil if none found. -func (m *Filter) ValidateAll() error { - return m.validate(true) -} - -func (m *Filter) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - oneofTypePresent := false - switch v := m.Type.(type) { - case *Filter_Oidc: - if v == nil { - err := FilterValidationError{ - field: "Type", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - oneofTypePresent = true - - if all { - switch v := interface{}(m.GetOidc()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, FilterValidationError{ - field: "Oidc", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, FilterValidationError{ - field: "Oidc", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetOidc()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return FilterValidationError{ - field: "Oidc", - reason: "embedded message failed validation", - cause: err, - } - } - } - - case *Filter_OidcOverride: - if v == nil { - err := FilterValidationError{ - field: "Type", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - oneofTypePresent = true - - if all { - switch v := interface{}(m.GetOidcOverride()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, FilterValidationError{ - field: "OidcOverride", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, FilterValidationError{ - field: "OidcOverride", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetOidcOverride()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return FilterValidationError{ - field: "OidcOverride", - reason: "embedded message failed validation", - cause: err, - } - } - } - - case *Filter_Mock: - if v == nil { - err := FilterValidationError{ - field: "Type", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - oneofTypePresent = true - - if all { - switch v := interface{}(m.GetMock()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, FilterValidationError{ - field: "Mock", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, FilterValidationError{ - field: "Mock", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetMock()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return FilterValidationError{ - field: "Mock", - reason: "embedded message failed validation", - cause: err, - } - } - } - - default: - _ = v // ensures v is used - } - if !oneofTypePresent { - err := FilterValidationError{ - field: "Type", - reason: "value is required", - } - if !all { - return err - } - errors = append(errors, err) - } - - if len(errors) > 0 { - return FilterMultiError(errors) - } - - return nil -} - -// FilterMultiError is an error wrapping multiple validation errors returned by -// Filter.ValidateAll() if the designated constraints aren't met. -type FilterMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m FilterMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m FilterMultiError) AllErrors() []error { return m } - -// FilterValidationError is the validation error returned by Filter.Validate if -// the designated constraints aren't met. -type FilterValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e FilterValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e FilterValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e FilterValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e FilterValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e FilterValidationError) ErrorName() string { return "FilterValidationError" } - -// Error satisfies the builtin error interface -func (e FilterValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sFilter.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = FilterValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = FilterValidationError{} - -// Validate checks the field values on FilterChain with the rules defined in -// the proto definition for this message. If any rules are violated, the first -// error encountered is returned, or nil if there are no violations. -func (m *FilterChain) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on FilterChain with the rules defined in -// the proto definition for this message. If any rules are violated, the -// result is a list of violation errors wrapped in FilterChainMultiError, or -// nil if none found. -func (m *FilterChain) ValidateAll() error { - return m.validate(true) -} - -func (m *FilterChain) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - if utf8.RuneCountInString(m.GetName()) < 1 { - err := FilterChainValidationError{ - field: "Name", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - if all { - switch v := interface{}(m.GetMatch()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, FilterChainValidationError{ - field: "Match", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, FilterChainValidationError{ - field: "Match", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetMatch()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return FilterChainValidationError{ - field: "Match", - reason: "embedded message failed validation", - cause: err, - } - } - } - - if len(m.GetFilters()) < 1 { - err := FilterChainValidationError{ - field: "Filters", - reason: "value must contain at least 1 item(s)", - } - if !all { - return err - } - errors = append(errors, err) - } - - for idx, item := range m.GetFilters() { - _, _ = idx, item - - if all { - switch v := interface{}(item).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, FilterChainValidationError{ - field: fmt.Sprintf("Filters[%v]", idx), - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, FilterChainValidationError{ - field: fmt.Sprintf("Filters[%v]", idx), - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return FilterChainValidationError{ - field: fmt.Sprintf("Filters[%v]", idx), - reason: "embedded message failed validation", - cause: err, - } - } - } - - } - - if len(errors) > 0 { - return FilterChainMultiError(errors) - } - - return nil -} - -// FilterChainMultiError is an error wrapping multiple validation errors -// returned by FilterChain.ValidateAll() if the designated constraints aren't met. -type FilterChainMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m FilterChainMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m FilterChainMultiError) AllErrors() []error { return m } - -// FilterChainValidationError is the validation error returned by -// FilterChain.Validate if the designated constraints aren't met. -type FilterChainValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e FilterChainValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e FilterChainValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e FilterChainValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e FilterChainValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e FilterChainValidationError) ErrorName() string { return "FilterChainValidationError" } - -// Error satisfies the builtin error interface -func (e FilterChainValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sFilterChain.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = FilterChainValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = FilterChainValidationError{} - -// Validate checks the field values on Config with the rules defined in the -// proto definition for this message. If any rules are violated, the first -// error encountered is returned, or nil if there are no violations. -func (m *Config) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on Config with the rules defined in the -// proto definition for this message. If any rules are violated, the result is -// a list of violation errors wrapped in ConfigMultiError, or nil if none found. -func (m *Config) ValidateAll() error { - return m.validate(true) -} - -func (m *Config) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - if len(m.GetChains()) < 1 { - err := ConfigValidationError{ - field: "Chains", - reason: "value must contain at least 1 item(s)", - } - if !all { - return err - } - errors = append(errors, err) - } - - for idx, item := range m.GetChains() { - _, _ = idx, item - - if all { - switch v := interface{}(item).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, ConfigValidationError{ - field: fmt.Sprintf("Chains[%v]", idx), - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, ConfigValidationError{ - field: fmt.Sprintf("Chains[%v]", idx), - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return ConfigValidationError{ - field: fmt.Sprintf("Chains[%v]", idx), - reason: "embedded message failed validation", - cause: err, - } - } - } - - } - - if ip := net.ParseIP(m.GetListenAddress()); ip == nil { - err := ConfigValidationError{ - field: "ListenAddress", - reason: "value must be a valid IP address", - } - if !all { - return err - } - errors = append(errors, err) - } - - if m.GetListenPort() >= 65536 { - err := ConfigValidationError{ - field: "ListenPort", - reason: "value must be less than 65536", - } - if !all { - return err - } - errors = append(errors, err) - } - - if _, ok := _Config_LogLevel_InLookup[m.GetLogLevel()]; !ok { - err := ConfigValidationError{ - field: "LogLevel", - reason: "value must be in list [trace debug info error critical]", - } - if !all { - return err - } - errors = append(errors, err) - } - - if m.GetThreads() < 1 { - err := ConfigValidationError{ - field: "Threads", - reason: "value must be greater than or equal to 1", - } - if !all { - return err - } - errors = append(errors, err) - } - - for idx, item := range m.GetTriggerRules() { - _, _ = idx, item - - if all { - switch v := interface{}(item).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, ConfigValidationError{ - field: fmt.Sprintf("TriggerRules[%v]", idx), - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, ConfigValidationError{ - field: fmt.Sprintf("TriggerRules[%v]", idx), - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return ConfigValidationError{ - field: fmt.Sprintf("TriggerRules[%v]", idx), - reason: "embedded message failed validation", - cause: err, - } - } - } - - } - - if all { - switch v := interface{}(m.GetDefaultOidcConfig()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, ConfigValidationError{ - field: "DefaultOidcConfig", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, ConfigValidationError{ - field: "DefaultOidcConfig", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetDefaultOidcConfig()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return ConfigValidationError{ - field: "DefaultOidcConfig", - reason: "embedded message failed validation", - cause: err, - } - } - } - - // no validation rules for AllowUnmatchedRequests - - // no validation rules for HealthListenAddress - - if m.GetHealthListenPort() >= 65536 { - err := ConfigValidationError{ - field: "HealthListenPort", - reason: "value must be less than 65536", - } - if !all { - return err - } - errors = append(errors, err) - } - - // no validation rules for HealthListenPath - - if len(errors) > 0 { - return ConfigMultiError(errors) - } - - return nil -} - -// ConfigMultiError is an error wrapping multiple validation errors returned by -// Config.ValidateAll() if the designated constraints aren't met. -type ConfigMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m ConfigMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m ConfigMultiError) AllErrors() []error { return m } - -// ConfigValidationError is the validation error returned by Config.Validate if -// the designated constraints aren't met. -type ConfigValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e ConfigValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e ConfigValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e ConfigValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e ConfigValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e ConfigValidationError) ErrorName() string { return "ConfigValidationError" } - -// Error satisfies the builtin error interface -func (e ConfigValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sConfig.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = ConfigValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = ConfigValidationError{} - -var _Config_LogLevel_InLookup = map[string]struct{}{ - "trace": {}, - "debug": {}, - "info": {}, - "error": {}, - "critical": {}, -} - -// Validate checks the field values on TriggerRule with the rules defined in -// the proto definition for this message. If any rules are violated, the first -// error encountered is returned, or nil if there are no violations. -func (m *TriggerRule) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on TriggerRule with the rules defined in -// the proto definition for this message. If any rules are violated, the -// result is a list of violation errors wrapped in TriggerRuleMultiError, or -// nil if none found. -func (m *TriggerRule) ValidateAll() error { - return m.validate(true) -} - -func (m *TriggerRule) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - for idx, item := range m.GetExcludedPaths() { - _, _ = idx, item - - if all { - switch v := interface{}(item).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, TriggerRuleValidationError{ - field: fmt.Sprintf("ExcludedPaths[%v]", idx), - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, TriggerRuleValidationError{ - field: fmt.Sprintf("ExcludedPaths[%v]", idx), - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return TriggerRuleValidationError{ - field: fmt.Sprintf("ExcludedPaths[%v]", idx), - reason: "embedded message failed validation", - cause: err, - } - } - } - - } - - for idx, item := range m.GetIncludedPaths() { - _, _ = idx, item - - if all { - switch v := interface{}(item).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, TriggerRuleValidationError{ - field: fmt.Sprintf("IncludedPaths[%v]", idx), - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, TriggerRuleValidationError{ - field: fmt.Sprintf("IncludedPaths[%v]", idx), - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return TriggerRuleValidationError{ - field: fmt.Sprintf("IncludedPaths[%v]", idx), - reason: "embedded message failed validation", - cause: err, - } - } - } - - } - - if len(errors) > 0 { - return TriggerRuleMultiError(errors) - } - - return nil -} - -// TriggerRuleMultiError is an error wrapping multiple validation errors -// returned by TriggerRule.ValidateAll() if the designated constraints aren't met. -type TriggerRuleMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m TriggerRuleMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m TriggerRuleMultiError) AllErrors() []error { return m } - -// TriggerRuleValidationError is the validation error returned by -// TriggerRule.Validate if the designated constraints aren't met. -type TriggerRuleValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e TriggerRuleValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e TriggerRuleValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e TriggerRuleValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e TriggerRuleValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e TriggerRuleValidationError) ErrorName() string { return "TriggerRuleValidationError" } - -// Error satisfies the builtin error interface -func (e TriggerRuleValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sTriggerRule.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = TriggerRuleValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = TriggerRuleValidationError{} - -// Validate checks the field values on StringMatch with the rules defined in -// the proto definition for this message. If any rules are violated, the first -// error encountered is returned, or nil if there are no violations. -func (m *StringMatch) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on StringMatch with the rules defined in -// the proto definition for this message. If any rules are violated, the -// result is a list of violation errors wrapped in StringMatchMultiError, or -// nil if none found. -func (m *StringMatch) ValidateAll() error { - return m.validate(true) -} - -func (m *StringMatch) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - switch v := m.MatchType.(type) { - case *StringMatch_Exact: - if v == nil { - err := StringMatchValidationError{ - field: "MatchType", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - // no validation rules for Exact - case *StringMatch_Prefix: - if v == nil { - err := StringMatchValidationError{ - field: "MatchType", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - // no validation rules for Prefix - case *StringMatch_Suffix: - if v == nil { - err := StringMatchValidationError{ - field: "MatchType", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - // no validation rules for Suffix - case *StringMatch_Regex: - if v == nil { - err := StringMatchValidationError{ - field: "MatchType", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - // no validation rules for Regex - default: - _ = v // ensures v is used - } - - if len(errors) > 0 { - return StringMatchMultiError(errors) - } - - return nil -} - -// StringMatchMultiError is an error wrapping multiple validation errors -// returned by StringMatch.ValidateAll() if the designated constraints aren't met. -type StringMatchMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m StringMatchMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m StringMatchMultiError) AllErrors() []error { return m } - -// StringMatchValidationError is the validation error returned by -// StringMatch.Validate if the designated constraints aren't met. -type StringMatchValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e StringMatchValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e StringMatchValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e StringMatchValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e StringMatchValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e StringMatchValidationError) ErrorName() string { return "StringMatchValidationError" } - -// Error satisfies the builtin error interface -func (e StringMatchValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sStringMatch.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = StringMatchValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = StringMatchValidationError{} diff --git a/config/gen/go/v1/mock/config.pb.go b/config/gen/go/v1/mock/config.pb.go deleted file mode 100644 index 66b25b5..0000000 --- a/config/gen/go/v1/mock/config.pb.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.33.0 -// protoc (unknown) -// source: v1/mock/config.proto - -package mock - -import ( - reflect "reflect" - sync "sync" - - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// Mock filter config. The only thing which can be defined is whether it -// allows or rejects any request it matches. -type MockConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Boolean specifying whether the filter should return OK for any - // request it matches. Defaults to false (not OK). - Allow bool `protobuf:"varint,1,opt,name=allow,proto3" json:"allow,omitempty"` -} - -func (x *MockConfig) Reset() { - *x = MockConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_mock_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *MockConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MockConfig) ProtoMessage() {} - -func (x *MockConfig) ProtoReflect() protoreflect.Message { - mi := &file_v1_mock_config_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use MockConfig.ProtoReflect.Descriptor instead. -func (*MockConfig) Descriptor() ([]byte, []int) { - return file_v1_mock_config_proto_rawDescGZIP(), []int{0} -} - -func (x *MockConfig) GetAllow() bool { - if x != nil { - return x.Allow - } - return false -} - -var File_v1_mock_config_proto protoreflect.FileDescriptor - -var file_v1_mock_config_proto_rawDesc = []byte{ - 0x0a, 0x14, 0x76, 0x31, 0x2f, 0x6d, 0x6f, 0x63, 0x6b, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1a, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x6d, 0x6f, - 0x63, 0x6b, 0x22, 0x22, 0x0a, 0x0a, 0x4d, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x14, 0x0a, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x42, 0xf4, 0x01, 0x0a, 0x1e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x6d, 0x6f, 0x63, 0x6b, 0x42, 0x0b, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x65, 0x74, 0x72, 0x61, 0x74, 0x65, 0x69, 0x6f, 0x2f, 0x61, - 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2d, 0x67, 0x6f, 0x2f, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x2f, 0x6d, - 0x6f, 0x63, 0x6b, 0xa2, 0x02, 0x04, 0x41, 0x43, 0x56, 0x4d, 0xaa, 0x02, 0x1a, 0x41, 0x75, 0x74, - 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, - 0x56, 0x31, 0x2e, 0x4d, 0x6f, 0x63, 0x6b, 0xca, 0x02, 0x1a, 0x41, 0x75, 0x74, 0x68, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5c, 0x56, 0x31, 0x5c, - 0x4d, 0x6f, 0x63, 0x6b, 0xe2, 0x02, 0x26, 0x41, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5c, 0x56, 0x31, 0x5c, 0x4d, 0x6f, 0x63, - 0x6b, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1d, - 0x41, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x3a, 0x3a, 0x56, 0x31, 0x3a, 0x3a, 0x4d, 0x6f, 0x63, 0x6b, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_v1_mock_config_proto_rawDescOnce sync.Once - file_v1_mock_config_proto_rawDescData = file_v1_mock_config_proto_rawDesc -) - -func file_v1_mock_config_proto_rawDescGZIP() []byte { - file_v1_mock_config_proto_rawDescOnce.Do(func() { - file_v1_mock_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_mock_config_proto_rawDescData) - }) - return file_v1_mock_config_proto_rawDescData -} - -var file_v1_mock_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_v1_mock_config_proto_goTypes = []interface{}{ - (*MockConfig)(nil), // 0: authservice.config.v1.mock.MockConfig -} -var file_v1_mock_config_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_v1_mock_config_proto_init() } -func file_v1_mock_config_proto_init() { - if File_v1_mock_config_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_v1_mock_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MockConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_v1_mock_config_proto_rawDesc, - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_v1_mock_config_proto_goTypes, - DependencyIndexes: file_v1_mock_config_proto_depIdxs, - MessageInfos: file_v1_mock_config_proto_msgTypes, - }.Build() - File_v1_mock_config_proto = out.File - file_v1_mock_config_proto_rawDesc = nil - file_v1_mock_config_proto_goTypes = nil - file_v1_mock_config_proto_depIdxs = nil -} diff --git a/config/gen/go/v1/mock/config.pb.validate.go b/config/gen/go/v1/mock/config.pb.validate.go deleted file mode 100644 index b4c61fc..0000000 --- a/config/gen/go/v1/mock/config.pb.validate.go +++ /dev/null @@ -1,137 +0,0 @@ -// Code generated by protoc-gen-validate. DO NOT EDIT. -// source: v1/mock/config.proto - -package mock - -import ( - "bytes" - "errors" - "fmt" - "net" - "net/mail" - "net/url" - "regexp" - "sort" - "strings" - "time" - "unicode/utf8" - - "google.golang.org/protobuf/types/known/anypb" -) - -// ensure the imports are used -var ( - _ = bytes.MinRead - _ = errors.New("") - _ = fmt.Print - _ = utf8.UTFMax - _ = (*regexp.Regexp)(nil) - _ = (*strings.Reader)(nil) - _ = net.IPv4len - _ = time.Duration(0) - _ = (*url.URL)(nil) - _ = (*mail.Address)(nil) - _ = anypb.Any{} - _ = sort.Sort -) - -// Validate checks the field values on MockConfig with the rules defined in the -// proto definition for this message. If any rules are violated, the first -// error encountered is returned, or nil if there are no violations. -func (m *MockConfig) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on MockConfig with the rules defined in -// the proto definition for this message. If any rules are violated, the -// result is a list of violation errors wrapped in MockConfigMultiError, or -// nil if none found. -func (m *MockConfig) ValidateAll() error { - return m.validate(true) -} - -func (m *MockConfig) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - // no validation rules for Allow - - if len(errors) > 0 { - return MockConfigMultiError(errors) - } - - return nil -} - -// MockConfigMultiError is an error wrapping multiple validation errors -// returned by MockConfig.ValidateAll() if the designated constraints aren't met. -type MockConfigMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m MockConfigMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m MockConfigMultiError) AllErrors() []error { return m } - -// MockConfigValidationError is the validation error returned by -// MockConfig.Validate if the designated constraints aren't met. -type MockConfigValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e MockConfigValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e MockConfigValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e MockConfigValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e MockConfigValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e MockConfigValidationError) ErrorName() string { return "MockConfigValidationError" } - -// Error satisfies the builtin error interface -func (e MockConfigValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sMockConfig.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = MockConfigValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = MockConfigValidationError{} diff --git a/config/gen/go/v1/oidc/config.pb.go b/config/gen/go/v1/oidc/config.pb.go deleted file mode 100644 index 21ba7f2..0000000 --- a/config/gen/go/v1/oidc/config.pb.go +++ /dev/null @@ -1,1059 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.33.0 -// protoc (unknown) -// source: v1/oidc/config.proto - -package oidc - -import ( - reflect "reflect" - sync "sync" - - _ "github.com/envoyproxy/protoc-gen-validate/validate" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - durationpb "google.golang.org/protobuf/types/known/durationpb" - structpb "google.golang.org/protobuf/types/known/structpb" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// Defines how a token obtained through an OIDC flow is forwarded to services. -type TokenConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The name of the header that Authservice adds to the request when forwarding to services. - // The value of this header will contain the `preamble` and the token. - // This value is case-insensitive, as http header names are case-insensitive. - // Note that this value must be `Authorization` for the - // [Istio Authentication Policy](https://istio.io/docs/tasks/security/authn-policy/) - // to inspect the token. - // Required. - Header string `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"` - // The authentication scheme of the token. - // For example, when the preamble is `Bearer` and `header` is `Authorization`, the following - // header will be added to the request to the service: `Authorization: Bearer ID_TOKEN_VALUE`. - // Note that this value must be `Bearer`, case-sensitive, when header is `Authorization`. - // Optional. - Preamble string `protobuf:"bytes,2,opt,name=preamble,proto3" json:"preamble,omitempty"` -} - -func (x *TokenConfig) Reset() { - *x = TokenConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_oidc_config_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *TokenConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*TokenConfig) ProtoMessage() {} - -func (x *TokenConfig) ProtoReflect() protoreflect.Message { - mi := &file_v1_oidc_config_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use TokenConfig.ProtoReflect.Descriptor instead. -func (*TokenConfig) Descriptor() ([]byte, []int) { - return file_v1_oidc_config_proto_rawDescGZIP(), []int{0} -} - -func (x *TokenConfig) GetHeader() string { - if x != nil { - return x.Header - } - return "" -} - -func (x *TokenConfig) GetPreamble() string { - if x != nil { - return x.Preamble - } - return "" -} - -// When specified, the Authservice will use the configured Redis server to store session data -type RedisConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The Redis server uri, e.g. "tcp://127.0.0.1:6379" - ServerUri string `protobuf:"bytes,1,opt,name=server_uri,json=serverUri,proto3" json:"server_uri,omitempty"` -} - -func (x *RedisConfig) Reset() { - *x = RedisConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_oidc_config_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *RedisConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*RedisConfig) ProtoMessage() {} - -func (x *RedisConfig) ProtoReflect() protoreflect.Message { - mi := &file_v1_oidc_config_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use RedisConfig.ProtoReflect.Descriptor instead. -func (*RedisConfig) Descriptor() ([]byte, []int) { - return file_v1_oidc_config_proto_rawDescGZIP(), []int{1} -} - -func (x *RedisConfig) GetServerUri() string { - if x != nil { - return x.ServerUri - } - return "" -} - -// When specified, the Authservice will destroy the Authservice session when a request is -// made to the configured path. -type LogoutConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // A http request path that the Authservice matches against to initiate logout. - // Whenever a request is made to that path, the Authservice will remove the Authservice-specific - // cookies and respond with a redirect to the configured `redirect_uri`. Removing the cookies - // causes the user to be unauthenticated in future requests. - // If the service application has its own logout controller, then it may be desirable to have its - // logout controller redirect to this path. If the service application does not need its own logout - // controller, then the application's logout button/link's href can GET or POST directly to this path. - // Required. - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - // A URI specifying the destination to which the Authservice will redirect any request made to the - // logout `path`. For example, it may be desirable to redirect the logged out user to the homepage - // of the service application, or to the - // [logout endpoint of the OIDC Provider](https://openid.net/specs/openid-connect-session-1_0.html#RPLogout). - // As with all redirects, the user's browser will perform a GET to this URI. - // Required. - RedirectUri string `protobuf:"bytes,2,opt,name=redirect_uri,json=redirectUri,proto3" json:"redirect_uri,omitempty"` -} - -func (x *LogoutConfig) Reset() { - *x = LogoutConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_oidc_config_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *LogoutConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LogoutConfig) ProtoMessage() {} - -func (x *LogoutConfig) ProtoReflect() protoreflect.Message { - mi := &file_v1_oidc_config_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use LogoutConfig.ProtoReflect.Descriptor instead. -func (*LogoutConfig) Descriptor() ([]byte, []int) { - return file_v1_oidc_config_proto_rawDescGZIP(), []int{2} -} - -func (x *LogoutConfig) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *LogoutConfig) GetRedirectUri() string { - if x != nil { - return x.RedirectUri - } - return "" -} - -// The configuration of an OpenID Connect filter that can be used to retrieve identity and access tokens -// via the standard authorization code grant flow from an OIDC Provider. -type OIDCConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig). - // If this is set, the endpoints will be dynamically retrieved from the OIDC Provider's configuration endpoint. - ConfigurationUri string `protobuf:"bytes,19,opt,name=configuration_uri,json=configurationUri,proto3" json:"configuration_uri,omitempty"` - // The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). - // Required if `configuration_uri` is not set. - AuthorizationUri string `protobuf:"bytes,1,opt,name=authorization_uri,json=authorizationUri,proto3" json:"authorization_uri,omitempty"` - // The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). - // Required if `configuration_uri` is not set. - TokenUri string `protobuf:"bytes,2,opt,name=token_uri,json=tokenUri,proto3" json:"token_uri,omitempty"` - // This value will be used as the `redirect_uri` param of the authorization code grant - // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - // This URL must be one of the Redirection URI values for the Client pre-registered at the OIDC provider. - // Note: The Istio gateway's VirtualService must be prepared to ensure that this URL will get routed to - // the service so that the Authservice can intercept the request and handle it - // (see [example](https://github.com/istio-ecosystem/authservice/blob/master/bookinfo-example/config/bookinfo-gateway.yaml)). - // Required. - CallbackUri string `protobuf:"bytes,3,opt,name=callback_uri,json=callbackUri,proto3" json:"callback_uri,omitempty"` - // Types that are assignable to JwksConfig: - // - // *OIDCConfig_Jwks - // *OIDCConfig_JwksFetcher - JwksConfig isOIDCConfig_JwksConfig `protobuf_oneof:"jwks_config"` - // The OIDC client ID assigned to the filter to be used in the - // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - // Required. - ClientId string `protobuf:"bytes,5,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` - // Types that are assignable to ClientSecretConfig: - // - // *OIDCConfig_ClientSecret - // *OIDCConfig_ClientSecretRef - ClientSecretConfig isOIDCConfig_ClientSecretConfig `protobuf_oneof:"client_secret_config"` - // Additional scopes passed to the OIDC Provider in the - // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - // The `openid` scope is always sent to the OIDC Provider, and does not need to be specified here. - // Required, but an empty array is allowed. - Scopes []string `protobuf:"bytes,7,rep,name=scopes,proto3" json:"scopes,omitempty"` - // A unique identifier of the Authservice's browser cookies. Can be any string. - // Needed when multiple services in the same domain are each protected by - // their own Authservice, in which case each service's Authservice should have - // a unique value to avoid cookie name conflicts. Also needed when an Authservice - // is configured with multiple `oidc` filters (across multiple `chains`), each - // sharing a Redis server for their session storage, to avoid having those - // `oidc` filters read/write the same sessions in Redis. - // Optional. - CookieNamePrefix string `protobuf:"bytes,8,opt,name=cookie_name_prefix,json=cookieNamePrefix,proto3" json:"cookie_name_prefix,omitempty"` - // The configuration for adding ID Tokens as headers to requests forwarded to a service. - // Required. - IdToken *TokenConfig `protobuf:"bytes,9,opt,name=id_token,json=idToken,proto3" json:"id_token,omitempty"` - // The configuration for adding Access Tokens as headers to requests forwarded to a service. - // Optional. - AccessToken *TokenConfig `protobuf:"bytes,10,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` - // When specified, the Authservice will destroy the Authservice session when a request is - // made to the configured path. - // Optional. - Logout *LogoutConfig `protobuf:"bytes,11,opt,name=logout,proto3" json:"logout,omitempty"` - // The Authservice associates obtained OIDC tokens with a session ID in a session store. - // It also stores some temporary information during the login process into the session store, - // which will be removed when the user finishes the login. - // This configuration option sets the number of seconds since a user's session with the Authservice has started - // until that session should expire. - // When configured to `0`, which is the default value, the session will never timeout based on the time - // that it was started, but can still timeout due to being idle. - // When both `absolute_session_timeout` and `idle_session_timeout` are zero, then sessions will never - // expire. These settings do not affect how quickly the OIDC tokens contained inside the user's session expire. - // Optional. - AbsoluteSessionTimeout uint32 `protobuf:"varint,12,opt,name=absolute_session_timeout,json=absoluteSessionTimeout,proto3" json:"absolute_session_timeout,omitempty"` - // The Authservice associates obtained OIDC tokens with a session ID in a session store. - // It also stores some temporary information during the login process into the session store, - // which will be removed when the user finishes the login. - // This configuration option sets the number of seconds since the most recent incoming request from that user - // until the user's session with the Authservice should expire. - // When configured to `0`, which is the default value, session expiration will not consider idle time, - // but can still consider timeout based on maximum absolute time since added. - // When both `absolute_session_timeout` and `idle_session_timeout` are zero, then sessions will never - // expire. These settings do not affect how quickly the OIDC tokens contained inside the user's session expire. - // Optional. - IdleSessionTimeout uint32 `protobuf:"varint,13,opt,name=idle_session_timeout,json=idleSessionTimeout,proto3" json:"idle_session_timeout,omitempty"` - // When specified, the Authservice will trust the specified Certificate Authority when performing HTTPS calls to - // the OIDC Identity Provider. - // - // Types that are assignable to TrustedCaConfig: - // - // *OIDCConfig_TrustedCertificateAuthority - // *OIDCConfig_TrustedCertificateAuthorityFile - TrustedCaConfig isOIDCConfig_TrustedCaConfig `protobuf_oneof:"trusted_ca_config"` - // The duration between refreshes of the trusted certificate authority if `trusted_certificate_authority_file` is set. - // Unset or 0 (the default) disables the refresh, useful is no rotation is expected. - // Is a String that ends in `s` to indicate seconds and is preceded by the number of seconds, e.g. `120s` (represents 2 minutes). - // Optional. - TrustedCertificateAuthorityRefreshInterval *durationpb.Duration `protobuf:"bytes,22,opt,name=trusted_certificate_authority_refresh_interval,json=trustedCertificateAuthorityRefreshInterval,proto3" json:"trusted_certificate_authority_refresh_interval,omitempty"` - // The Authservice makes two kinds of direct network connections directly to the OIDC Provider. - // Both are POST requests to the configured `token_uri` of the OIDC Provider. - // The first is to exchange the authorization code for tokens, and the other is to use the - // refresh token to obtain new tokens. Configure the `proxy_uri` when - // both of these requests should be made through a web proxy. The format of `proxy_uri` is - // `http://proxyserver.example.com:8080`, where `:` is optional. - // Userinfo (usernames and passwords) in the `proxy_uri` setting are not yet supported. - // The `proxy_uri` should always start with `http://`. - // The Authservice will upgrade the connection to the OIDC provider to HTTPS using - // an HTTP CONNECT request to the proxy server. The proxy server will see the hostname and port number - // of the OIDC provider in plain text in the CONNECT request, but all other communication will occur - // over an encrypted HTTPS connection negotiated directly between the Authservice and - // the OIDC provider. See also the related `trusted_certificate_authority` configuration option. - // Optional. - ProxyUri string `protobuf:"bytes,15,opt,name=proxy_uri,json=proxyUri,proto3" json:"proxy_uri,omitempty"` - // When specified, the Authservice will use the configured Redis server to store session data. - // Optional. - RedisSessionStoreConfig *RedisConfig `protobuf:"bytes,16,opt,name=redis_session_store_config,json=redisSessionStoreConfig,proto3" json:"redis_session_store_config,omitempty"` - // If set to true, the verification of the destination certificate will be skipped when - // making a request to the Token Endpoint. This option is useful when you want to use a - // self-signed certificate for testing purposes, but basically should not be set to true - // in any other cases. - // Optional. - SkipVerifyPeerCert *structpb.Value `protobuf:"bytes,18,opt,name=skip_verify_peer_cert,json=skipVerifyPeerCert,proto3" json:"skip_verify_peer_cert,omitempty"` // keep this field out from the trusted_ca_config one of for backward compatibility. -} - -func (x *OIDCConfig) Reset() { - *x = OIDCConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_oidc_config_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *OIDCConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*OIDCConfig) ProtoMessage() {} - -func (x *OIDCConfig) ProtoReflect() protoreflect.Message { - mi := &file_v1_oidc_config_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use OIDCConfig.ProtoReflect.Descriptor instead. -func (*OIDCConfig) Descriptor() ([]byte, []int) { - return file_v1_oidc_config_proto_rawDescGZIP(), []int{3} -} - -func (x *OIDCConfig) GetConfigurationUri() string { - if x != nil { - return x.ConfigurationUri - } - return "" -} - -func (x *OIDCConfig) GetAuthorizationUri() string { - if x != nil { - return x.AuthorizationUri - } - return "" -} - -func (x *OIDCConfig) GetTokenUri() string { - if x != nil { - return x.TokenUri - } - return "" -} - -func (x *OIDCConfig) GetCallbackUri() string { - if x != nil { - return x.CallbackUri - } - return "" -} - -func (m *OIDCConfig) GetJwksConfig() isOIDCConfig_JwksConfig { - if m != nil { - return m.JwksConfig - } - return nil -} - -func (x *OIDCConfig) GetJwks() string { - if x, ok := x.GetJwksConfig().(*OIDCConfig_Jwks); ok { - return x.Jwks - } - return "" -} - -func (x *OIDCConfig) GetJwksFetcher() *OIDCConfig_JwksFetcherConfig { - if x, ok := x.GetJwksConfig().(*OIDCConfig_JwksFetcher); ok { - return x.JwksFetcher - } - return nil -} - -func (x *OIDCConfig) GetClientId() string { - if x != nil { - return x.ClientId - } - return "" -} - -func (m *OIDCConfig) GetClientSecretConfig() isOIDCConfig_ClientSecretConfig { - if m != nil { - return m.ClientSecretConfig - } - return nil -} - -func (x *OIDCConfig) GetClientSecret() string { - if x, ok := x.GetClientSecretConfig().(*OIDCConfig_ClientSecret); ok { - return x.ClientSecret - } - return "" -} - -func (x *OIDCConfig) GetClientSecretRef() *OIDCConfig_SecretReference { - if x, ok := x.GetClientSecretConfig().(*OIDCConfig_ClientSecretRef); ok { - return x.ClientSecretRef - } - return nil -} - -func (x *OIDCConfig) GetScopes() []string { - if x != nil { - return x.Scopes - } - return nil -} - -func (x *OIDCConfig) GetCookieNamePrefix() string { - if x != nil { - return x.CookieNamePrefix - } - return "" -} - -func (x *OIDCConfig) GetIdToken() *TokenConfig { - if x != nil { - return x.IdToken - } - return nil -} - -func (x *OIDCConfig) GetAccessToken() *TokenConfig { - if x != nil { - return x.AccessToken - } - return nil -} - -func (x *OIDCConfig) GetLogout() *LogoutConfig { - if x != nil { - return x.Logout - } - return nil -} - -func (x *OIDCConfig) GetAbsoluteSessionTimeout() uint32 { - if x != nil { - return x.AbsoluteSessionTimeout - } - return 0 -} - -func (x *OIDCConfig) GetIdleSessionTimeout() uint32 { - if x != nil { - return x.IdleSessionTimeout - } - return 0 -} - -func (m *OIDCConfig) GetTrustedCaConfig() isOIDCConfig_TrustedCaConfig { - if m != nil { - return m.TrustedCaConfig - } - return nil -} - -func (x *OIDCConfig) GetTrustedCertificateAuthority() string { - if x, ok := x.GetTrustedCaConfig().(*OIDCConfig_TrustedCertificateAuthority); ok { - return x.TrustedCertificateAuthority - } - return "" -} - -func (x *OIDCConfig) GetTrustedCertificateAuthorityFile() string { - if x, ok := x.GetTrustedCaConfig().(*OIDCConfig_TrustedCertificateAuthorityFile); ok { - return x.TrustedCertificateAuthorityFile - } - return "" -} - -func (x *OIDCConfig) GetTrustedCertificateAuthorityRefreshInterval() *durationpb.Duration { - if x != nil { - return x.TrustedCertificateAuthorityRefreshInterval - } - return nil -} - -func (x *OIDCConfig) GetProxyUri() string { - if x != nil { - return x.ProxyUri - } - return "" -} - -func (x *OIDCConfig) GetRedisSessionStoreConfig() *RedisConfig { - if x != nil { - return x.RedisSessionStoreConfig - } - return nil -} - -func (x *OIDCConfig) GetSkipVerifyPeerCert() *structpb.Value { - if x != nil { - return x.SkipVerifyPeerCert - } - return nil -} - -type isOIDCConfig_JwksConfig interface { - isOIDCConfig_JwksConfig() -} - -type OIDCConfig_Jwks struct { - // The JSON JWKS response from the OIDC provider’s `jwks_uri` URI which can be found in - // the OIDC provider's - // [configuration response](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). - // Note that this JSON value must be escaped when embedded in a json configmap - // (see [example](https://github.com/istio-ecosystem/authservice/blob/master/bookinfo-example/config/authservice-configmap-template.yaml)). - // Used during token verification. - Jwks string `protobuf:"bytes,4,opt,name=jwks,proto3,oneof"` -} - -type OIDCConfig_JwksFetcher struct { - // Configuration to allow JWKs to be retrieved and updated asynchronously at regular intervals. - JwksFetcher *OIDCConfig_JwksFetcherConfig `protobuf:"bytes,17,opt,name=jwks_fetcher,json=jwksFetcher,proto3,oneof"` -} - -func (*OIDCConfig_Jwks) isOIDCConfig_JwksConfig() {} - -func (*OIDCConfig_JwksFetcher) isOIDCConfig_JwksConfig() {} - -type isOIDCConfig_ClientSecretConfig interface { - isOIDCConfig_ClientSecretConfig() -} - -type OIDCConfig_ClientSecret struct { - // The OIDC client secret assigned to the filter to be used in the - // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - // This field keeps the client secret in plain text. Recommend to use `client_secret_ref` instead - // when running in a Kubernetes cluster. - ClientSecret string `protobuf:"bytes,6,opt,name=client_secret,json=clientSecret,proto3,oneof"` -} - -type OIDCConfig_ClientSecretRef struct { - // The Kubernetes secret that contains the OIDC client secret assigned to the filter to be used in the - // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - // - // This is an Opaque secret. The client secret should be stored in the key "client-secret". - // This filed is only valid when running in a Kubernetes cluster. - ClientSecretRef *OIDCConfig_SecretReference `protobuf:"bytes,21,opt,name=client_secret_ref,json=clientSecretRef,proto3,oneof"` -} - -func (*OIDCConfig_ClientSecret) isOIDCConfig_ClientSecretConfig() {} - -func (*OIDCConfig_ClientSecretRef) isOIDCConfig_ClientSecretConfig() {} - -type isOIDCConfig_TrustedCaConfig interface { - isOIDCConfig_TrustedCaConfig() -} - -type OIDCConfig_TrustedCertificateAuthority struct { - // String PEM-encoded certificate authority to trust when performing HTTPS calls to the OIDC Identity Provider. - // Optional. - TrustedCertificateAuthority string `protobuf:"bytes,14,opt,name=trusted_certificate_authority,json=trustedCertificateAuthority,proto3,oneof"` -} - -type OIDCConfig_TrustedCertificateAuthorityFile struct { - // The file path to the PEM-encoded certificate authority to trust when performing HTTPS calls to the OIDC Identity Provider. - // Optional. - TrustedCertificateAuthorityFile string `protobuf:"bytes,20,opt,name=trusted_certificate_authority_file,json=trustedCertificateAuthorityFile,proto3,oneof"` -} - -func (*OIDCConfig_TrustedCertificateAuthority) isOIDCConfig_TrustedCaConfig() {} - -func (*OIDCConfig_TrustedCertificateAuthorityFile) isOIDCConfig_TrustedCaConfig() {} - -// This message defines a setting to allow asynchronous retrieval and update of the JWK for -// JWT validation at regular intervals. -type OIDCConfig_JwksFetcherConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Request URI that has the JWKs. - // Required if `configuration_uri` is not set. - JwksUri string `protobuf:"bytes,1,opt,name=jwks_uri,json=jwksUri,proto3" json:"jwks_uri,omitempty"` - // Request interval to check whether new JWKs are available. If not specified, - // default to 1200 seconds, 20min. - // Optional. - PeriodicFetchIntervalSec uint32 `protobuf:"varint,2,opt,name=periodic_fetch_interval_sec,json=periodicFetchIntervalSec,proto3" json:"periodic_fetch_interval_sec,omitempty"` - // If set to true, the verification of the destination certificate will be skipped when - // making a request to the JWKs URI. This option is useful when you want to use a - // self-signed certificate for testing purposes, but basically should not be set to - // true in any other cases. - // Optional. - // Deprecated: Use the one from the OIDCConfig instead. - // - // Deprecated: Marked as deprecated in v1/oidc/config.proto. - SkipVerifyPeerCert *structpb.Value `protobuf:"bytes,3,opt,name=skip_verify_peer_cert,json=skipVerifyPeerCert,proto3" json:"skip_verify_peer_cert,omitempty"` -} - -func (x *OIDCConfig_JwksFetcherConfig) Reset() { - *x = OIDCConfig_JwksFetcherConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_oidc_config_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *OIDCConfig_JwksFetcherConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*OIDCConfig_JwksFetcherConfig) ProtoMessage() {} - -func (x *OIDCConfig_JwksFetcherConfig) ProtoReflect() protoreflect.Message { - mi := &file_v1_oidc_config_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use OIDCConfig_JwksFetcherConfig.ProtoReflect.Descriptor instead. -func (*OIDCConfig_JwksFetcherConfig) Descriptor() ([]byte, []int) { - return file_v1_oidc_config_proto_rawDescGZIP(), []int{3, 0} -} - -func (x *OIDCConfig_JwksFetcherConfig) GetJwksUri() string { - if x != nil { - return x.JwksUri - } - return "" -} - -func (x *OIDCConfig_JwksFetcherConfig) GetPeriodicFetchIntervalSec() uint32 { - if x != nil { - return x.PeriodicFetchIntervalSec - } - return 0 -} - -// Deprecated: Marked as deprecated in v1/oidc/config.proto. -func (x *OIDCConfig_JwksFetcherConfig) GetSkipVerifyPeerCert() *structpb.Value { - if x != nil { - return x.SkipVerifyPeerCert - } - return nil -} - -// This message defines a reference to a Kubernetes Secret resource. -type OIDCConfig_SecretReference struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The namespace of the referenced Secret, if not set, default to "default" namespace. - Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - // The name of the referenced Secret. - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` -} - -func (x *OIDCConfig_SecretReference) Reset() { - *x = OIDCConfig_SecretReference{} - if protoimpl.UnsafeEnabled { - mi := &file_v1_oidc_config_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *OIDCConfig_SecretReference) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*OIDCConfig_SecretReference) ProtoMessage() {} - -func (x *OIDCConfig_SecretReference) ProtoReflect() protoreflect.Message { - mi := &file_v1_oidc_config_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use OIDCConfig_SecretReference.ProtoReflect.Descriptor instead. -func (*OIDCConfig_SecretReference) Descriptor() ([]byte, []int) { - return file_v1_oidc_config_proto_rawDescGZIP(), []int{3, 1} -} - -func (x *OIDCConfig_SecretReference) GetNamespace() string { - if x != nil { - return x.Namespace - } - return "" -} - -func (x *OIDCConfig_SecretReference) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -var File_v1_oidc_config_proto protoreflect.FileDescriptor - -var file_v1_oidc_config_proto_rawDesc = []byte{ - 0x0a, 0x14, 0x76, 0x31, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1a, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x6f, 0x69, - 0x64, 0x63, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, - 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x4a, 0x0a, 0x0b, 0x54, 0x6f, 0x6b, - 0x65, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1f, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, - 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, - 0x01, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, - 0x61, 0x6d, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x65, - 0x61, 0x6d, 0x62, 0x6c, 0x65, 0x22, 0x35, 0x0a, 0x0b, 0x52, 0x65, 0x64, 0x69, 0x73, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x26, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x75, - 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, - 0x01, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x55, 0x72, 0x69, 0x22, 0x57, 0x0a, 0x0c, - 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1b, 0x0a, 0x04, - 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, - 0x02, 0x10, 0x01, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x2a, 0x0a, 0x0c, 0x72, 0x65, 0x64, - 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x55, 0x72, 0x69, 0x22, 0x9e, 0x0d, 0x0a, 0x0a, 0x4f, 0x49, 0x44, 0x43, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2b, 0x0a, 0x11, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, - 0x69, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x61, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x69, 0x12, 0x1b, - 0x0a, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x72, 0x69, 0x12, 0x2a, 0x0a, 0x0c, 0x63, - 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x0b, 0x63, 0x61, 0x6c, 0x6c, - 0x62, 0x61, 0x63, 0x6b, 0x55, 0x72, 0x69, 0x12, 0x14, 0x0a, 0x04, 0x6a, 0x77, 0x6b, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6a, 0x77, 0x6b, 0x73, 0x12, 0x5d, 0x0a, - 0x0c, 0x6a, 0x77, 0x6b, 0x73, 0x5f, 0x66, 0x65, 0x74, 0x63, 0x68, 0x65, 0x72, 0x18, 0x11, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x6f, 0x69, 0x64, 0x63, - 0x2e, 0x4f, 0x49, 0x44, 0x43, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4a, 0x77, 0x6b, 0x73, - 0x46, 0x65, 0x74, 0x63, 0x68, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, - 0x0b, 0x6a, 0x77, 0x6b, 0x73, 0x46, 0x65, 0x74, 0x63, 0x68, 0x65, 0x72, 0x12, 0x24, 0x0a, 0x09, - 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, - 0x10, 0x01, 0x48, 0x01, 0x52, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x12, 0x64, 0x0a, 0x11, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x36, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x2e, 0x4f, 0x49, 0x44, 0x43, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x66, 0x65, - 0x72, 0x65, 0x6e, 0x63, 0x65, 0x48, 0x01, 0x52, 0x0f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x65, 0x66, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x6f, 0x70, - 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, - 0x12, 0x2c, 0x0a, 0x12, 0x63, 0x6f, 0x6f, 0x6b, 0x69, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, - 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, - 0x6f, 0x6b, 0x69, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x4c, - 0x0a, 0x08, 0x69, 0x64, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x2e, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x8a, 0x01, - 0x02, 0x10, 0x01, 0x52, 0x07, 0x69, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x4a, 0x0a, 0x0c, - 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x2e, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x61, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x40, 0x0a, 0x06, 0x6c, 0x6f, 0x67, 0x6f, - 0x75, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, - 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x2e, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x06, 0x6c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x38, 0x0a, 0x18, 0x61, 0x62, - 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x16, 0x61, 0x62, - 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, - 0x65, 0x6f, 0x75, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x69, 0x64, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x0d, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x12, 0x69, 0x64, 0x6c, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, - 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x44, 0x0a, 0x1d, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, - 0x64, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, - 0x1b, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x4d, 0x0a, 0x22, - 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x66, 0x69, - 0x6c, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x1f, 0x74, 0x72, 0x75, 0x73, - 0x74, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x7d, 0x0a, 0x2e, 0x74, - 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x72, 0x65, 0x66, - 0x72, 0x65, 0x73, 0x68, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x16, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x2a, - 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x52, 0x65, 0x66, 0x72, 0x65, - 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x72, - 0x6f, 0x78, 0x79, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, - 0x72, 0x6f, 0x78, 0x79, 0x55, 0x72, 0x69, 0x12, 0x64, 0x0a, 0x1a, 0x72, 0x65, 0x64, 0x69, 0x73, - 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x2e, 0x76, 0x31, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x2e, 0x52, 0x65, 0x64, 0x69, 0x73, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x17, 0x72, 0x65, 0x64, 0x69, 0x73, 0x53, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x49, 0x0a, - 0x15, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x70, 0x65, 0x65, - 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x73, 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, - 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x1a, 0xbc, 0x01, 0x0a, 0x11, 0x4a, 0x77, 0x6b, - 0x73, 0x46, 0x65, 0x74, 0x63, 0x68, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, - 0x0a, 0x08, 0x6a, 0x77, 0x6b, 0x73, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x6a, 0x77, 0x6b, 0x73, 0x55, 0x72, 0x69, 0x12, 0x3d, 0x0a, 0x1b, 0x70, 0x65, 0x72, - 0x69, 0x6f, 0x64, 0x69, 0x63, 0x5f, 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x76, 0x61, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x18, - 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x69, 0x63, 0x46, 0x65, 0x74, 0x63, 0x68, 0x49, 0x6e, 0x74, - 0x65, 0x72, 0x76, 0x61, 0x6c, 0x53, 0x65, 0x63, 0x12, 0x4d, 0x0a, 0x15, 0x73, 0x6b, 0x69, 0x70, - 0x5f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, - 0x02, 0x18, 0x01, 0x52, 0x12, 0x73, 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, - 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x1a, 0x4c, 0x0a, 0x0f, 0x53, 0x65, 0x63, 0x72, 0x65, - 0x74, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1b, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x6a, 0x77, 0x6b, 0x73, 0x5f, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x42, 0x1b, 0x0a, 0x14, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x03, 0xf8, 0x42, - 0x01, 0x42, 0x13, 0x0a, 0x11, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x61, 0x5f, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0xf4, 0x01, 0x0a, 0x1e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x42, 0x0b, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x39, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x65, 0x74, 0x72, 0x61, 0x74, 0x65, 0x69, 0x6f, 0x2f, 0x61, - 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2d, 0x67, 0x6f, 0x2f, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x2f, 0x6f, - 0x69, 0x64, 0x63, 0xa2, 0x02, 0x04, 0x41, 0x43, 0x56, 0x4f, 0xaa, 0x02, 0x1a, 0x41, 0x75, 0x74, - 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, - 0x56, 0x31, 0x2e, 0x4f, 0x69, 0x64, 0x63, 0xca, 0x02, 0x1a, 0x41, 0x75, 0x74, 0x68, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5c, 0x56, 0x31, 0x5c, - 0x4f, 0x69, 0x64, 0x63, 0xe2, 0x02, 0x26, 0x41, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5c, 0x56, 0x31, 0x5c, 0x4f, 0x69, 0x64, - 0x63, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x1d, - 0x41, 0x75, 0x74, 0x68, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x3a, 0x3a, 0x56, 0x31, 0x3a, 0x3a, 0x4f, 0x69, 0x64, 0x63, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_v1_oidc_config_proto_rawDescOnce sync.Once - file_v1_oidc_config_proto_rawDescData = file_v1_oidc_config_proto_rawDesc -) - -func file_v1_oidc_config_proto_rawDescGZIP() []byte { - file_v1_oidc_config_proto_rawDescOnce.Do(func() { - file_v1_oidc_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_oidc_config_proto_rawDescData) - }) - return file_v1_oidc_config_proto_rawDescData -} - -var file_v1_oidc_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6) -var file_v1_oidc_config_proto_goTypes = []interface{}{ - (*TokenConfig)(nil), // 0: authservice.config.v1.oidc.TokenConfig - (*RedisConfig)(nil), // 1: authservice.config.v1.oidc.RedisConfig - (*LogoutConfig)(nil), // 2: authservice.config.v1.oidc.LogoutConfig - (*OIDCConfig)(nil), // 3: authservice.config.v1.oidc.OIDCConfig - (*OIDCConfig_JwksFetcherConfig)(nil), // 4: authservice.config.v1.oidc.OIDCConfig.JwksFetcherConfig - (*OIDCConfig_SecretReference)(nil), // 5: authservice.config.v1.oidc.OIDCConfig.SecretReference - (*durationpb.Duration)(nil), // 6: google.protobuf.Duration - (*structpb.Value)(nil), // 7: google.protobuf.Value -} -var file_v1_oidc_config_proto_depIdxs = []int32{ - 4, // 0: authservice.config.v1.oidc.OIDCConfig.jwks_fetcher:type_name -> authservice.config.v1.oidc.OIDCConfig.JwksFetcherConfig - 5, // 1: authservice.config.v1.oidc.OIDCConfig.client_secret_ref:type_name -> authservice.config.v1.oidc.OIDCConfig.SecretReference - 0, // 2: authservice.config.v1.oidc.OIDCConfig.id_token:type_name -> authservice.config.v1.oidc.TokenConfig - 0, // 3: authservice.config.v1.oidc.OIDCConfig.access_token:type_name -> authservice.config.v1.oidc.TokenConfig - 2, // 4: authservice.config.v1.oidc.OIDCConfig.logout:type_name -> authservice.config.v1.oidc.LogoutConfig - 6, // 5: authservice.config.v1.oidc.OIDCConfig.trusted_certificate_authority_refresh_interval:type_name -> google.protobuf.Duration - 1, // 6: authservice.config.v1.oidc.OIDCConfig.redis_session_store_config:type_name -> authservice.config.v1.oidc.RedisConfig - 7, // 7: authservice.config.v1.oidc.OIDCConfig.skip_verify_peer_cert:type_name -> google.protobuf.Value - 7, // 8: authservice.config.v1.oidc.OIDCConfig.JwksFetcherConfig.skip_verify_peer_cert:type_name -> google.protobuf.Value - 9, // [9:9] is the sub-list for method output_type - 9, // [9:9] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name -} - -func init() { file_v1_oidc_config_proto_init() } -func file_v1_oidc_config_proto_init() { - if File_v1_oidc_config_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_v1_oidc_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*TokenConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_oidc_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RedisConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_oidc_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LogoutConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_oidc_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*OIDCConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_oidc_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*OIDCConfig_JwksFetcherConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_v1_oidc_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*OIDCConfig_SecretReference); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_v1_oidc_config_proto_msgTypes[3].OneofWrappers = []interface{}{ - (*OIDCConfig_Jwks)(nil), - (*OIDCConfig_JwksFetcher)(nil), - (*OIDCConfig_ClientSecret)(nil), - (*OIDCConfig_ClientSecretRef)(nil), - (*OIDCConfig_TrustedCertificateAuthority)(nil), - (*OIDCConfig_TrustedCertificateAuthorityFile)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_v1_oidc_config_proto_rawDesc, - NumEnums: 0, - NumMessages: 6, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_v1_oidc_config_proto_goTypes, - DependencyIndexes: file_v1_oidc_config_proto_depIdxs, - MessageInfos: file_v1_oidc_config_proto_msgTypes, - }.Build() - File_v1_oidc_config_proto = out.File - file_v1_oidc_config_proto_rawDesc = nil - file_v1_oidc_config_proto_goTypes = nil - file_v1_oidc_config_proto_depIdxs = nil -} diff --git a/config/gen/go/v1/oidc/config.pb.validate.go b/config/gen/go/v1/oidc/config.pb.validate.go deleted file mode 100644 index ae4c4cb..0000000 --- a/config/gen/go/v1/oidc/config.pb.validate.go +++ /dev/null @@ -1,1117 +0,0 @@ -// Code generated by protoc-gen-validate. DO NOT EDIT. -// source: v1/oidc/config.proto - -package oidc - -import ( - "bytes" - "errors" - "fmt" - "net" - "net/mail" - "net/url" - "regexp" - "sort" - "strings" - "time" - "unicode/utf8" - - "google.golang.org/protobuf/types/known/anypb" -) - -// ensure the imports are used -var ( - _ = bytes.MinRead - _ = errors.New("") - _ = fmt.Print - _ = utf8.UTFMax - _ = (*regexp.Regexp)(nil) - _ = (*strings.Reader)(nil) - _ = net.IPv4len - _ = time.Duration(0) - _ = (*url.URL)(nil) - _ = (*mail.Address)(nil) - _ = anypb.Any{} - _ = sort.Sort -) - -// Validate checks the field values on TokenConfig with the rules defined in -// the proto definition for this message. If any rules are violated, the first -// error encountered is returned, or nil if there are no violations. -func (m *TokenConfig) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on TokenConfig with the rules defined in -// the proto definition for this message. If any rules are violated, the -// result is a list of violation errors wrapped in TokenConfigMultiError, or -// nil if none found. -func (m *TokenConfig) ValidateAll() error { - return m.validate(true) -} - -func (m *TokenConfig) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - if utf8.RuneCountInString(m.GetHeader()) < 1 { - err := TokenConfigValidationError{ - field: "Header", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - // no validation rules for Preamble - - if len(errors) > 0 { - return TokenConfigMultiError(errors) - } - - return nil -} - -// TokenConfigMultiError is an error wrapping multiple validation errors -// returned by TokenConfig.ValidateAll() if the designated constraints aren't met. -type TokenConfigMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m TokenConfigMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m TokenConfigMultiError) AllErrors() []error { return m } - -// TokenConfigValidationError is the validation error returned by -// TokenConfig.Validate if the designated constraints aren't met. -type TokenConfigValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e TokenConfigValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e TokenConfigValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e TokenConfigValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e TokenConfigValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e TokenConfigValidationError) ErrorName() string { return "TokenConfigValidationError" } - -// Error satisfies the builtin error interface -func (e TokenConfigValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sTokenConfig.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = TokenConfigValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = TokenConfigValidationError{} - -// Validate checks the field values on RedisConfig with the rules defined in -// the proto definition for this message. If any rules are violated, the first -// error encountered is returned, or nil if there are no violations. -func (m *RedisConfig) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on RedisConfig with the rules defined in -// the proto definition for this message. If any rules are violated, the -// result is a list of violation errors wrapped in RedisConfigMultiError, or -// nil if none found. -func (m *RedisConfig) ValidateAll() error { - return m.validate(true) -} - -func (m *RedisConfig) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - if utf8.RuneCountInString(m.GetServerUri()) < 1 { - err := RedisConfigValidationError{ - field: "ServerUri", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - if len(errors) > 0 { - return RedisConfigMultiError(errors) - } - - return nil -} - -// RedisConfigMultiError is an error wrapping multiple validation errors -// returned by RedisConfig.ValidateAll() if the designated constraints aren't met. -type RedisConfigMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m RedisConfigMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m RedisConfigMultiError) AllErrors() []error { return m } - -// RedisConfigValidationError is the validation error returned by -// RedisConfig.Validate if the designated constraints aren't met. -type RedisConfigValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e RedisConfigValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e RedisConfigValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e RedisConfigValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e RedisConfigValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e RedisConfigValidationError) ErrorName() string { return "RedisConfigValidationError" } - -// Error satisfies the builtin error interface -func (e RedisConfigValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sRedisConfig.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = RedisConfigValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = RedisConfigValidationError{} - -// Validate checks the field values on LogoutConfig with the rules defined in -// the proto definition for this message. If any rules are violated, the first -// error encountered is returned, or nil if there are no violations. -func (m *LogoutConfig) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on LogoutConfig with the rules defined -// in the proto definition for this message. If any rules are violated, the -// result is a list of violation errors wrapped in LogoutConfigMultiError, or -// nil if none found. -func (m *LogoutConfig) ValidateAll() error { - return m.validate(true) -} - -func (m *LogoutConfig) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - if utf8.RuneCountInString(m.GetPath()) < 1 { - err := LogoutConfigValidationError{ - field: "Path", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - if utf8.RuneCountInString(m.GetRedirectUri()) < 1 { - err := LogoutConfigValidationError{ - field: "RedirectUri", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - if len(errors) > 0 { - return LogoutConfigMultiError(errors) - } - - return nil -} - -// LogoutConfigMultiError is an error wrapping multiple validation errors -// returned by LogoutConfig.ValidateAll() if the designated constraints aren't met. -type LogoutConfigMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m LogoutConfigMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m LogoutConfigMultiError) AllErrors() []error { return m } - -// LogoutConfigValidationError is the validation error returned by -// LogoutConfig.Validate if the designated constraints aren't met. -type LogoutConfigValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e LogoutConfigValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e LogoutConfigValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e LogoutConfigValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e LogoutConfigValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e LogoutConfigValidationError) ErrorName() string { return "LogoutConfigValidationError" } - -// Error satisfies the builtin error interface -func (e LogoutConfigValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sLogoutConfig.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = LogoutConfigValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = LogoutConfigValidationError{} - -// Validate checks the field values on OIDCConfig with the rules defined in the -// proto definition for this message. If any rules are violated, the first -// error encountered is returned, or nil if there are no violations. -func (m *OIDCConfig) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on OIDCConfig with the rules defined in -// the proto definition for this message. If any rules are violated, the -// result is a list of violation errors wrapped in OIDCConfigMultiError, or -// nil if none found. -func (m *OIDCConfig) ValidateAll() error { - return m.validate(true) -} - -func (m *OIDCConfig) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - // no validation rules for ConfigurationUri - - // no validation rules for AuthorizationUri - - // no validation rules for TokenUri - - if utf8.RuneCountInString(m.GetCallbackUri()) < 1 { - err := OIDCConfigValidationError{ - field: "CallbackUri", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - if utf8.RuneCountInString(m.GetClientId()) < 1 { - err := OIDCConfigValidationError{ - field: "ClientId", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - // no validation rules for CookieNamePrefix - - if m.GetIdToken() == nil { - err := OIDCConfigValidationError{ - field: "IdToken", - reason: "value is required", - } - if !all { - return err - } - errors = append(errors, err) - } - - if all { - switch v := interface{}(m.GetIdToken()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "IdToken", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "IdToken", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetIdToken()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return OIDCConfigValidationError{ - field: "IdToken", - reason: "embedded message failed validation", - cause: err, - } - } - } - - if all { - switch v := interface{}(m.GetAccessToken()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "AccessToken", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "AccessToken", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetAccessToken()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return OIDCConfigValidationError{ - field: "AccessToken", - reason: "embedded message failed validation", - cause: err, - } - } - } - - if all { - switch v := interface{}(m.GetLogout()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "Logout", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "Logout", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetLogout()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return OIDCConfigValidationError{ - field: "Logout", - reason: "embedded message failed validation", - cause: err, - } - } - } - - // no validation rules for AbsoluteSessionTimeout - - // no validation rules for IdleSessionTimeout - - if all { - switch v := interface{}(m.GetTrustedCertificateAuthorityRefreshInterval()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "TrustedCertificateAuthorityRefreshInterval", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "TrustedCertificateAuthorityRefreshInterval", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetTrustedCertificateAuthorityRefreshInterval()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return OIDCConfigValidationError{ - field: "TrustedCertificateAuthorityRefreshInterval", - reason: "embedded message failed validation", - cause: err, - } - } - } - - // no validation rules for ProxyUri - - if all { - switch v := interface{}(m.GetRedisSessionStoreConfig()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "RedisSessionStoreConfig", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "RedisSessionStoreConfig", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetRedisSessionStoreConfig()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return OIDCConfigValidationError{ - field: "RedisSessionStoreConfig", - reason: "embedded message failed validation", - cause: err, - } - } - } - - if all { - switch v := interface{}(m.GetSkipVerifyPeerCert()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "SkipVerifyPeerCert", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "SkipVerifyPeerCert", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetSkipVerifyPeerCert()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return OIDCConfigValidationError{ - field: "SkipVerifyPeerCert", - reason: "embedded message failed validation", - cause: err, - } - } - } - - switch v := m.JwksConfig.(type) { - case *OIDCConfig_Jwks: - if v == nil { - err := OIDCConfigValidationError{ - field: "JwksConfig", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - // no validation rules for Jwks - case *OIDCConfig_JwksFetcher: - if v == nil { - err := OIDCConfigValidationError{ - field: "JwksConfig", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - - if all { - switch v := interface{}(m.GetJwksFetcher()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "JwksFetcher", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "JwksFetcher", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetJwksFetcher()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return OIDCConfigValidationError{ - field: "JwksFetcher", - reason: "embedded message failed validation", - cause: err, - } - } - } - - default: - _ = v // ensures v is used - } - oneofClientSecretConfigPresent := false - switch v := m.ClientSecretConfig.(type) { - case *OIDCConfig_ClientSecret: - if v == nil { - err := OIDCConfigValidationError{ - field: "ClientSecretConfig", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - oneofClientSecretConfigPresent = true - - if utf8.RuneCountInString(m.GetClientSecret()) < 1 { - err := OIDCConfigValidationError{ - field: "ClientSecret", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - case *OIDCConfig_ClientSecretRef: - if v == nil { - err := OIDCConfigValidationError{ - field: "ClientSecretConfig", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - oneofClientSecretConfigPresent = true - - if all { - switch v := interface{}(m.GetClientSecretRef()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "ClientSecretRef", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, OIDCConfigValidationError{ - field: "ClientSecretRef", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetClientSecretRef()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return OIDCConfigValidationError{ - field: "ClientSecretRef", - reason: "embedded message failed validation", - cause: err, - } - } - } - - default: - _ = v // ensures v is used - } - if !oneofClientSecretConfigPresent { - err := OIDCConfigValidationError{ - field: "ClientSecretConfig", - reason: "value is required", - } - if !all { - return err - } - errors = append(errors, err) - } - switch v := m.TrustedCaConfig.(type) { - case *OIDCConfig_TrustedCertificateAuthority: - if v == nil { - err := OIDCConfigValidationError{ - field: "TrustedCaConfig", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - // no validation rules for TrustedCertificateAuthority - case *OIDCConfig_TrustedCertificateAuthorityFile: - if v == nil { - err := OIDCConfigValidationError{ - field: "TrustedCaConfig", - reason: "oneof value cannot be a typed-nil", - } - if !all { - return err - } - errors = append(errors, err) - } - // no validation rules for TrustedCertificateAuthorityFile - default: - _ = v // ensures v is used - } - - if len(errors) > 0 { - return OIDCConfigMultiError(errors) - } - - return nil -} - -// OIDCConfigMultiError is an error wrapping multiple validation errors -// returned by OIDCConfig.ValidateAll() if the designated constraints aren't met. -type OIDCConfigMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m OIDCConfigMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m OIDCConfigMultiError) AllErrors() []error { return m } - -// OIDCConfigValidationError is the validation error returned by -// OIDCConfig.Validate if the designated constraints aren't met. -type OIDCConfigValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e OIDCConfigValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e OIDCConfigValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e OIDCConfigValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e OIDCConfigValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e OIDCConfigValidationError) ErrorName() string { return "OIDCConfigValidationError" } - -// Error satisfies the builtin error interface -func (e OIDCConfigValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sOIDCConfig.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = OIDCConfigValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = OIDCConfigValidationError{} - -// Validate checks the field values on OIDCConfig_JwksFetcherConfig with the -// rules defined in the proto definition for this message. If any rules are -// violated, the first error encountered is returned, or nil if there are no violations. -func (m *OIDCConfig_JwksFetcherConfig) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on OIDCConfig_JwksFetcherConfig with the -// rules defined in the proto definition for this message. If any rules are -// violated, the result is a list of violation errors wrapped in -// OIDCConfig_JwksFetcherConfigMultiError, or nil if none found. -func (m *OIDCConfig_JwksFetcherConfig) ValidateAll() error { - return m.validate(true) -} - -func (m *OIDCConfig_JwksFetcherConfig) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - // no validation rules for JwksUri - - // no validation rules for PeriodicFetchIntervalSec - - if all { - switch v := interface{}(m.GetSkipVerifyPeerCert()).(type) { - case interface{ ValidateAll() error }: - if err := v.ValidateAll(); err != nil { - errors = append(errors, OIDCConfig_JwksFetcherConfigValidationError{ - field: "SkipVerifyPeerCert", - reason: "embedded message failed validation", - cause: err, - }) - } - case interface{ Validate() error }: - if err := v.Validate(); err != nil { - errors = append(errors, OIDCConfig_JwksFetcherConfigValidationError{ - field: "SkipVerifyPeerCert", - reason: "embedded message failed validation", - cause: err, - }) - } - } - } else if v, ok := interface{}(m.GetSkipVerifyPeerCert()).(interface{ Validate() error }); ok { - if err := v.Validate(); err != nil { - return OIDCConfig_JwksFetcherConfigValidationError{ - field: "SkipVerifyPeerCert", - reason: "embedded message failed validation", - cause: err, - } - } - } - - if len(errors) > 0 { - return OIDCConfig_JwksFetcherConfigMultiError(errors) - } - - return nil -} - -// OIDCConfig_JwksFetcherConfigMultiError is an error wrapping multiple -// validation errors returned by OIDCConfig_JwksFetcherConfig.ValidateAll() if -// the designated constraints aren't met. -type OIDCConfig_JwksFetcherConfigMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m OIDCConfig_JwksFetcherConfigMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m OIDCConfig_JwksFetcherConfigMultiError) AllErrors() []error { return m } - -// OIDCConfig_JwksFetcherConfigValidationError is the validation error returned -// by OIDCConfig_JwksFetcherConfig.Validate if the designated constraints -// aren't met. -type OIDCConfig_JwksFetcherConfigValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e OIDCConfig_JwksFetcherConfigValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e OIDCConfig_JwksFetcherConfigValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e OIDCConfig_JwksFetcherConfigValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e OIDCConfig_JwksFetcherConfigValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e OIDCConfig_JwksFetcherConfigValidationError) ErrorName() string { - return "OIDCConfig_JwksFetcherConfigValidationError" -} - -// Error satisfies the builtin error interface -func (e OIDCConfig_JwksFetcherConfigValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sOIDCConfig_JwksFetcherConfig.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = OIDCConfig_JwksFetcherConfigValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = OIDCConfig_JwksFetcherConfigValidationError{} - -// Validate checks the field values on OIDCConfig_SecretReference with the -// rules defined in the proto definition for this message. If any rules are -// violated, the first error encountered is returned, or nil if there are no violations. -func (m *OIDCConfig_SecretReference) Validate() error { - return m.validate(false) -} - -// ValidateAll checks the field values on OIDCConfig_SecretReference with the -// rules defined in the proto definition for this message. If any rules are -// violated, the result is a list of violation errors wrapped in -// OIDCConfig_SecretReferenceMultiError, or nil if none found. -func (m *OIDCConfig_SecretReference) ValidateAll() error { - return m.validate(true) -} - -func (m *OIDCConfig_SecretReference) validate(all bool) error { - if m == nil { - return nil - } - - var errors []error - - // no validation rules for Namespace - - if utf8.RuneCountInString(m.GetName()) < 1 { - err := OIDCConfig_SecretReferenceValidationError{ - field: "Name", - reason: "value length must be at least 1 runes", - } - if !all { - return err - } - errors = append(errors, err) - } - - if len(errors) > 0 { - return OIDCConfig_SecretReferenceMultiError(errors) - } - - return nil -} - -// OIDCConfig_SecretReferenceMultiError is an error wrapping multiple -// validation errors returned by OIDCConfig_SecretReference.ValidateAll() if -// the designated constraints aren't met. -type OIDCConfig_SecretReferenceMultiError []error - -// Error returns a concatenation of all the error messages it wraps. -func (m OIDCConfig_SecretReferenceMultiError) Error() string { - var msgs []string - for _, err := range m { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "; ") -} - -// AllErrors returns a list of validation violation errors. -func (m OIDCConfig_SecretReferenceMultiError) AllErrors() []error { return m } - -// OIDCConfig_SecretReferenceValidationError is the validation error returned -// by OIDCConfig_SecretReference.Validate if the designated constraints aren't met. -type OIDCConfig_SecretReferenceValidationError struct { - field string - reason string - cause error - key bool -} - -// Field function returns field value. -func (e OIDCConfig_SecretReferenceValidationError) Field() string { return e.field } - -// Reason function returns reason value. -func (e OIDCConfig_SecretReferenceValidationError) Reason() string { return e.reason } - -// Cause function returns cause value. -func (e OIDCConfig_SecretReferenceValidationError) Cause() error { return e.cause } - -// Key function returns key value. -func (e OIDCConfig_SecretReferenceValidationError) Key() bool { return e.key } - -// ErrorName returns error name. -func (e OIDCConfig_SecretReferenceValidationError) ErrorName() string { - return "OIDCConfig_SecretReferenceValidationError" -} - -// Error satisfies the builtin error interface -func (e OIDCConfig_SecretReferenceValidationError) Error() string { - cause := "" - if e.cause != nil { - cause = fmt.Sprintf(" | caused by: %v", e.cause) - } - - key := "" - if e.key { - key = "key for " - } - - return fmt.Sprintf( - "invalid %sOIDCConfig_SecretReference.%s: %s%s", - key, - e.field, - e.reason, - cause) -} - -var _ error = OIDCConfig_SecretReferenceValidationError{} - -var _ interface { - Field() string - Reason() string - Key() bool - Cause() error - ErrorName() string -} = OIDCConfig_SecretReferenceValidationError{} diff --git a/config/v1/config.proto b/config/v1/config.proto deleted file mode 100644 index 1fda6c8..0000000 --- a/config/v1/config.proto +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package authservice.config.v1; - -import "validate/validate.proto"; - -import "v1/oidc/config.proto"; -import "v1/mock/config.proto"; - -// Specifies how a request can be matched to a filter chain. -message Match { - - // The name of the http header used to match against. - // Required. - string header = 1 [(validate.rules).string.min_len = 1]; - - // The criteria by which to match. - // Must be one of `prefix` or `equality`. - // Required. - oneof criteria { - option (validate.required) = true; - - // The expected prefix. If the actual value of the header starts with this prefix, - // then it will be considered a match. - string prefix = 2 [(validate.rules).string.min_len = 1]; - - // The expected value. If the actual value of the header exactly equals this value, - // then it will be considered a match. - string equality = 3 [(validate.rules).string.min_len = 1]; - } -} - -// A filter configuration. -message Filter { - - // The type of filter. Currently, the only valid types are `oidc` - // and `mock`. Required. - oneof type { - option (validate.required) = true; - - // An OpenID Connect filter configuration. - oidc.OIDCConfig oidc = 1; - - // This value will be used when `default_oidc_config` exists. - // It will override values of them. If that doesn't exist, - // this configuration will be rejected. - oidc.OIDCConfig oidc_override = 2; - - // Mock filter configuration for testing and letting - // AuthService run even if no OIDC providers are configured. - mock.MockConfig mock = 3; - } -} - -// A chain of one or more filters that will sequentially process an HTTP request. -message FilterChain { - - // A user-defined identifier for the processing chain used in log messages. - // Required. - string name = 1 [(validate.rules).string.min_len = 1]; - - // A rule to determine whether an HTTP request should be processed by the filter chain. - // If not defined, the filter chain will match every request. - // Optional. - Match match = 2; - - // The configuration of one of more filters in the filter chain. When the filter chain - // matches an incoming request, then this list of filters will be applied to the request - // in the order that they are declared. - // All filters are evaluated until one of them returns a non-OK response. - // If all filters return OK, the envoy proxy is notified that the request may continue. - // The first filter that returns a non-OK response causes the request to be rejected with - // the filter's returned status and any remaining filters are skipped. - // At least one `Filter` is required in this array. - repeated Filter filters = 3 [(validate.rules).repeated.min_items = 1]; -} - -// The top-level configuration object. -// For a simple example, see the [sample JSON in the bookinfo configmap template](https://github.com/istio-ecosystem/authservice/blob/master/bookinfo-example/config/authservice-configmap-template-for-authn-and-authz.yaml). -message Config { - - // Each incoming http request is matched against the list of filters in the chain, in order, - // until a matching filter is found. The first matching filter is then applied to the request. - // After the first match is made, other filters in the chain are ignored. - // Order of chain declaration is therefore important. - // At least one `FilterChain` is required in this array. - repeated FilterChain chains = 1 [(validate.rules).repeated.min_items = 1]; - - // The IP address for the Authservice to listen for incoming requests to process. - // Required. - string listen_address = 2 [(validate.rules).string.ip = true]; - - // The TCP port for the Authservice to listen for incoming requests to process. - // Required. - int32 listen_port = 3 [(validate.rules).int32.lt = 65536]; - - // The verbosity of logs generated by the Authservice. - // Must be one of `trace`, `debug`, `info', 'error' or 'critical'. - // Required. - string log_level = 4 [(validate.rules).string = {in: ["trace", "debug", "info", "error", "critical"]}]; - - // The number of threads in the thread pool to use for processing. - // The main thread will be used for accepting connections, before sending them to the thread-pool - // for processing. The total number of running threads, including the main thread, will be N+1. - // Required. - uint32 threads = 5 [(validate.rules).uint32.gte = 1]; - - // List of trigger rules to decide if the Authservice should be used to authenticate the - // request. The Authservice authentication happens if any one of the rules matched. - // If the list is not empty and none of the rules matched, the request will be allowed - // to proceed without Authservice authentication. - // The format and semantics of `trigger_rules` are the same as the `triggerRules` setting - // on the Istio Authentication Policy - // (see https://istio.io/docs/reference/config/security/istio.authentication.v1alpha1). - // CAUTION: Be sure that your configured `OIDCConfig.callback` and `OIDCConfig.logout` paths - // each satisfies at least one of the trigger rules, or else the Authservice will not be able to - // intercept requests made to those paths to perform the appropriate login/logout behavior. - // Optional. Leave this empty to always trigger authentication for all paths. - repeated TriggerRule trigger_rules = 9; - - // Global configuration of OIDC. This value will be applied to all filter definition - // when it defined as `oidc_override`. - // Optional. - oidc.OIDCConfig default_oidc_config = 10; - - // If true will allow the the requests even no filter chain match is found. Default false. - // Optional. - bool allow_unmatched_requests = 11; - - // The Authservice provides an HTTP server to check the health state. - // This configures the address for the health server to listen for. - // Optional. Defaults to the value of `listen_address`. - string health_listen_address = 12; - - // The TCP port for the health server to listen for. - // Optional. Defaults 10004. - int32 health_listen_port = 13 [(validate.rules).int32.lt = 65536]; - - // The path for the health server to attend. - // Optional. Defaults to "/healthz". - string health_listen_path = 14; -} - -// Trigger rule to match against a request. The trigger rule is satisfied if -// and only if both rules, excluded_paths and include_paths are satisfied. -message TriggerRule { - // List of paths to be excluded from the request. The rule is satisfied if - // request path does not match to any of the path in this list. - // Optional. - repeated StringMatch excluded_paths = 1; - - // List of paths that the request must include. If the list is not empty, the - // rule is satisfied if request path matches at least one of the path in the list. - // If the list is empty, the rule is ignored, in other words the rule is always satisfied. - // Optional. - repeated StringMatch included_paths = 2; -} - -// Describes how to match a given string. Match is case-sensitive. -message StringMatch { - oneof match_type { - // exact string match. - string exact = 1; - - // prefix-based match. - string prefix = 2; - - // suffix-based match. - string suffix = 3; - - // ECMAscript style regex-based match as defined by [EDCA-262](http://en.cppreference.com/w/cpp/regex/ecmascript). - // Example: "^/pets/(.*?)?" - string regex = 4; - } -} diff --git a/config/v1/mock/config.proto b/config/v1/mock/config.proto deleted file mode 100644 index e53b61c..0000000 --- a/config/v1/mock/config.proto +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package authservice.config.v1.mock; - -// Mock filter config. The only thing which can be defined is whether it -// allows or rejects any request it matches. -message MockConfig { - // Boolean specifying whether the filter should return OK for any - // request it matches. Defaults to false (not OK). - bool allow = 1; -} diff --git a/config/v1/oidc/config.proto b/config/v1/oidc/config.proto deleted file mode 100644 index 2c68245..0000000 --- a/config/v1/oidc/config.proto +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package authservice.config.v1.oidc; - -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "validate/validate.proto"; - -// Defines how a token obtained through an OIDC flow is forwarded to services. -message TokenConfig { - - // The name of the header that Authservice adds to the request when forwarding to services. - // The value of this header will contain the `preamble` and the token. - // This value is case-insensitive, as http header names are case-insensitive. - // Note that this value must be `Authorization` for the - // [Istio Authentication Policy](https://istio.io/docs/tasks/security/authn-policy/) - // to inspect the token. - // Required. - string header = 1 [(validate.rules).string.min_len = 1]; - - // The authentication scheme of the token. - // For example, when the preamble is `Bearer` and `header` is `Authorization`, the following - // header will be added to the request to the service: `Authorization: Bearer ID_TOKEN_VALUE`. - // Note that this value must be `Bearer`, case-sensitive, when header is `Authorization`. - // Optional. - string preamble = 2; -} - -// When specified, the Authservice will use the configured Redis server to store session data -message RedisConfig { - - // The Redis server uri, e.g. "tcp://127.0.0.1:6379" - string server_uri = 1 [(validate.rules).string.min_len = 1]; -} - -// When specified, the Authservice will destroy the Authservice session when a request is -// made to the configured path. -message LogoutConfig { - - // A http request path that the Authservice matches against to initiate logout. - // Whenever a request is made to that path, the Authservice will remove the Authservice-specific - // cookies and respond with a redirect to the configured `redirect_uri`. Removing the cookies - // causes the user to be unauthenticated in future requests. - // If the service application has its own logout controller, then it may be desirable to have its - // logout controller redirect to this path. If the service application does not need its own logout - // controller, then the application's logout button/link's href can GET or POST directly to this path. - // Required. - string path = 1 [(validate.rules).string.min_len = 1]; - - // A URI specifying the destination to which the Authservice will redirect any request made to the - // logout `path`. For example, it may be desirable to redirect the logged out user to the homepage - // of the service application, or to the - // [logout endpoint of the OIDC Provider](https://openid.net/specs/openid-connect-session-1_0.html#RPLogout). - // As with all redirects, the user's browser will perform a GET to this URI. - // Required. - string redirect_uri = 2 [(validate.rules).string.min_len = 1]; -} - -// The configuration of an OpenID Connect filter that can be used to retrieve identity and access tokens -// via the standard authorization code grant flow from an OIDC Provider. -message OIDCConfig { - // The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig). - // If this is set, the endpoints will be dynamically retrieved from the OIDC Provider's configuration endpoint. - string configuration_uri = 19; - - // The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). - // Required if `configuration_uri` is not set. - string authorization_uri = 1; - - // The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). - // Required if `configuration_uri` is not set. - string token_uri = 2; - - // This value will be used as the `redirect_uri` param of the authorization code grant - // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - // This URL must be one of the Redirection URI values for the Client pre-registered at the OIDC provider. - // Note: The Istio gateway's VirtualService must be prepared to ensure that this URL will get routed to - // the service so that the Authservice can intercept the request and handle it - // (see [example](https://github.com/istio-ecosystem/authservice/blob/master/bookinfo-example/config/bookinfo-gateway.yaml)). - // Required. - string callback_uri = 3 [(validate.rules).string.min_len = 1]; - - // This message defines a setting to allow asynchronous retrieval and update of the JWK for - // JWT validation at regular intervals. - message JwksFetcherConfig { - // Request URI that has the JWKs. - // Required if `configuration_uri` is not set. - string jwks_uri = 1; - - // Request interval to check whether new JWKs are available. If not specified, - // default to 1200 seconds, 20min. - // Optional. - uint32 periodic_fetch_interval_sec = 2; - - // If set to true, the verification of the destination certificate will be skipped when - // making a request to the JWKs URI. This option is useful when you want to use a - // self-signed certificate for testing purposes, but basically should not be set to - // true in any other cases. - // Optional. - // Deprecated: Use the one from the OIDCConfig instead. - google.protobuf.Value skip_verify_peer_cert = 3 [deprecated = true]; - } - - oneof jwks_config { - // The JSON JWKS response from the OIDC provider’s `jwks_uri` URI which can be found in - // the OIDC provider's - // [configuration response](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). - // Note that this JSON value must be escaped when embedded in a json configmap - // (see [example](https://github.com/istio-ecosystem/authservice/blob/master/bookinfo-example/config/authservice-configmap-template.yaml)). - // Used during token verification. - string jwks = 4; - - // Configuration to allow JWKs to be retrieved and updated asynchronously at regular intervals. - JwksFetcherConfig jwks_fetcher = 17; - } - - // The OIDC client ID assigned to the filter to be used in the - // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - // Required. - string client_id = 5 [(validate.rules).string.min_len = 1]; - - // This message defines a reference to a Kubernetes Secret resource. - message SecretReference { - // The namespace of the referenced Secret, if not set, default to "default" namespace. - string namespace = 1; - - // The name of the referenced Secret. - string name = 2 [(validate.rules).string.min_len = 1]; - } - - oneof client_secret_config { - option(validate.required) = true; - // The OIDC client secret assigned to the filter to be used in the - // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - // This field keeps the client secret in plain text. Recommend to use `client_secret_ref` instead - // when running in a Kubernetes cluster. - string client_secret = 6 [(validate.rules).string.min_len = 1]; - - // The Kubernetes secret that contains the OIDC client secret assigned to the filter to be used in the - // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - // - // This is an Opaque secret. The client secret should be stored in the key "client-secret". - // This filed is only valid when running in a Kubernetes cluster. - SecretReference client_secret_ref = 21; - } - - // Additional scopes passed to the OIDC Provider in the - // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). - // The `openid` scope is always sent to the OIDC Provider, and does not need to be specified here. - // Required, but an empty array is allowed. - repeated string scopes = 7; - - // A unique identifier of the Authservice's browser cookies. Can be any string. - // Needed when multiple services in the same domain are each protected by - // their own Authservice, in which case each service's Authservice should have - // a unique value to avoid cookie name conflicts. Also needed when an Authservice - // is configured with multiple `oidc` filters (across multiple `chains`), each - // sharing a Redis server for their session storage, to avoid having those - // `oidc` filters read/write the same sessions in Redis. - // Optional. - string cookie_name_prefix = 8; - - // The configuration for adding ID Tokens as headers to requests forwarded to a service. - // Required. - TokenConfig id_token = 9 [(validate.rules).message.required = true]; - - // The configuration for adding Access Tokens as headers to requests forwarded to a service. - // Optional. - TokenConfig access_token = 10; - - // When specified, the Authservice will destroy the Authservice session when a request is - // made to the configured path. - // Optional. - LogoutConfig logout = 11; - - // The Authservice associates obtained OIDC tokens with a session ID in a session store. - // It also stores some temporary information during the login process into the session store, - // which will be removed when the user finishes the login. - // This configuration option sets the number of seconds since a user's session with the Authservice has started - // until that session should expire. - // When configured to `0`, which is the default value, the session will never timeout based on the time - // that it was started, but can still timeout due to being idle. - // When both `absolute_session_timeout` and `idle_session_timeout` are zero, then sessions will never - // expire. These settings do not affect how quickly the OIDC tokens contained inside the user's session expire. - // Optional. - uint32 absolute_session_timeout = 12; - - // The Authservice associates obtained OIDC tokens with a session ID in a session store. - // It also stores some temporary information during the login process into the session store, - // which will be removed when the user finishes the login. - // This configuration option sets the number of seconds since the most recent incoming request from that user - // until the user's session with the Authservice should expire. - // When configured to `0`, which is the default value, session expiration will not consider idle time, - // but can still consider timeout based on maximum absolute time since added. - // When both `absolute_session_timeout` and `idle_session_timeout` are zero, then sessions will never - // expire. These settings do not affect how quickly the OIDC tokens contained inside the user's session expire. - // Optional. - uint32 idle_session_timeout = 13; - - // When specified, the Authservice will trust the specified Certificate Authority when performing HTTPS calls to - // the OIDC Identity Provider. - oneof trusted_ca_config { - // String PEM-encoded certificate authority to trust when performing HTTPS calls to the OIDC Identity Provider. - // Optional. - string trusted_certificate_authority = 14; - - // The file path to the PEM-encoded certificate authority to trust when performing HTTPS calls to the OIDC Identity Provider. - // Optional. - string trusted_certificate_authority_file = 20; - } - - // The duration between refreshes of the trusted certificate authority if `trusted_certificate_authority_file` is set. - // Unset or 0 (the default) disables the refresh, useful is no rotation is expected. - // Is a String that ends in `s` to indicate seconds and is preceded by the number of seconds, e.g. `120s` (represents 2 minutes). - // Optional. - google.protobuf.Duration trusted_certificate_authority_refresh_interval = 22; - - // The Authservice makes two kinds of direct network connections directly to the OIDC Provider. - // Both are POST requests to the configured `token_uri` of the OIDC Provider. - // The first is to exchange the authorization code for tokens, and the other is to use the - // refresh token to obtain new tokens. Configure the `proxy_uri` when - // both of these requests should be made through a web proxy. The format of `proxy_uri` is - // `http://proxyserver.example.com:8080`, where `:` is optional. - // Userinfo (usernames and passwords) in the `proxy_uri` setting are not yet supported. - // The `proxy_uri` should always start with `http://`. - // The Authservice will upgrade the connection to the OIDC provider to HTTPS using - // an HTTP CONNECT request to the proxy server. The proxy server will see the hostname and port number - // of the OIDC provider in plain text in the CONNECT request, but all other communication will occur - // over an encrypted HTTPS connection negotiated directly between the Authservice and - // the OIDC provider. See also the related `trusted_certificate_authority` configuration option. - // Optional. - string proxy_uri = 15; - - // When specified, the Authservice will use the configured Redis server to store session data. - // Optional. - RedisConfig redis_session_store_config = 16; - - // If set to true, the verification of the destination certificate will be skipped when - // making a request to the Token Endpoint. This option is useful when you want to use a - // self-signed certificate for testing purposes, but basically should not be set to true - // in any other cases. - // Optional. - google.protobuf.Value skip_verify_peer_cert = 18; // keep this field out from the trusted_ca_config one of for backward compatibility. -} diff --git a/e2e/Makefile b/e2e/Makefile deleted file mode 100644 index 044789f..0000000 --- a/e2e/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -SUITES_DOCKER := mock redis keycloak -SUITES_K8S := istio -SUITES := $(SUITES_DOCKER) $(SUITES_K8S) legacy - -.PHONY: e2e -e2e: $(SUITES:%=e2e/%) ## Run all e2e tests - -.PHONY: e2e/docker -e2e/docker: $(SUITES_DOCKER:%=e2e/%) - -.PHONY: e2e/k8s -e2e/k8s: $(SUITES_K8S:%=e2e/%) - -e2e/%: - @$(MAKE) -C $(@F) $(@D) - -.PHONY: clean -clean: $(SUITES:%=clean/%) - -clean/%: - @$(MAKE) -C $(@F) $(@D) diff --git a/e2e/README.md b/e2e/README.md deleted file mode 100644 index f8869ac..0000000 --- a/e2e/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# e2e tests - -Each directory is an end-to-end test suite that is self-contained and can be run independently. -Refer to the [Developer Guide](../DEVELOPMENT.md) for more information on how to run these tests. diff --git a/e2e/docker.go b/e2e/docker.go deleted file mode 100644 index 6f63237..0000000 --- a/e2e/docker.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package e2e - -import ( - "fmt" - "os/exec" - "strings" - "time" -) - -var ( - // DockerServiceExited is a DockerServiceStatus that matches the state "Exited" - DockerServiceExited DockerServiceStatus = containsMatcher{"Exited"} - // DockerServiceContainerUp is a DockerServiceStatus that matches the state "Up" - DockerServiceContainerUp DockerServiceStatus = containsMatcher{"Up"} - // DockerServiceContainerUpAndHealthy is a DockerServiceStatus that matches the state "Up (healthy)" - DockerServiceContainerUpAndHealthy DockerServiceStatus = containsMatcher{"(healthy)"} -) - -type ( - // DockerCompose is a helper to interact with docker compose command - DockerCompose struct { - log func(...any) - } - - // DockerComposeOption is a functional option for DockerCompose initialization - DockerComposeOption func(compose *DockerCompose) -) - -// NewDockerCompose creates a new DockerCompose with the given options -func NewDockerCompose(opts ...DockerComposeOption) DockerCompose { - d := DockerCompose{} - d.log = NoopLogFunc // default - for _, opt := range opts { - opt(&d) - } - return d - -} - -// WithDockerComposeLogFunc sets the log function for the DockerCompose. The default is NoopLogFunc -func WithDockerComposeLogFunc(logFunc func(...any)) DockerComposeOption { - return func(compose *DockerCompose) { - compose.log = logFunc - } -} - -// NoopLogFunc is a log function that does nothing -func NoopLogFunc(...any) {} - -// StartDockerService starts a docker service or returns an error -func (d DockerCompose) StartDockerService(name string) error { - d.log("Starting docker service", name) - out, err := exec.Command("docker", "compose", "start", name).CombinedOutput() - if err != nil { - return fmt.Errorf("%w: %s", err, string(out)) - } - return nil -} - -// StopDockerService stops a docker service or returns an error -func (d DockerCompose) StopDockerService(name string) error { - d.log("Stopping docker service", name) - out, err := exec.Command("docker", "compose", "stop", name).CombinedOutput() - if err != nil { - return fmt.Errorf("%w: %s", err, string(out)) - } - return nil -} - -// WaitForDockerService waits for a docker service to match a status in the given timeout or returns an error -func (d DockerCompose) WaitForDockerService(name string, status DockerServiceStatus, timeout, tick time.Duration) error { - d.log("Waiting for docker service", name, "to match", status) - cmd := exec.Command("docker", "compose", "ps", "-a", "--format", "{{ .Status }}", name) - - to := time.NewTimer(timeout) - tk := time.NewTicker(tick) - defer tk.Stop() - defer to.Stop() - - for { - select { - case <-to.C: - return fmt.Errorf("timeout waiting for service %s to match: %s", name, status) - case <-tk.C: - out, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("%w: %s", err, string(out)) - } - if status.Match(string(out)) { - d.log("Service", name, "matched", status) - return nil - } - } - } -} - -type ( - // DockerServiceStatus is an interface that matches the status of a docker service - DockerServiceStatus interface { - // Match returns true if the status matches the given docker service status - Match(string) bool - // String returns a string representation of the status - String() string - } - - // containsMatcher is a DockerServiceStatus that matches the status if it contains a string - containsMatcher struct { - contains string - } -) - -// Match implements DockerServiceStatus -func (c containsMatcher) Match(out string) bool { - return strings.Contains(out, c.contains) -} - -// String implements DockerServiceStatus -func (c containsMatcher) String() string { - return c.contains -} diff --git a/e2e/istio/Makefile b/e2e/istio/Makefile deleted file mode 100644 index bf46dae..0000000 --- a/e2e/istio/Makefile +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -include ../suite-certs.mk -include ../suite-k8s.mk - -.PHONY: gen-certs -gen-certs: clean-certs ca/ca.authservice.internal certificate/http-echo.authservice.internal - @chmod -R a+r $(CERTS_DIR) - -.PHONY: clean -clean:: clean-certs - -.PHONY: e2e-pre -e2e-pre:: gen-certs - @kubectl --kubeconfig $(E2E_KUBECONFIG) create namespace istio-system - @kubectl --kubeconfig $(E2E_KUBECONFIG) -n istio-system create secret tls http-echo-certs \ - --cert=certs/http-echo.authservice.internal.crt \ - --key=certs/http-echo.authservice.internal.key diff --git a/e2e/istio/README.md b/e2e/istio/README.md deleted file mode 100644 index 96f6eb5..0000000 --- a/e2e/istio/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Istio e2e tests - -The [Istio](https://istio.io/) end-to-end tests are designed to verify the integration of the -Auth Service with Istio. They deploy a [KinD](https://kind.sigs.k8s.io/) Kubernetes cluster where Istio and the Auth Service are -installed and then run a series of tests to verify the integration. The following diagram shows the setup: - -```mermaid -flowchart LR - subgraph "KinD Cluster" - subgraph http-echo - sidecar - app - end - istio-ingress["istio-ingress\n(nodeport:30000)"] - authservice - redis - keycloak["keycloak\n(nodeport:30001)"] - end - subgraph "Host" - test-suite - end - - sidecar --> app - sidecar -.OIDC.-> authservice - authservice -.-> sidecar - authservice --sessions--> redis - authservice --OIDC-->keycloak - istio-ingress --> sidecar - test-suite --> istio-ingress - test-suite --user login--> keycloak -``` - -## Accessing the cluster from the host machine - -For convenience, the Kind cluster Kubeconfig is generated in `cluster/kubeconfig`, and an be used to access -the cluster from the host machine. For example: - -```bash -$ kubectl --kubeconfig cluster/kubeconfig get namespaces -``` - -## Manually creating and destroying the cluster - -The Kind cluster is automatically created and destroyed when running the test suites. However, it is -possible to manually create and destroy the cluster by running the following commands: - -```bash -$ make kind-create -$ make kind-destroy -``` - -This is useful for debugging purposes. diff --git a/e2e/istio/cluster/istiod-config.yaml b/e2e/istio/cluster/istiod-config.yaml deleted file mode 100644 index a3443b2..0000000 --- a/e2e/istio/cluster/istiod-config.yaml +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -pilot: - # Clear the default resources to allow it to run in very constrained local environments - # without explicitly requesting more memory than the one that might be available in the - # local Kind cluster. - resources: null - -#global: -# proxy: -# # Default log levels to be used by sidecars and gateways. -# # The HTTP and RBAC loggers will print the requests and responses with all the headers, -# # and the access decision records. -# # The `misc` is a bit noisy due to the version of Envoy being used, and we just silence -# # it to remove deprecation warning messages. -# componentLogLevel: "http:debug,rbac:debug,misc:error" - -meshConfig: - defaultConfig: - # Make sure everything is up and running before we start trying to - # send traffic to the services - holdApplicationUntilProxyStarts: true - extensionProviders: - # Configure the backend for the Auth Service provider that can be used in AuthorizationPolicies - # in CUSTOM mode. - - name: authservice-grpc - envoyExtAuthzGrpc: - service: "authservice.authservice.svc.cluster.local" - port: "10003" diff --git a/e2e/istio/cluster/istiogw-config.yaml b/e2e/istio/cluster/istiogw-config.yaml deleted file mode 100644 index b1de018..0000000 --- a/e2e/istio/cluster/istiogw-config.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2023 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Configure the ingress as NodePort to make accessible to the local test environment -service: - type: NodePort - ports: - - name: status-port - port: 15021 - protocol: TCP - targetPort: 15021 - - name: https - port: 443 - nodePort: 30000 # Make it accessible form the host without having to install MetalLB or others - protocol: TCP - targetPort: 443 - - name: http - port: 80 - nodePort: 30002 - protocol: TCP - targetPort: 80 - -# Clear the default resources to allow it to run in very constrained local environments -# without explicitly requesting more memory than the one that might be available in the -# local Kind cluster. -resources: null diff --git a/e2e/istio/cluster/kind-config.yaml b/e2e/istio/cluster/kind-config.yaml deleted file mode 100644 index d5264fb..0000000 --- a/e2e/istio/cluster/kind-config.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: kind.x-k8s.io/v1alpha4 -kind: Cluster -nodes: - - role: control-plane - extraPortMappings: - # We expose the httpbin service in these host ports to make them accessible from the host without - # having to install additional tooling such as MetalLB to access it. - - containerPort: 30000 - hostPort: 30000 - - containerPort: 30001 - hostPort: 30001 - - containerPort: 30002 - hostPort: 30002 diff --git a/e2e/istio/cluster/manifests/authservice.yaml b/e2e/istio/cluster/manifests/authservice.yaml deleted file mode 100644 index 447f7fb..0000000 --- a/e2e/istio/cluster/manifests/authservice.yaml +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: v1 -kind: Namespace -metadata: - name: authservice ---- -apiVersion: v1 -kind: Service -metadata: - name: authservice - namespace: authservice - labels: - app: authservice -spec: - ports: - - port: 10003 - targetPort: 10003 - name: grpc-authservice - protocol: TCP - - port: 10004 - targetPort: 10004 - name: grpc-health - protocol: TCP - selector: - app: authservice ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: authservice - namespace: authservice - labels: - app: authservice ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: authservice - namespace: authservice -spec: - replicas: 1 - selector: - matchLabels: - app: authservice - version: v1 - template: - metadata: - labels: - app: authservice - version: v1 - spec: - serviceAccountName: authservice - containers: - - name: authservice - # This image is automatically generated by the e2e test setup in the `make kind-load` target - image: kind-local/authservice:e2e - imagePullPolicy: Never # Load directly from kind - ports: - - name: authz - containerPort: 10003 - protocol: TCP - - name: health - containerPort: 10004 - protocol: TCP - volumeMounts: - - name: config - mountPath: /etc/authservice - livenessProbe: - initialDelaySeconds: 1 - periodSeconds: 5 - tcpSocket: - port: 10003 - readinessProbe: - initialDelaySeconds: 5 - periodSeconds: 5 - httpGet: - port: 10004 - path: /healthz - volumes: - - name: config - configMap: - name: authservice-config ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: authservice-secrets - namespace: authservice -rules: - - apiGroups: [""] - resources: ["secrets"] - verbs: ["get", "watch", "list"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: authservice-secrets - namespace: authservice -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: authservice-secrets -subjects: - - kind: ServiceAccount - name: authservice - namespace: authservice ---- -apiVersion: v1 -kind: Secret -metadata: - name: client-secret - namespace: authservice -type: Opaque -stringData: - client-secret: "authservice-secret" ---- -kind: ConfigMap -apiVersion: v1 -metadata: - name: authservice-config - namespace: authservice -data: - config.json: | - { - "listen_address": "0.0.0.0", - "listen_port": "10003", - "log_level": "debug", - "allow_unmatched_requests": false, - "chains": [ - { - "name": "keycloak", - "filters": [ - { - "oidc": - { - "configuration_uri": "http://keycloak.keycloak:8080/realms/master/.well-known/openid-configuration", - "callback_uri": "https://http-echo.authservice.internal/callback", - "client_id": "authservice", - "client_secret_ref": { - "namespace": "authservice", - "name": "client-secret" - }, - "cookie_name_prefix": "authservice", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "access_token": { - "header": "x-access-token" - }, - "redis_session_store_config": { - "server_uri": "redis://redis.redis.svc.cluster.local:6379" - } - } - } - ] - } - ] - } diff --git a/e2e/istio/cluster/manifests/authz-policy.yaml b/e2e/istio/cluster/manifests/authz-policy.yaml deleted file mode 100644 index 0ed3fa2..0000000 --- a/e2e/istio/cluster/manifests/authz-policy.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: security.istio.io/v1beta1 -kind: AuthorizationPolicy -metadata: - name: authservice - namespace: http-echo -spec: - action: CUSTOM - provider: - # Name defined in the extensionProviders property in the MeshConfig - # (the `istio` ConfigMap in the istio-system namespace) - name: authservice-grpc - # A single empty rule will force all requests to be forwarded to the external - # authorization backend, as long as the workload is captured by the selectors - # configured above. - rules: - - {} diff --git a/e2e/istio/cluster/manifests/http-echo.yaml b/e2e/istio/cluster/manifests/http-echo.yaml deleted file mode 100644 index 17e4397..0000000 --- a/e2e/istio/cluster/manifests/http-echo.yaml +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: v1 -kind: Namespace -metadata: - name: http-echo - labels: - istio-injection: enabled ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: http-echo - namespace: http-echo ---- -apiVersion: v1 -kind: Service -metadata: - name: http-echo - namespace: http-echo - labels: - app: http-echo - service: http-echo -spec: - ports: - - name: http - port: 8080 - targetPort: 8080 - protocol: TCP - selector: - app: http-echo ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: http-echo - namespace: http-echo -spec: - replicas: 1 - selector: - matchLabels: - app: http-echo - version: v1 - template: - metadata: - labels: - app: http-echo - version: v1 - spec: - serviceAccountName: http-echo - containers: - - name: http-echo - image: jmalloc/echo-server:0.3.6 - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: 8080 - protocol: TCP diff --git a/e2e/istio/cluster/manifests/ingress-gateway.yaml b/e2e/istio/cluster/manifests/ingress-gateway.yaml deleted file mode 100644 index 424ada6..0000000 --- a/e2e/istio/cluster/manifests/ingress-gateway.yaml +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: networking.istio.io/v1alpha3 -kind: Gateway -metadata: - name: http-echo - namespace: http-echo -spec: - selector: - istio: ingress - servers: - - hosts: - - "http-echo.authservice.internal" - port: - number: 443 - name: https - protocol: HTTPS - tls: - mode: SIMPLE - credentialName: http-echo-certs - - hosts: - - "http-echo.authservice.internal" - port: - number: 80 - name: http - protocol: HTTP - tls: - httpsRedirect: true ---- -apiVersion: networking.istio.io/v1alpha3 -kind: VirtualService -metadata: - name: http-echo - namespace: http-echo -spec: - hosts: - - "http-echo.authservice.internal" - gateways: - - http-echo - http: - - route: - - destination: - host: http-echo.http-echo.svc.cluster.local - port: - number: 8080 diff --git a/e2e/istio/cluster/manifests/keycloak.yaml b/e2e/istio/cluster/manifests/keycloak.yaml deleted file mode 100644 index 1047201..0000000 --- a/e2e/istio/cluster/manifests/keycloak.yaml +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: v1 -kind: Namespace -metadata: - name: keycloak ---- -apiVersion: v1 -kind: Service -metadata: - name: keycloak - namespace: keycloak - labels: - app: keycloak -spec: - type: NodePort # Make it accessible form the host without having to install MetalLB or others - ports: - - port: 8080 - targetPort: 8080 - name: http-keycloak - nodePort: 30001 # Expose it directly to the e2e tests - protocol: TCP - selector: - app: keycloak ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: keycloak - namespace: keycloak - labels: - app: keycloak ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: keycloak - namespace: keycloak -spec: - replicas: 1 - selector: - matchLabels: - app: keycloak - version: v1 - template: - metadata: - labels: - app: keycloak - version: v1 - spec: - serviceAccountName: keycloak - containers: - - name: keycloak - image: quay.io/keycloak/keycloak:23.0.6 - imagePullPolicy: IfNotPresent - args: - - "start-dev" - ports: - - name: keycloak - containerPort: 8080 - protocol: TCP - env: - - name: KEYCLOAK_ADMIN - value: admin - - name: KEYCLOAK_ADMIN_PASSWORD - value: admin - readinessProbe: - initialDelaySeconds: 5 - periodSeconds: 5 - tcpSocket: - port: 8080 ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: setup-keycloak - namespace: keycloak -spec: - template: - spec: - initContainers: - - name: wait-for-keycloak - image: busybox:stable - command: ["sh", "-c", "until nc -v -z -w3 keycloak 8080; do sleep 2; done"] - containers: - - name: setup-keycloak - image: quay.io/keycloak/keycloak:23.0.6 - command: ["bash", "/opt/keycloak/scripts/setup.sh"] - env: - - name: KEYCLOAK_ADMIN - value: admin - - name: KEYCLOAK_ADMIN_PASSWORD - value: admin - volumeMounts: - - name: setup-script - mountPath: /opt/keycloak/scripts - volumes: - - name: setup-script - configMap: - name: setup-keycloak - restartPolicy: Never - backoffLimit: 4 ---- -kind: ConfigMap -apiVersion: v1 -metadata: - name: setup-keycloak - namespace: keycloak -data: - setup.sh: | - KEYCLOAK_SERVER="http://keycloak:8080" - REALM="master" - USERNAME=authservice - PASSWORD=authservice - CLIENT_ID=authservice - CLIENT_SECRET=authservice-secret - REDIRECT_URL=https://http-echo.authservice.internal/callback - - set -ex - - /opt/keycloak/bin/kcadm.sh create users \ - -s username="${USERNAME}" \ - -s enabled=true \ - --server "${KEYCLOAK_SERVER}" \ - --realm "${REALM}" \ - --user "${KEYCLOAK_ADMIN}" \ - --password "${KEYCLOAK_ADMIN_PASSWORD}" - - /opt/keycloak/bin/kcadm.sh set-password \ - --username "${USERNAME}" \ - --new-password "${PASSWORD}" \ - --server "${KEYCLOAK_SERVER}" \ - --realm "${REALM}" \ - --user "${KEYCLOAK_ADMIN}" \ - --password "${KEYCLOAK_ADMIN_PASSWORD}" - - /opt/keycloak/bin/kcreg.sh create \ - -s clientId="${CLIENT_ID}" \ - -s secret="${CLIENT_SECRET}" \ - -s "redirectUris=[\"${REDIRECT_URL}\"]" \ - -s consentRequired=false \ - --server "${KEYCLOAK_SERVER}" \ - --realm "${REALM}" \ - --user "${KEYCLOAK_ADMIN}" \ - --password "${KEYCLOAK_ADMIN_PASSWORD}" diff --git a/e2e/istio/cluster/manifests/redis.yaml b/e2e/istio/cluster/manifests/redis.yaml deleted file mode 100644 index 971bf94..0000000 --- a/e2e/istio/cluster/manifests/redis.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: v1 -kind: Namespace -metadata: - name: redis ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: redis - namespace: redis ---- -apiVersion: v1 -kind: Service -metadata: - name: redis - namespace: redis - labels: - app: redis - service: redis -spec: - ports: - - name: redis - port: 6379 - targetPort: 6379 - protocol: TCP - selector: - app: redis ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: redis - namespace: redis -spec: - replicas: 1 - selector: - matchLabels: - app: redis - version: v1 - template: - metadata: - labels: - app: redis - version: v1 - spec: - serviceAccountName: redis - containers: - - name: redis - image: redis:7.2.4 - imagePullPolicy: IfNotPresent - ports: - - name: redis - containerPort: 6379 - protocol: TCP - livenessProbe: - initialDelaySeconds: 1 - periodSeconds: 5 - tcpSocket: - port: 6379 diff --git a/e2e/istio/cluster/manifests/telemetry.yaml b/e2e/istio/cluster/manifests/telemetry.yaml deleted file mode 100644 index 5f4668d..0000000 --- a/e2e/istio/cluster/manifests/telemetry.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -apiVersion: telemetry.istio.io/v1alpha1 -kind: Telemetry -metadata: - name: access-logs - namespace: istio-system -spec: - accessLogging: - - providers: - - name: envoy diff --git a/e2e/istio/istio_test.go b/e2e/istio/istio_test.go deleted file mode 100644 index 14a4e67..0000000 --- a/e2e/istio/istio_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package istio - -import ( - "io" - "net/http" - - "github.com/tetrateio/authservice-go/e2e" -) - -const ( - testURLTLS = "https://http-echo.authservice.internal" - testURLPlain = "http://http-echo.authservice.internal" - testCAFile = "certs/ca.crt" - keyCloakLoginFormID = "kc-form-login" - username = "authservice" - password = "authservice" -) - -func (i *IstioSuite) TestIstioEnforcement() { - for name, uri := range map[string]string{ - "client requests TLS": testURLTLS, - "client requests plain text, is redirected to TLS": testURLPlain, - } { - i.Run(name, func() { - // Initialize the test OIDC client that will keep track of the state of the OIDC login process - // Initialize it for each test to not reuse the session between them - client, err := e2e.NewOIDCTestClient( - e2e.WithLoggingOptions(i.T().Log, true), - e2e.WithCustomCA(testCAFile), - // Map the keycloak cluster DNS name to the local address where the service is exposed - e2e.WithCustomAddressMappings(map[string]string{ - "http-echo.authservice.internal:80": "localhost:30002", - "http-echo.authservice.internal:443": "localhost:30000", - "keycloak.keycloak:8080": "localhost:30001", - }), - ) - i.Require().NoError(err) - - // Send a request to the test server. It will be redirected to the IdP login page - res, err := client.Get(uri) - i.Require().NoError(err) - - // Parse the response body to get the URL where the login page would post the user-entered credentials - i.Require().NoError(client.ParseLoginForm(res.Body, keyCloakLoginFormID)) - - // Submit the login form to the IdP. This will authenticate and redirect back to the application - res, err = client.Login(map[string]string{"username": username, "password": password, "credentialId": ""}) - i.Require().NoError(err) - - // Verify that we get the expected response from the application - body, err := io.ReadAll(res.Body) - i.Require().NoError(err) - i.Require().Equal(http.StatusOK, res.StatusCode) - i.Require().Contains(string(body), "Request served by http-echo") - // as the destination app is an echo server that returns the received request in the body, we can verify this - // received contained the proper tokens - i.Require().Contains(string(body), "Authorization: Bearer") - i.Require().Contains(string(body), "X-Access-Token:") - }) - } -} diff --git a/e2e/istio/suite_test.go b/e2e/istio/suite_test.go deleted file mode 100644 index 4725590..0000000 --- a/e2e/istio/suite_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package istio - -import ( - "context" - "fmt" - "os/exec" - "strings" - "testing" - - "github.com/stretchr/testify/suite" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - - "github.com/tetrateio/authservice-go/e2e" -) - -const ( - istiodConfig = "cluster/istiod-config.yaml" - istioGwConfig = "cluster/istiogw-config.yaml" - manifestsDir = "cluster/manifests" -) - -// testManifests contains the list of manifests that will be deployed in the cluster before running the e2e tests -var testManifests = []string{ - "keycloak.yaml", - "redis.yaml", - "authservice.yaml", - "http-echo.yaml", - "ingress-gateway.yaml", - "authz-policy.yaml", - "telemetry.yaml", -} - -// istioInstall contains the commands to install Istio using Helm, so we don't require -// downloading `istioctl` or other tooling that would just make the e2e tests take more time. -var istioInstall = []string{ - "helm repo add istio https://istio-release.storage.googleapis.com/charts --force-update", - "helm repo update istio", - fmt.Sprintf("helm --kubeconfig %s install istio-base istio/base -n istio-system --create-namespace", e2e.KubeConfig), - fmt.Sprintf("helm --kubeconfig %s install istiod istio/istiod -n istio-system -f %s --wait", e2e.KubeConfig, istiodConfig), - fmt.Sprintf("helm --kubeconfig %s install istio-ingress istio/gateway -n istio-system -f %s --wait", e2e.KubeConfig, istioGwConfig), -} - -// IstioSuite is a suite that installs Istio in the Kubernetes cluster and runs tests against it. -type IstioSuite struct { - e2e.K8sSuite -} - -func TestIstio(t *testing.T) { - suite.Run(t, &IstioSuite{}) -} - -// SetupSuite initializes the Kubernetes clients, installs Istio in the cluster and waits until the -// services are up and running. -func (i *IstioSuite) SetupSuite() { - i.K8sSuite.SetupSuite() - - client, err := kubernetes.NewForConfig(i.Kubeconfig) - i.Require().NoError(err) - - // If Istio is already installed, just return and do not try to install it again - // and make e2e tests easier to run multiple times without tearing down the entire - // environment - if !i.istioInstalled(client) { - i.installistio() - } - - i.T().Log("deploying the test services...") - for _, f := range testManifests { - i.MustApply(context.Background(), manifestsDir+"/"+f) - } - i.WaitForPods(client, "keycloak", "job-name=setup-keycloak", corev1.PodSucceeded, e2e.PodInitialized) - i.WaitForPods(client, "redis", "", corev1.PodRunning, e2e.PodReady) - i.WaitForPods(client, "authservice", "", corev1.PodRunning, e2e.PodReady) - i.WaitForPods(client, "http-echo", "", corev1.PodRunning, e2e.PodReady) -} - -func (i *IstioSuite) installistio() { - i.T().Log("installing Istio...") - - for _, cmd := range istioInstall { - parts := strings.Split(cmd, " ") - out, err := exec.Command(parts[0], parts[1:]...).CombinedOutput() - i.Require().NoError(err, string(out)) - } -} - -func (i *IstioSuite) istioInstalled(client kubernetes.Interface) bool { - _, err := client.CoreV1().Services("istio-system").Get(context.Background(), "istiod", metav1.GetOptions{}) - return err == nil -} diff --git a/e2e/k8s_suite.go b/e2e/k8s_suite.go deleted file mode 100644 index 4d805cf..0000000 --- a/e2e/k8s_suite.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package e2e - -import ( - "bytes" - "context" - "errors" - "io" - "os" - "time" - - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - "gopkg.in/yaml.v3" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - k8syamlserializer "k8s.io/apimachinery/pkg/runtime/serializer/yaml" - "k8s.io/client-go/discovery" - memory "k8s.io/client-go/discovery/cached" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/restmapper" - "k8s.io/client-go/tools/clientcmd" -) - -const ( - // KubeConfig is the path where the e2e test setup generates the kubeconfig file - KubeConfig = "cluster/kubeconfig" - // This timeout accounts for the image pull and the pod and - // sidecar bootstrap - defaultServiceStartupTimeout = 5 * time.Minute -) - -var ( - PodReady = corev1.PodCondition{Type: corev1.PodReady, Status: corev1.ConditionTrue} - PodInitialized = corev1.PodCondition{Type: corev1.PodInitialized, Status: corev1.ConditionTrue} -) - -// K8sSuite is a suite that provides a Kubernetes client and a set of helper methods -// to interact with the Kubernetes API. -// Kubernetes tests can crete specific suite types that embeds this one to get access to -// the Kubernetes client and the helper methods. -type K8sSuite struct { - suite.Suite - - Kubeconfig *rest.Config - - dynamicClient *dynamic.DynamicClient - discoveryClient *discovery.DiscoveryClient - mapper meta.RESTMapper - unstructuredSerializer runtime.Serializer -} - -// SetupSuite initializes the Kubernetes clients. -func (k *K8sSuite) SetupSuite() { - cfg, err := clientcmd.BuildConfigFromFlags("", KubeConfig) - k.Require().NoError(err) - k.Kubeconfig = cfg - - k.dynamicClient, err = dynamic.NewForConfig(cfg) - k.Require().NoError(err) - k.discoveryClient, err = discovery.NewDiscoveryClientForConfig(cfg) - k.Require().NoError(err) - - k.mapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(k.discoveryClient)) - k.unstructuredSerializer = k8syamlserializer.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) -} - -// MustApply applies the given file to the Kubernetes cluster and fails the test if an error occurs. -func (k *K8sSuite) MustApply(ctx context.Context, file string) { - k.Require().NoError(k.Apply(ctx, file)) -} - -// Apply the given file to the Kubernetes cluster. -func (k *K8sSuite) Apply(ctx context.Context, file string) error { - var errs []error - for _, o := range k.ReadObjects(file) { - _, err := k.dynamicClientFor(o).Apply(ctx, o.GetName(), o, metav1.ApplyOptions{FieldManager: "e2e"}) - if err != nil { - errs = append(errs, err) - } - } - return errors.Join(errs...) -} - -// MustDelete deletes the resources defined in the given file from the Kubernetes cluster and fails -// the test if an error occurs. -func (k *K8sSuite) MustDelete(ctx context.Context, file string) { - k.Require().NoError(k.Delete(ctx, file)) -} - -// Delete the resources defined in the given file from the Kubernetes cluster. -func (k *K8sSuite) Delete(ctx context.Context, file string) error { - var ( - errs []error - objs = k.ReadObjects(file) - ) - - for i := len(objs) - 1; i >= 0; i-- { - o := objs[i] - if err := k.dynamicClientFor(o).Delete(ctx, o.GetName(), metav1.DeleteOptions{}); err != nil { - errs = append(errs, err) - } - } - - return errors.Join(errs...) -} - -// ReadObjects reads the given file and returns the list of Kubernetes objects defined in it. -func (k *K8sSuite) ReadObjects(file string) []*unstructured.Unstructured { - content, err := os.ReadFile(file) - k.Require().NoError(err) - - out := make([]*unstructured.Unstructured, 0) - dec := yaml.NewDecoder(bytes.NewReader(content)) - - for { - var node yaml.Node - err := dec.Decode(&node) - if errors.Is(err, io.EOF) { - break - } - k.Require().NoError(err) - - content, err := yaml.Marshal(&node) - k.Require().NoError(err) - - obj := &unstructured.Unstructured{} - _, _, err = k.unstructuredSerializer.Decode(content, nil, obj) - k.Require().NoError(err) - - out = append(out, obj) - } - - return out -} - -// dynamicClientFor returns a dynamic client for the given object. -func (k *K8sSuite) dynamicClientFor(obj *unstructured.Unstructured) dynamic.ResourceInterface { - gvk := obj.GetObjectKind().GroupVersionKind() - mapping, err := k.mapper.RESTMapping(gvk.GroupKind(), gvk.Version) - k.Require().NoError(err) - - var dr dynamic.ResourceInterface - if mapping.Scope.Name() == meta.RESTScopeNameNamespace { - dr = k.dynamicClient.Resource(mapping.Resource).Namespace(obj.GetNamespace()) - } else { - dr = k.dynamicClient.Resource(mapping.Resource) - } - - return dr -} - -// WaitForPods waits for the pods in the given namespace and with the given selector -// to be in the given phase and condition. -func (k *K8sSuite) WaitForPods(client kubernetes.Interface, namespace, selector string, phase corev1.PodPhase, condition corev1.PodCondition) { - k.T().Logf("waiting for %s/[%s] to be %v...", namespace, selector, phase) - - require.Eventually(k.T(), func() bool { - opts := metav1.ListOptions{ - LabelSelector: selector, - } - pods, err := client.CoreV1().Pods(namespace).List(context.Background(), opts) - if err != nil || len(pods.Items) == 0 { - return false - } - - checkPods: - for _, p := range pods.Items { - if p.Status.Phase != phase { - return false - } - - if p.Status.Conditions == nil { - return false - } - - for _, c := range p.Status.Conditions { - if c.Type == condition.Type && c.Status == condition.Status { - continue checkPods // pod is ready, check next pod - } - } - - return false - } - - return true - }, defaultServiceStartupTimeout, 2*time.Second) -} diff --git a/e2e/keycloak/Makefile b/e2e/keycloak/Makefile deleted file mode 100644 index da8b4a8..0000000 --- a/e2e/keycloak/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -.PHONY: e2e-pre -e2e-pre:: gen - -include ../suite-certs.mk -include ../suite-docker.mk - -gen: clean-certs ca/ca.internal certificate/host.docker.internal ## Generates the CA and certificates - @chmod -R a+r $(CERTS_DIR) - -.PHONY: clean -clean:: clean-certs diff --git a/e2e/keycloak/README.md b/e2e/keycloak/README.md deleted file mode 100644 index 753a40b..0000000 --- a/e2e/keycloak/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Keycloak e2e tests - -The Keycloak e2e test suite contains tests that use the Keycloak OIDC provider. A -Keycloak instance is deployed and configured in the Docker environment as the backend -OIDC provider. The following diagram shows the setup: - -```mermaid -flowchart LR - subgraph "Docker Compose" - envoy["envoy\n(localhost:8443)"] - app - authservice - redis - keycloak["keycloak\n(localhost:9443)"] - idp-proxy["idp-proxy\n(localhost:9000)"] - end - subgraph "Host" - test-suite - end - authservice --sessions--> redis - authservice --OIDC--> idp-proxy --> keycloak - test-suite --user login--> keycloak - test-suite --> envoy - envoy -.OIDC.-> authservice - authservice -.-> envoy - envoy --> app -``` - -The setup is performed in the [setup-keycloak.sh](setup-keycloak.sh) script, which configures the default -`master` realm with: - -* A user named `authservice` with a predefined password. -* A client named `authservice` with a predefined secret. - -The user and client will be used in the e2e tests to verify the entire Authorization Code flow. diff --git a/e2e/keycloak/authz-config.json b/e2e/keycloak/authz-config.json deleted file mode 100644 index a999f9c..0000000 --- a/e2e/keycloak/authz-config.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 10003, - "log_level": "debug", - "chains": [ - { - "name": "keycloak", - "filters": [ - { - "oidc": { - "configuration_uri": "https://host.docker.internal:9443/realms/master/.well-known/openid-configuration", - "proxy_uri": "http://idp-proxy:9000", - "callback_uri": "https://host.docker.internal:8443/callback", - "client_id": "authservice", - "client_secret": "authservice-secret", - "cookie_name_prefix": "authservice", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "access_token": { - "header": "x-access-token" - }, - "logout": { - "path": "/logout", - "redirect_uri": "https://host.docker.internal:9443/realms/master/protocol/openid-connect/logout" - }, - "redis_session_store_config": { - "server_uri": "redis://redis:6379" - }, - "trusted_certificate_authority_file": "/etc/authservice/certs/ca.crt", - "trusted_certificate_authority_refresh_interval": "60.25s" - } - } - ] - } - ] -} diff --git a/e2e/keycloak/docker-compose.yaml b/e2e/keycloak/docker-compose.yaml deleted file mode 100644 index 87e3ccb..0000000 --- a/e2e/keycloak/docker-compose.yaml +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -version: "3.9" - -services: - # This is a proxy that intercepts requests to the target application and calls the authservice to - # perform the OIDC authorization check. - envoy: - depends_on: - ext-authz: - condition: service_started - image: envoyproxy/envoy:v1.29-latest - platform: linux/${ARCH:-amd64} - command: -c /etc/envoy/envoy-config.yaml --log-level warning - ports: - - "8443:443" - volumes: - - type: bind - source: envoy-config.yaml - target: /etc/envoy/envoy-config.yaml - - type: bind - source: certs - target: /etc/envoy/certs - - # This is a simple HTTP server that will be used as the target application for the tests. - http-echo: - image: jmalloc/echo-server:0.3.6 - platform: linux/${ARCH:-amd64} - hostname: http-echo - - # idp-proxy is a proxy that will be used to forward traffic to the external authorization server - # Set the OIDC config `proxy_url` to `http://idp-proxy:9000` in the `authservice` config to use this proxy. - idp-proxy: - image: envoyproxy/envoy:v1.29-latest - platform: linux/${ARCH:-amd64} - command: -c /etc/envoy/envoy-config.yaml --log-level warning - ports: - - "9000:9000" # Expose the proxy to auth0 - volumes: - - type: bind - source: idp-proxy-config.yaml - target: /etc/envoy/envoy-config.yaml - extra_hosts: # Required when running on Linux - - "host.docker.internal:host-gateway" - - # This is the `authservice` image that should be up-to-date when running the tests. - ext-authz: - depends_on: - setup-keycloak: - condition: service_completed_successfully - idp-proxy: - condition: service_started - image: ${DOCKER_HUB}/authservice:latest-${ARCH:-amd64} - platform: linux/${ARCH:-amd64} - volumes: - - type: bind - source: authz-config.json - target: /etc/authservice/config.json - - type: bind - source: certs - target: /etc/authservice/certs - extra_hosts: # Required when running on Linux - - "host.docker.internal:host-gateway" - - # Redis container to be used to persist the session information and OIDC authorization - # state. - redis: - image: redis:7.2.4 - platform: linux/${ARCH:-amd64} - - # Keycloak container to be used as the OIDC provider. The tests will use the `master` realm - keycloak: - image: quay.io/keycloak/keycloak:23.0.6 - platform: linux/${ARCH:-amd64} - environment: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin - ports: - - "9443:9443" - command: start-dev --https-port=9443 --https-certificate-file=/opt/keycloak/certs/host.docker.internal.crt --https-certificate-key-file=/opt/keycloak/certs/host.docker.internal.key - volumes: - - type: bind - source: certs - target: /opt/keycloak/certs - healthcheck: - test: /opt/keycloak/bin/kcadm.sh get realms/master --server http://localhost:8080 --realm master --user admin --password admin - interval: 5s - timeout: 2s - retries: 30 - start_period: 5s - extra_hosts: # Required when running on Linux - - "host.docker.internal:host-gateway" - - # Container to configure the Keycloak instance with a User and Client application - setup-keycloak: - depends_on: - keycloak: - condition: service_healthy - image: quay.io/keycloak/keycloak:23.0.6 - platform: linux/${ARCH:-amd64} - environment: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin - entrypoint: /opt/setup-keycloak.sh - volumes: - - type: bind - source: setup-keycloak.sh - target: /opt/setup-keycloak.sh - # Healthcheck to make sure the created client has been successfully created, and that other services - # can depend on - healthcheck: - test: /opt/keycloak/bin/kcreg.sh get authservice --server http://keycloak:8080 --realm master --user admin --password admin - interval: 2s - timeout: 2s - retries: 10 - start_period: 2s diff --git a/e2e/keycloak/envoy-config.yaml b/e2e/keycloak/envoy-config.yaml deleted file mode 100644 index 8b71191..0000000 --- a/e2e/keycloak/envoy-config.yaml +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -static_resources: - listeners: - - name: http - address: - socket_address: - address: 0.0.0.0 - port_value: 443 - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: http - access_log: - - name: envoy.access_loggers.stdout - typed_config: - "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog - route_config: - name: http - virtual_hosts: - - name: http - domains: ["*"] - routes: - - match: - prefix: "/" - route: - cluster: http_echo - http_filters: - - name: envoy.filters.http.ext_authz - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz - transport_api_version: V3 - grpc_service: - envoy_grpc: - cluster_name: ext_authz - timeout: 300s - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - transport_socket: - name: envoy.transport_sockets.tls - typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext - common_tls_context: - tls_certificates: - - certificate_chain: - filename: /etc/envoy/certs/host.docker.internal.crt - private_key: - filename: /etc/envoy/certs/host.docker.internal.key - validation_context: - trusted_ca: - filename: /etc/envoy/certs/ca.crt - - clusters: - - name: ext_authz - connect_timeout: 0.25s - type: LOGICAL_DNS - lb_policy: ROUND_ROBIN - typed_extension_protocol_options: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - explicit_http_config: - http2_protocol_options: {} - load_assignment: - cluster_name: ext_authz - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: ext-authz - port_value: 10003 - - name: http_echo - connect_timeout: 0.25s - type: LOGICAL_DNS - lb_policy: ROUND_ROBIN - typed_extension_protocol_options: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - explicit_http_config: - http2_protocol_options: {} - load_assignment: - cluster_name: http_echo - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: http-echo - port_value: 8080 diff --git a/e2e/keycloak/idp-proxy-config.yaml b/e2e/keycloak/idp-proxy-config.yaml deleted file mode 100644 index d372fa4..0000000 --- a/e2e/keycloak/idp-proxy-config.yaml +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -static_resources: - listeners: - - # This listener is used as a proxy to the Keycloak server - - name: keycloak-proxy - address: - socket_address: - address: 0.0.0.0 - port_value: 9000 - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: auth0-proxy - access_log: - - name: envoy.access_loggers.stdout - typed_config: - "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog - # Allow receiving HTTP/2 CONNECT requests - http2_protocol_options: - allow_connect: true - route_config: - name: keycloak-proxy - virtual_hosts: - - name: keycloak-proxy - domains: ["*"] - routes: - - match: - connect_matcher: {} - route: - cluster: keycloak - upgrade_configs: - - upgrade_type: CONNECT - connect_config: {} - http_filters: - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - - - clusters: - - name: keycloak - connect_timeout: 1s - type: LOGICAL_DNS - dns_lookup_family: V4_ONLY - load_assignment: - cluster_name: keycloak - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: # to the Keycloak server serving TLS - address: host.docker.internal - port_value: 9443 diff --git a/e2e/keycloak/keycloak_test.go b/e2e/keycloak/keycloak_test.go deleted file mode 100644 index 80bc19a..0000000 --- a/e2e/keycloak/keycloak_test.go +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package keycloak - -import ( - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/tetrateio/authservice-go/e2e" -) - -const ( - dockerLocalHost = "host.docker.internal" - idpBaseURLHost = "https://host.docker.internal:9443" - keyCloakLoginFormID = "kc-form-login" - testCAFile = "certs/ca.crt" - username = "authservice" - password = "authservice" -) - -var ( - testURL = fmt.Sprintf("https://%s:8443", dockerLocalHost) - - // customAddressMappings to let the test HTTP client connect to the right hosts - customAddressMappings = map[string]string{ - "host.docker.internal:9443": "localhost:9443", // Keycloak - "host.docker.internal:8443": "localhost:8443", // Target application - } - - idpProxyService = "idp-proxy" - okPayload = "Request served by http-echo" -) - -func TestOIDCUsesTheConfiguredProxy(t *testing.T) { - client, err := e2e.NewOIDCTestClient( - e2e.WithCustomCA(testCAFile), - e2e.WithLoggingOptions(t.Log, true), - e2e.WithCustomAddressMappings(customAddressMappings), - ) - require.NoError(t, err) - - docker := e2e.NewDockerCompose(e2e.WithDockerComposeLogFunc(t.Log)) - - // Stop the IDP proxy and verify that the request is rejected - require.NoError(t, docker.StopDockerService(idpProxyService)) - require.NoError(t, docker.WaitForDockerService(idpProxyService, e2e.DockerServiceExited, 10*time.Second, 500*time.Millisecond)) - - res, err := client.Get(testURL) - require.NoError(t, err) - require.Equal(t, http.StatusForbidden, res.StatusCode) - - // Start the IDP proxy and verify that the request is accepted - require.NoError(t, docker.StartDockerService(idpProxyService)) - require.NoError(t, docker.WaitForDockerService(idpProxyService, e2e.DockerServiceContainerUp, 10*time.Second, 500*time.Millisecond)) - - res, err = client.Get(testURL) - require.NoError(t, err) - // As this is the first request with no kind of session, the client is redirected to the IdP login page. - // Assume this redirect as enough to consider the test successful and relay the details into the TestOIDC test. - require.Equal(t, http.StatusOK, res.StatusCode) - require.NoError(t, client.ParseLoginForm(res.Body, keyCloakLoginFormID)) -} - -func TestOIDC(t *testing.T) { - // Initialize the test OIDC client that will keep track of the state of the OIDC login process - client, err := e2e.NewOIDCTestClient( - e2e.WithCustomCA(testCAFile), - e2e.WithLoggingOptions(t.Log, true), - e2e.WithCustomAddressMappings(customAddressMappings), - ) - require.NoError(t, err) - - // Send a request to the test server. It will be redirected to the IdP login page - res, err := client.Get(testURL) - require.NoError(t, err) - - // Parse the response body to get the URL where the login page would post the user-entered credentials - require.NoError(t, client.ParseLoginForm(res.Body, keyCloakLoginFormID)) - - // Submit the login form to the IdP. This will authenticate and redirect back to the application - res, err = client.Login(map[string]string{"username": username, "password": password, "credentialId": ""}) - require.NoError(t, err) - - // Verify that we get the expected response from the application - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) -} - -func TestOIDCRefreshTokens(t *testing.T) { - // Initialize the test OIDC client that will keep track of the state of the OIDC login process - client, err := e2e.NewOIDCTestClient( - e2e.WithCustomCA(testCAFile), - e2e.WithLoggingOptions(t.Log, true), - e2e.WithCustomAddressMappings(customAddressMappings), - ) - require.NoError(t, err) - - // Send a request to the test server. It will be redirected to the IdP login page - res, err := client.Get(testURL) - require.NoError(t, err) - - // Parse the response body to get the URL where the login page would post the user-entered credentials - require.NoError(t, client.ParseLoginForm(res.Body, keyCloakLoginFormID)) - - // Submit the login form to the IdP. This will authenticate and redirect back to the application - res, err = client.Login(map[string]string{"username": username, "password": password, "credentialId": ""}) - require.NoError(t, err) - - // Verify that we get the expected response from the application - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - - // Access tokens should expire in 10 seconds (tried with 5, but keycloak setup fails) - // Let's perform a request now and after 10 seconds to verify that the access token is refreshed - - t.Run("request with same tokens", func(t *testing.T) { - res, err = client.Get(testURL) - require.NoError(t, err) - - body, err = io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - }) - - t.Log("waiting for access token to expire...") - time.Sleep(10 * time.Second) - - t.Run("request with expired tokens", func(t *testing.T) { - res, err = client.Get(testURL) - require.NoError(t, err) - - body, err = io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - }) -} - -func TestOIDCLogout(t *testing.T) { - - // Initialize the test OIDC client that will keep track of the state of the OIDC login process - client, err := e2e.NewOIDCTestClient( - e2e.WithCustomCA(testCAFile), - e2e.WithLoggingOptions(t.Log, true), - e2e.WithBaseURL(idpBaseURLHost), - e2e.WithCustomAddressMappings(customAddressMappings), - ) - require.NoError(t, err) - - t.Run("first request requires login", func(t *testing.T) { - // Send a request to the test server. It will be redirected to the IdP login page - res, err := client.Get(testURL) - require.NoError(t, err) - - // Parse the response body to get the URL where the login page would post the user-entered credentials - require.NoError(t, client.ParseLoginForm(res.Body, keyCloakLoginFormID)) - - // Submit the login form to the IdP. This will authenticate and redirect back to the application - res, err = client.Login(map[string]string{"username": username, "password": password, "credentialId": ""}) - require.NoError(t, err) - - // Verify that we get the expected response from the application - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - }) - - t.Run("second request works without login redirect", func(t *testing.T) { - res, err := client.Get(testURL) - require.NoError(t, err) - - // Verify that we get the expected response from the application - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - }) - - t.Run("logout", func(t *testing.T) { - // Logout - res, err := client.Get(testURL + "/logout") - require.NoError(t, err) - - // Parse the response body to get the URL where the login page would post the session logout - require.NoError(t, client.ParseLogoutForm(res.Body)) - - // Submit the logout form to the IdP. This will log out the user and redirect back to the application - res, err = client.Logout() - require.NoError(t, err) - - // Verify that we get the logout confirmation from the IDP - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), "You are logged out") - }) - - t.Run("request after logout requires login again", func(t *testing.T) { - // Send a request to the test server. It will be redirected to the IdP login page - res, err := client.Get(testURL) - require.NoError(t, err) - - // Parse the response body to get the URL where the login page would post the user-entered credentials - require.NoError(t, client.ParseLoginForm(res.Body, keyCloakLoginFormID)) - - // Submit the login form to the IdP. This will authenticate and redirect back to the application - res, err = client.Login(map[string]string{"username": username, "password": password, "credentialId": ""}) - require.NoError(t, err) - - // Verify that we get the expected response from the application - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - }) -} diff --git a/e2e/keycloak/setup-keycloak.sh b/e2e/keycloak/setup-keycloak.sh deleted file mode 100755 index 7fc46af..0000000 --- a/e2e/keycloak/setup-keycloak.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -KEYCLOAK_SERVER="http://keycloak:8080" -REALM="master" -USERNAME=authservice -PASSWORD=authservice -CLIENT_ID=authservice -CLIENT_SECRET=authservice-secret -REDIRECT_URL=https://host.docker.internal:8443/callback - -set -ex - -/opt/keycloak/bin/kcadm.sh update realms/${REALM} \ - -s accessTokenLifespan=10 \ - --realm "${REALM}" \ - --server "${KEYCLOAK_SERVER}" \ - --user "${KEYCLOAK_ADMIN}" \ - --password "${KEYCLOAK_ADMIN_PASSWORD}" - -/opt/keycloak/bin/kcadm.sh create users \ - -s username="${USERNAME}" \ - -s enabled=true \ - --server "${KEYCLOAK_SERVER}" \ - --realm "${REALM}" \ - --user "${KEYCLOAK_ADMIN}" \ - --password "${KEYCLOAK_ADMIN_PASSWORD}" - -/opt/keycloak/bin/kcadm.sh set-password \ - --username "${USERNAME}" \ - --new-password "${PASSWORD}" \ - --server "${KEYCLOAK_SERVER}" \ - --realm "${REALM}" \ - --user "${KEYCLOAK_ADMIN}" \ - --password "${KEYCLOAK_ADMIN_PASSWORD}" - -/opt/keycloak/bin/kcreg.sh create \ - -s clientId="${CLIENT_ID}" \ - -s secret="${CLIENT_SECRET}" \ - -s "redirectUris=[\"${REDIRECT_URL}\"]" \ - -s consentRequired=false \ - --server "${KEYCLOAK_SERVER}" \ - --realm "${REALM}" \ - --user "${KEYCLOAK_ADMIN}" \ - --password "${KEYCLOAK_ADMIN_PASSWORD}" diff --git a/e2e/legacy/Makefile b/e2e/legacy/Makefile deleted file mode 100644 index eb576f4..0000000 --- a/e2e/legacy/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Variables exported to parameterize the image used in the docker-compose.yaml -# This allows running the same setup with the authservice-go and the legacy authservice images. - -ifneq ($(E2E_SUITE_MODE),legacy) -export E2E_IMAGE ?= $(DOCKER_HUB)/$(NAME):latest-$(ARCH) -export E2E_PLATFORM ?= linux/$(ARCH) -else -E2E_LEGACY_IMAGE ?= ghcr.io/istio-ecosystem/authservice/authservice:0.5.3 -export E2E_IMAGE ?= $(E2E_LEGACY_IMAGE) -export E2E_PLATFORM ?= linux/amd64 -endif - - -.PHONY: e2e-pre -e2e-pre:: gen - -include ../suite-certs.mk -include ../suite-docker.mk - -gen: clean-certs ca/ca.internal certificate/host.docker.internal ## Generates the CA and certificates - @chmod -R a+r $(CERTS_DIR) - -.PHONY: clean -clean:: clean-certs diff --git a/e2e/legacy/README.md b/e2e/legacy/README.md deleted file mode 100644 index d153971..0000000 --- a/e2e/legacy/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Legacy e2e tests - -The legacy e2e test suite has a configuration that is compatible with the old authservice, and -contains tests that validate that the current authservice can be used as a drop-in replacement -for the old one. - -The test suite contains tests that use the Keycloak OIDC provider. A -Keycloak instance is deployed and configured in the Docker environment as the backend -OIDC provider. The following diagram shows the setup: - -```mermaid -flowchart LR - subgraph "Docker Compose" - envoy["envoy\n(localhost:8443)"] - app - authservice - redis - keycloak["keycloak\n(localhost:9443)"] - end - subgraph "Host" - test-suite - end - authservice --sessions--> redis - authservice --OIDC--> keycloak - test-suite --user login--> keycloak - test-suite --> envoy - envoy -.OIDC.-> authservice - authservice -.-> envoy - envoy --> app -``` - -The setup is performed in the [setup-keycloak.sh](setup-keycloak.sh) script, which -configures the default `master` realm with: - -* A user named `authservice` with a predefined password. -* A client named `authservice` with a predefined secret. - -The user and client will be used in the e2e tests to verify the entire Authorization Code flow. diff --git a/e2e/legacy/authz-config.json b/e2e/legacy/authz-config.json deleted file mode 100644 index 4d4dd55..0000000 --- a/e2e/legacy/authz-config.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 10003, - "log_level": "trace", - "threads": 1, - "chains": [ - { - "name": "keycloak", - "filters": [ - { - "oidc": { - "authorization_uri": "https://host.docker.internal:9443/realms/master/protocol/openid-connect/auth", - "token_uri": "https://host.docker.internal:9443/realms/master/protocol/openid-connect/token", - "jwks_fetcher": { - "jwks_uri": "https://host.docker.internal:9443/realms/master/protocol/openid-connect/certs", - "skip_verify_peer_cert": "true" - }, - "callback_uri": "https://host.docker.internal:8443/callback", - "client_id": "authservice", - "client_secret": "authservice-secret", - "cookie_name_prefix": "authservice", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "access_token": { - "header": "x-access-token" - }, - "logout": { - "path": "/logout", - "redirect_uri": "https://host.docker.internal:9443/realms/master/protocol/openid-connect/logout" - }, - "redis_session_store_config": { - "server_uri": "tcp://redis:6379" - }, - "skip_verify_peer_cert": true - } - } - ] - } - ] -} diff --git a/e2e/legacy/docker-compose.yaml b/e2e/legacy/docker-compose.yaml deleted file mode 100644 index 5a886ba..0000000 --- a/e2e/legacy/docker-compose.yaml +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -version: "3.9" - -services: - # This is a proxy that intercepts requests to the target application and calls the authservice to - # perform the OIDC authorization check. - envoy: - depends_on: - ext-authz: - condition: service_started - image: envoyproxy/envoy:v1.29-latest - platform: linux/${ARCH:-amd64} - command: -c /etc/envoy/envoy-config.yaml --log-level warning - ports: - - "8443:443" - volumes: - - type: bind - source: envoy-config.yaml - target: /etc/envoy/envoy-config.yaml - - type: bind - source: certs - target: /etc/envoy/certs - - # This is a simple HTTP server that will be used as the target application for the tests. - http-echo: - image: jmalloc/echo-server:0.3.6 - platform: linux/${ARCH:-amd64} - hostname: http-echo - - # This is the `authservice` image that should be up-to-date when running the tests. - ext-authz: - depends_on: - setup-keycloak: - condition: service_completed_successfully - image: ${E2E_IMAGE} - platform: ${E2E_PLATFORM} - volumes: - - type: bind - source: authz-config.json - target: /etc/authservice/config.json - extra_hosts: # Required when running on Linux - - "host.docker.internal:host-gateway" - - # Redis container to be used to persist the session information and OIDC authorization - # state. - redis: - image: redis:7.2.4 - platform: linux/${ARCH:-amd64} - - # Keycloak container to be used as the OIDC provider. The tests will use the `master` realm - keycloak: - image: quay.io/keycloak/keycloak:23.0.6 - platform: linux/${ARCH:-amd64} - environment: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin - ports: - - "9443:9443" - command: start-dev --https-port=9443 --https-certificate-file=/opt/keycloak/certs/host.docker.internal.crt --https-certificate-key-file=/opt/keycloak/certs/host.docker.internal.key - volumes: - - type: bind - source: certs - target: /opt/keycloak/certs - healthcheck: - test: /opt/keycloak/bin/kcadm.sh get realms/master --server http://localhost:8080 --realm master --user admin --password admin - interval: 5s - timeout: 2s - retries: 30 - start_period: 5s - extra_hosts: # Required when running on Linux - - "host.docker.internal:host-gateway" - - # Container to configure the Keycloak instance with a User and Client application - setup-keycloak: - depends_on: - keycloak: - condition: service_healthy - image: quay.io/keycloak/keycloak:23.0.6 - platform: linux/${ARCH:-amd64} - environment: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin - entrypoint: /opt/setup-keycloak.sh - volumes: - - type: bind - source: setup-keycloak.sh - target: /opt/setup-keycloak.sh - # Healthcheck to make sure the created client has been successfully created, and that other services - # can depend on - healthcheck: - test: /opt/keycloak/bin/kcreg.sh get authservice --server http://keycloak:8080 --realm master --user admin --password admin - interval: 2s - timeout: 2s - retries: 10 - start_period: 2s diff --git a/e2e/legacy/envoy-config.yaml b/e2e/legacy/envoy-config.yaml deleted file mode 100644 index 8b71191..0000000 --- a/e2e/legacy/envoy-config.yaml +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -static_resources: - listeners: - - name: http - address: - socket_address: - address: 0.0.0.0 - port_value: 443 - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: http - access_log: - - name: envoy.access_loggers.stdout - typed_config: - "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog - route_config: - name: http - virtual_hosts: - - name: http - domains: ["*"] - routes: - - match: - prefix: "/" - route: - cluster: http_echo - http_filters: - - name: envoy.filters.http.ext_authz - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz - transport_api_version: V3 - grpc_service: - envoy_grpc: - cluster_name: ext_authz - timeout: 300s - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - transport_socket: - name: envoy.transport_sockets.tls - typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext - common_tls_context: - tls_certificates: - - certificate_chain: - filename: /etc/envoy/certs/host.docker.internal.crt - private_key: - filename: /etc/envoy/certs/host.docker.internal.key - validation_context: - trusted_ca: - filename: /etc/envoy/certs/ca.crt - - clusters: - - name: ext_authz - connect_timeout: 0.25s - type: LOGICAL_DNS - lb_policy: ROUND_ROBIN - typed_extension_protocol_options: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - explicit_http_config: - http2_protocol_options: {} - load_assignment: - cluster_name: ext_authz - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: ext-authz - port_value: 10003 - - name: http_echo - connect_timeout: 0.25s - type: LOGICAL_DNS - lb_policy: ROUND_ROBIN - typed_extension_protocol_options: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - explicit_http_config: - http2_protocol_options: {} - load_assignment: - cluster_name: http_echo - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: http-echo - port_value: 8080 diff --git a/e2e/legacy/legacy_test.go b/e2e/legacy/legacy_test.go deleted file mode 100644 index 0549d33..0000000 --- a/e2e/legacy/legacy_test.go +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package keycloak - -import ( - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/tetrateio/authservice-go/e2e" -) - -const ( - dockerLocalHost = "host.docker.internal" - idpBaseURLHost = "https://host.docker.internal:9443" - keyCloakLoginFormID = "kc-form-login" - testCAFile = "certs/ca.crt" - username = "authservice" - password = "authservice" -) - -var ( - testURL = fmt.Sprintf("https://%s:8443", dockerLocalHost) - - // customAddressMappings to let the test HTTP client connect to the right hosts - customAddressMappings = map[string]string{ - "host.docker.internal:9443": "localhost:9443", // Keycloak - "host.docker.internal:8443": "localhost:8443", // Target application - } - - okPayload = "Request served by http-echo" -) - -func TestOIDC(t *testing.T) { - // Initialize the test OIDC client that will keep track of the state of the OIDC login process - client, err := e2e.NewOIDCTestClient( - e2e.WithCustomCA(testCAFile), - e2e.WithLoggingOptions(t.Log, true), - e2e.WithCustomAddressMappings(customAddressMappings), - ) - require.NoError(t, err) - - // Send a request to the test server. It will be redirected to the IdP login page - res, err := client.Get(testURL) - require.NoError(t, err) - - // Parse the response body to get the URL where the login page would post the user-entered credentials - require.NoError(t, client.ParseLoginForm(res.Body, keyCloakLoginFormID)) - - // Submit the login form to the IdP. This will authenticate and redirect back to the application - res, err = client.Login(map[string]string{"username": username, "password": password, "credentialId": ""}) - require.NoError(t, err) - - // Verify that we get the expected response from the application - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) -} - -func TestOIDCRefreshTokens(t *testing.T) { - // Initialize the test OIDC client that will keep track of the state of the OIDC login process - client, err := e2e.NewOIDCTestClient( - e2e.WithCustomCA(testCAFile), - e2e.WithLoggingOptions(t.Log, true), - e2e.WithCustomAddressMappings(customAddressMappings), - ) - require.NoError(t, err) - - // Send a request to the test server. It will be redirected to the IdP login page - res, err := client.Get(testURL) - require.NoError(t, err) - - // Parse the response body to get the URL where the login page would post the user-entered credentials - require.NoError(t, client.ParseLoginForm(res.Body, keyCloakLoginFormID)) - - // Submit the login form to the IdP. This will authenticate and redirect back to the application - res, err = client.Login(map[string]string{"username": username, "password": password, "credentialId": ""}) - require.NoError(t, err) - - // Verify that we get the expected response from the application - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - - // Access tokens should expire in 10 seconds (tried with 5, but keycloak setup fails) - // Let's perform a request now and after 10 seconds to verify that the access token is refreshed - - t.Run("request with same tokens", func(t *testing.T) { - res, err = client.Get(testURL) - require.NoError(t, err) - - body, err = io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - }) - - t.Log("waiting for access token to expire...") - time.Sleep(10 * time.Second) - - t.Run("request with expired tokens", func(t *testing.T) { - res, err = client.Get(testURL) - require.NoError(t, err) - - body, err = io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - }) -} - -func TestOIDCLogout(t *testing.T) { - // Initialize the test OIDC client that will keep track of the state of the OIDC login process - client, err := e2e.NewOIDCTestClient( - e2e.WithCustomCA(testCAFile), - e2e.WithLoggingOptions(t.Log, true), - e2e.WithBaseURL(idpBaseURLHost), - e2e.WithCustomAddressMappings(customAddressMappings), - ) - require.NoError(t, err) - - t.Run("first request requires login", func(t *testing.T) { - // Send a request to the test server. It will be redirected to the IdP login page - res, err := client.Get(testURL) - require.NoError(t, err) - - // Parse the response body to get the URL where the login page would post the user-entered credentials - require.NoError(t, client.ParseLoginForm(res.Body, keyCloakLoginFormID)) - - // Submit the login form to the IdP. This will authenticate and redirect back to the application - res, err = client.Login(map[string]string{"username": username, "password": password, "credentialId": ""}) - require.NoError(t, err) - - // Verify that we get the expected response from the application - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - }) - - t.Run("second request works without login redirect", func(t *testing.T) { - res, err := client.Get(testURL) - require.NoError(t, err) - - // Verify that we get the expected response from the application - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - }) - - t.Run("logout", func(t *testing.T) { - // Logout - res, err := client.Get(testURL + "/logout") - require.NoError(t, err) - - // Parse the response body to get the URL where the login page would post the session logout - require.NoError(t, client.ParseLogoutForm(res.Body)) - - // Submit the logout form to the IdP. This will log out the user and redirect back to the application - res, err = client.Logout() - require.NoError(t, err) - - // Verify that we get the logout confirmation from the IDP - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), "You are logged out") - }) - - t.Run("request after logout requires login again", func(t *testing.T) { - // Send a request to the test server. It will be redirected to the IdP login page - res, err := client.Get(testURL) - require.NoError(t, err) - - // Parse the response body to get the URL where the login page would post the user-entered credentials - require.NoError(t, client.ParseLoginForm(res.Body, keyCloakLoginFormID)) - - // Submit the login form to the IdP. This will authenticate and redirect back to the application - res, err = client.Login(map[string]string{"username": username, "password": password, "credentialId": ""}) - require.NoError(t, err) - - // Verify that we get the expected response from the application - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, http.StatusOK, res.StatusCode) - require.Contains(t, string(body), okPayload) - }) -} diff --git a/e2e/legacy/setup-keycloak.sh b/e2e/legacy/setup-keycloak.sh deleted file mode 100755 index 7fc46af..0000000 --- a/e2e/legacy/setup-keycloak.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -KEYCLOAK_SERVER="http://keycloak:8080" -REALM="master" -USERNAME=authservice -PASSWORD=authservice -CLIENT_ID=authservice -CLIENT_SECRET=authservice-secret -REDIRECT_URL=https://host.docker.internal:8443/callback - -set -ex - -/opt/keycloak/bin/kcadm.sh update realms/${REALM} \ - -s accessTokenLifespan=10 \ - --realm "${REALM}" \ - --server "${KEYCLOAK_SERVER}" \ - --user "${KEYCLOAK_ADMIN}" \ - --password "${KEYCLOAK_ADMIN_PASSWORD}" - -/opt/keycloak/bin/kcadm.sh create users \ - -s username="${USERNAME}" \ - -s enabled=true \ - --server "${KEYCLOAK_SERVER}" \ - --realm "${REALM}" \ - --user "${KEYCLOAK_ADMIN}" \ - --password "${KEYCLOAK_ADMIN_PASSWORD}" - -/opt/keycloak/bin/kcadm.sh set-password \ - --username "${USERNAME}" \ - --new-password "${PASSWORD}" \ - --server "${KEYCLOAK_SERVER}" \ - --realm "${REALM}" \ - --user "${KEYCLOAK_ADMIN}" \ - --password "${KEYCLOAK_ADMIN_PASSWORD}" - -/opt/keycloak/bin/kcreg.sh create \ - -s clientId="${CLIENT_ID}" \ - -s secret="${CLIENT_SECRET}" \ - -s "redirectUris=[\"${REDIRECT_URL}\"]" \ - -s consentRequired=false \ - --server "${KEYCLOAK_SERVER}" \ - --realm "${REALM}" \ - --user "${KEYCLOAK_ADMIN}" \ - --password "${KEYCLOAK_ADMIN_PASSWORD}" diff --git a/e2e/mock/Makefile b/e2e/mock/Makefile deleted file mode 100644 index 1eb95df..0000000 --- a/e2e/mock/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -include ../suite-docker.mk diff --git a/e2e/mock/README.md b/e2e/mock/README.md deleted file mode 100644 index b6e323d..0000000 --- a/e2e/mock/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Mock e2e tests - -The `mock` e2e test suite contains tests that use the `mock` OIDC provider. -The suite is mostly used to verify the correct behavior of the different configuration -options without making real requests to an OIDC provider. - -It can be used for rapid prorotyping and development of new features. diff --git a/e2e/mock/authz-config.json b/e2e/mock/authz-config.json deleted file mode 100644 index 976af13..0000000 --- a/e2e/mock/authz-config.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 10003, - "log_level": "debug", - "chains": [ - { - "name": "mock-allow", - "match": { - "header": "X-Authz-Decision", - "equality": "allow" - }, - "filters": [ - { - "mock": { - "allow": true - } - } - ] - }, - { - "name": "mock-allow-prefix", - "match": { - "header": "X-Authz-Decision", - "prefix": "ok" - }, - "filters": [ - { - "mock": { - "allow": true - } - } - ] - }, - { - "name": "mock-deny", - "filters": [ - { - "mock": { - "allow": false - } - } - ] - } - ], - "triggerRules": [ - { - "excludedPaths": [ - { "prefix": "/excluded" } - ], - "includedPaths": [ - { "prefix": "/included" } - ] - } - ] -} diff --git a/e2e/mock/docker-compose.yaml b/e2e/mock/docker-compose.yaml deleted file mode 100644 index 1e930a3..0000000 --- a/e2e/mock/docker-compose.yaml +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -version: "3.9" - -services: - envoy: - # This is the main backend service. It returns a fixed HTTP 200 response. - # It is configured to serve on port 80, and to use the ext-authz filter - # to intercept all requests. - image: envoyproxy/envoy:v1.29-latest - platform: linux/${ARCH:-amd64} - command: -c /etc/envoy/envoy-config.yaml --log-level warning - ports: - - "8080:80" - volumes: - - type: bind - source: envoy-config.yaml - target: /etc/envoy/envoy-config.yaml - - # This is the `authservice` image that should be up-to-date when running the tests. - ext-authz: - image: ${DOCKER_HUB}/authservice:latest-${ARCH:-amd64} - platform: linux/${ARCH:-amd64} - volumes: - - type: bind - source: authz-config.json - target: /etc/authservice/config.json diff --git a/e2e/mock/envoy-config.yaml b/e2e/mock/envoy-config.yaml deleted file mode 100644 index 8ac57ac..0000000 --- a/e2e/mock/envoy-config.yaml +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -static_resources: - listeners: - - name: http - address: - socket_address: - address: 0.0.0.0 - port_value: 80 - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: http - access_log: - - name: envoy.access_loggers.stdout - typed_config: - "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog - route_config: - name: http - virtual_hosts: - - name: http - domains: ["*"] - routes: - - match: - prefix: "/" - direct_response: - status: 200 - body: - inline_string: "Access allowed\n" - http_filters: - - name: envoy.filters.http.ext_authz - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz - transport_api_version: V3 - grpc_service: - envoy_grpc: - cluster_name: ext_authz - timeout: 300s - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - - clusters: - - name: ext_authz - connect_timeout: 0.25s - type: LOGICAL_DNS - lb_policy: ROUND_ROBIN - typed_extension_protocol_options: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - explicit_http_config: - http2_protocol_options: {} - load_assignment: - cluster_name: ext_authz - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: ext-authz - port_value: 10003 diff --git a/e2e/mock/mock_test.go b/e2e/mock/mock_test.go deleted file mode 100644 index f4a63bd..0000000 --- a/e2e/mock/mock_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package mock - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/require" -) - -const ( - testEnvoyURL = "http://localhost:8080" - testAuthzHeader = "X-Authz-Decision" - - excludedPath = "/excluded" - includedPath = "/included" -) - -func TestMock(t *testing.T) { - t.Run("allow-equality", func(t *testing.T) { - req, err := http.NewRequest("GET", withPath(includedPath), nil) - require.NoError(t, err) - req.Header.Set(testAuthzHeader, "allow") - - res, err := http.DefaultClient.Do(req) - require.NoError(t, err) - require.Equal(t, 200, res.StatusCode) - }) - - t.Run("allow-prefix", func(t *testing.T) { - req, err := http.NewRequest("GET", withPath(includedPath), nil) - require.NoError(t, err) - req.Header.Set(testAuthzHeader, "ok-prefix") - - res, err := http.DefaultClient.Do(req) - require.NoError(t, err) - require.Equal(t, 200, res.StatusCode) - }) - - t.Run("deny-header", func(t *testing.T) { - req, err := http.NewRequest("GET", withPath(includedPath), nil) - require.NoError(t, err) - req.Header.Set(testAuthzHeader, "non-match") - - res, err := http.DefaultClient.Do(req) - require.NoError(t, err) - require.Equal(t, 403, res.StatusCode) - }) - - t.Run("deny", func(t *testing.T) { - res, err := http.Get(withPath(includedPath)) - - require.NoError(t, err) - require.Equal(t, 403, res.StatusCode) - }) - - // excluded path should not be affected by the header since the auth service checks are not triggered. - t.Run("allow-non-matching-header-for-excluded-path", func(t *testing.T) { - req, err := http.NewRequest("GET", withPath(excludedPath), nil) - require.NoError(t, err) - req.Header.Set(testAuthzHeader, "non-match") - - res, err := http.DefaultClient.Do(req) - require.NoError(t, err) - require.Equal(t, 200, res.StatusCode) - }) -} - -func withPath(p string) string { - return testEnvoyURL + p -} diff --git a/e2e/redis/Makefile b/e2e/redis/Makefile deleted file mode 100644 index 1eb95df..0000000 --- a/e2e/redis/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -include ../suite-docker.mk diff --git a/e2e/redis/README.md b/e2e/redis/README.md deleted file mode 100644 index 0036c56..0000000 --- a/e2e/redis/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Redis e2e tests - -The Redis e2e test suite contains tests that verify the correct behavior of the Redis -session store for the OIDC providers. It targets the `SessionStore` interface directly -and verifies the contents of the Redis database on each operation. diff --git a/e2e/redis/docker-compose.yaml b/e2e/redis/docker-compose.yaml deleted file mode 100644 index e8a95db..0000000 --- a/e2e/redis/docker-compose.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -version: "3.9" - -services: - redis: - image: redis:7.2.4 - platform: linux/${ARCH:-amd64} - ports: - - "6379:6379" diff --git a/e2e/redis/store_test.go b/e2e/redis/store_test.go deleted file mode 100644 index a00d13e..0000000 --- a/e2e/redis/store_test.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package mock - -import ( - "context" - "testing" - "time" - - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/redis/go-redis/v9" - "github.com/stretchr/testify/require" - - "github.com/tetrateio/authservice-go/internal/oidc" -) - -const redisURL = "redis://localhost:6379" - -func TestRedisTokenResponse(t *testing.T) { - opts, err := redis.ParseURL(redisURL) - require.NoError(t, err) - client := redis.NewClient(opts) - - store, err := oidc.NewRedisStore(&oidc.Clock{}, client, 0, 1*time.Minute) - require.NoError(t, err) - - ctx := context.Background() - - tr, err := store.GetTokenResponse(ctx, "s1") - require.NoError(t, err) - require.Nil(t, tr) - - // Create a session and verify it's added and accessed time is set - tr = &oidc.TokenResponse{ - IDToken: newToken(), - AccessToken: newToken(), - AccessTokenExpiresAt: time.Now().Add(30 * time.Minute), - } - require.NoError(t, store.SetTokenResponse(ctx, "s1", tr)) - - // Verify we can retrieve the token - got, err := store.GetTokenResponse(ctx, "s1") - require.NoError(t, err) - // The testify library doesn't properly compare times, so we need to do it manually - // then set the times in the returned object so that we can compare the rest of the - // fields normally - require.True(t, tr.AccessTokenExpiresAt.Equal(got.AccessTokenExpiresAt)) - got.AccessTokenExpiresAt = tr.AccessTokenExpiresAt - require.Equal(t, tr, got) - - // Verify that the token TTL has been set - ttl := client.TTL(ctx, "s1").Val() - require.Greater(t, ttl, time.Duration(0)) -} - -func TestRedisAuthorizationState(t *testing.T) { - opts, err := redis.ParseURL(redisURL) - require.NoError(t, err) - client := redis.NewClient(opts) - - store, err := oidc.NewRedisStore(&oidc.Clock{}, client, 0, 1*time.Minute) - require.NoError(t, err) - - ctx := context.Background() - - as, err := store.GetAuthorizationState(ctx, "s1") - require.NoError(t, err) - require.Nil(t, as) - - // Create a session and verify it's added and accessed time is set - as = &oidc.AuthorizationState{ - State: "state", - Nonce: "nonce", - RequestedURL: "https://example.com", - } - require.NoError(t, store.SetAuthorizationState(ctx, "s1", as)) - - // Verify that the right state is returned - got, err := store.GetAuthorizationState(ctx, "s1") - require.NoError(t, err) - require.Equal(t, as, got) - - // Verify that the token TTL has been set - ttl := client.TTL(ctx, "s1").Val() - require.Greater(t, ttl, time.Duration(0)) -} - -func TestSessionExpiration(t *testing.T) { - opts, err := redis.ParseURL(redisURL) - require.NoError(t, err) - client := redis.NewClient(opts) - - store, err := oidc.NewRedisStore(&oidc.Clock{}, client, 2*time.Second, 0) - require.NoError(t, err) - - ctx := context.Background() - - t.Run("expire-token", func(t *testing.T) { - tr := &oidc.TokenResponse{ - IDToken: newToken(), - AccessToken: newToken(), - AccessTokenExpiresAt: time.Now().Add(30 * time.Minute), - } - require.NoError(t, store.SetTokenResponse(ctx, "s1", tr)) - require.Eventually(t, func() bool { - got, err := store.GetTokenResponse(ctx, "s1") - return got == nil && err == nil - }, 3*time.Second, 1*time.Second) - }) - - t.Run("expire-state", func(t *testing.T) { - as := &oidc.AuthorizationState{ - State: "state", - Nonce: "nonce", - RequestedURL: "https://example.com", - } - require.NoError(t, store.SetAuthorizationState(ctx, "s1", as)) - require.Eventually(t, func() bool { - got, err := store.GetAuthorizationState(ctx, "s1") - return got == nil && err == nil - }, 3*time.Second, 1*time.Second) - }) -} - -func newToken() string { - token, _ := jwt.NewBuilder(). - Issuer("authservice"). - Subject("user"). - Expiration(time.Now().Add(time.Hour)). - Build() - signed, _ := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte("key"))) - return string(signed) -} diff --git a/e2e/suite-certs.mk b/e2e/suite-certs.mk deleted file mode 100644 index ae79211..0000000 --- a/e2e/suite-certs.mk +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -CERTS_DIR := certs -SHELL := bash - -$(CERTS_DIR): - @mkdir -p $(CERTS_DIR) - -.PHONY: clean-certs -clean-certs: ## Cleans the certificates - @rm -rf $(CERTS_DIR) - -ca/%: $(CERTS_DIR) ## Generates the CA - @echo "Generating $(*) CA" - @openssl genrsa -out "$(CERTS_DIR)/ca.key" 4096 - @openssl req -x509 -new -sha256 -nodes -days 365 -key "$(CERTS_DIR)/ca.key" -out "$(CERTS_DIR)/ca.crt" \ - -subj "/C=US/ST=California/O=Tetrate/OU=Engineering/CN=$(*)" \ - -addext "basicConstraints=critical,CA:true,pathlen:1" \ - -addext "keyUsage=critical,digitalSignature,nonRepudiation,keyEncipherment,keyCertSign" \ - -addext "subjectAltName=DNS:$(*)" - -certificate/%: $(CERTS_DIR) ## Generates the certificates - @echo "Generating $(*) cert" - @openssl genrsa -out "$(CERTS_DIR)/$(*).key" 2048 - @openssl req -new -sha256 -key "$(CERTS_DIR)/$(*).key" -out "$(CERTS_DIR)/$(*).csr" \ - -subj "/C=US/ST=California/O=Tetrate/OU=Engineering/CN=$(*)" \ - -addext "subjectAltName=DNS:$(*)" - @openssl x509 -req -sha256 -days 120 -in "$(CERTS_DIR)/$(*).csr" -out "$(CERTS_DIR)/$(*).crt" \ - -CA "$(CERTS_DIR)/ca.crt" -CAkey "$(CERTS_DIR)/ca.key" -CAcreateserial -CAserial $(CERTS_DIR)/ca.srl \ - -extfile <(printf "subjectAltName=DNS:$(*)") diff --git a/e2e/suite-docker.mk b/e2e/suite-docker.mk deleted file mode 100644 index 6f7b646..0000000 --- a/e2e/suite-docker.mk +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file contains the common e2e targets and variables for e2e suites that use -# Docker compose to spin up the environment. -# When adding a suite, create a new directory under e2e/ and add a Makefile that -# includes this file. - -ROOT := $(shell git rev-parse --show-toplevel) - -include $(ROOT)/env.mk # Load common variables --include $(ROOT)/.makerc # Pick up any local overrides. - -# Force run of the e2e tests by default -E2E_TEST_OPTS ?= -count=1 - - -.PHONY: e2e -e2e: e2e-pre - @$(MAKE) e2e-test e2e-post - -.PHONY: e2e-test -e2e-test: - @go test $(E2E_TEST_OPTS) ./... || ( $(MAKE) e2e-post-error; exit 1 ) - -.PHONY: e2e-pre -e2e-pre:: - @docker compose up --detach --wait --force-recreate --remove-orphans || ($(MAKE) e2e-post-error; exit 1) - -.PHONY: e2e-post -e2e-post:: -ifeq ($(E2E_PRESERVE_LOGS),true) - @$(MAKE) capture-logs -endif - @docker compose down --remove-orphans - -.PHONY: e2e-post-error -e2e-post-error: capture-logs - -.PHONY: capture-logs -capture-logs: - @mkdir -p ./logs - @docker compose logs > logs/docker-compose-logs.log - -.PHONY: clean -clean:: - @rm -rf ./logs diff --git a/e2e/suite-k8s.mk b/e2e/suite-k8s.mk deleted file mode 100644 index e6f1865..0000000 --- a/e2e/suite-k8s.mk +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file contains the common e2e targets and variables for e2e suites that use -# Kubernetes to spin up the environment. -# When adding a suite, create a new directory under e2e/ and add a Makefile that -# includes this file. - -ROOT := $(shell git rev-parse --show-toplevel) - -include $(ROOT)/env.mk - -E2E_TEST_OPTS ?= -count=1 -E2E_CLUSTER_NAME ?= authservice -E2E_KIND_CONFIG := cluster/kind-config.yaml -E2E_KUBECONFIG := cluster/kubeconfig -E2E_IMAGE ?= $(DOCKER_HUB)/$(NAME):latest-$(ARCH) - - -.PHONY: e2e -e2e: e2e-pre - @$(MAKE) e2e-test e2e-post - -.PHONY: e2e-test -e2e-test: - @go test $(E2E_TEST_OPTS) ./... || ( $(MAKE) e2e-post-error; exit 1 ) - -# Creates the kind cluster and generates the kubeconfig. If the kubeconfig already exists, from -# previous test runs, the kind cluster is not recreated to allow repeated runs of the e2e tests -# against the same environment -e2e-pre:: $(E2E_KUBECONFIG) - @$(MAKE) kind-load - -.PHONY: e2e-post -e2e-post:: ## Destroy the kind cluster and the generated kubeconfig file -ifeq ($(E2E_PRESERVE_LOGS),true) - @$(MAKE) capture-logs -endif - @go run $(KIND) delete cluster -n $(E2E_CLUSTER_NAME) - @rm -f $(E2E_KUBECONFIG) - -.PHONY: e2e-post-error -e2e-post-error: capture-logs - -.PHONY: capture-logs -capture-logs: - @kind export logs --name $(E2E_CLUSTER_NAME) ./logs - -# If the kubeconfig file does not exist, create a new cluster and export the kubeconfig file to the -# configured file -$(E2E_KUBECONFIG): $(E2E_KIND_CONFIG) - @go run $(KIND) create cluster -n $(E2E_CLUSTER_NAME) --kubeconfig $(@) --config $(E2E_KIND_CONFIG) - -# Load the e2e images in the kind cluster. Note images are tagged in the `kind-local` registry -# to use it as a placeholder in e2e kubernetes manifests regardless of the configured $(DOCKER_HUB). This -# also helps state that the images needs to be kind-loaded. -.PHONY: kind-load -kind-load: ## Load the end-to-end test images in the local Kind cluster - @docker tag $(E2E_IMAGE) kind-local/$(NAME):e2e - @go run $(KIND) load docker-image kind-local/$(NAME):e2e -n $(E2E_CLUSTER_NAME) - -.PHONY: kind-create -kind-create: e2e-pre - -.PHONY: kind-destroy -kind-destroy: e2e-post - -.PHONY: clean -clean:: - @rm -rf ./logs diff --git a/e2e/testclient.go b/e2e/testclient.go deleted file mode 100644 index 950d3c4..0000000 --- a/e2e/testclient.go +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package e2e - -import ( - "context" - "crypto/tls" - "crypto/x509" - "fmt" - "io" - "net" - "net/http" - "net/http/httputil" - "net/url" - "os" - "strings" - - "golang.org/x/net/html" -) - -// LoggingRoundTripper is a http.RoundTripper that logs requests and responses. -type LoggingRoundTripper struct { - LogFunc func(...any) - LogBody bool - Delegate http.RoundTripper -} - -// RoundTrip logs all the requests and responses using the configured settings. -func (l LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - if l.LogFunc != nil { - if dump, derr := httputil.DumpRequest(req, l.LogBody); derr == nil { - l.LogFunc(string(dump)) - } - } - - res, err := l.Delegate.RoundTrip(req) - if err == nil && l.LogFunc != nil { - if dump, derr := httputil.DumpResponse(res, l.LogBody); derr == nil { - l.LogFunc(string(dump)) - } - } - - return res, err -} - -// CookieTracker is a http.RoundTripper that tracks cookies received from the server. -type CookieTracker struct { - Delegate http.RoundTripper - Cookies map[string]*http.Cookie -} - -// RoundTrip tracks the cookies received from the server. -func (c *CookieTracker) RoundTrip(req *http.Request) (*http.Response, error) { - for _, ck := range c.Cookies { - req.AddCookie(ck) - } - - res, err := c.Delegate.RoundTrip(req) - - if err == nil { - // Track the cookies received from the server - for _, ck := range res.Cookies() { - c.Cookies[ck.Name] = ck - } - } - - return res, err -} - -// OIDCTestClient encapsulates a http.Client and keeps track of the state of the OIDC login process. -type OIDCTestClient struct { - http *http.Client // Delegate HTTP client - loginURL string // URL of the IdP where users need to authenticate - loginMethod string // Method (GET/POST) to use when posting the credentials to the IdP - idpBaseURL string // Base URL of the IdP - logoutURL string // URL of the IdP where users need to log out - logoutMethod string // Method (GET/POST) to use when posting the logout request to the IdP - logoutForm url.Values // Form data to use when posting the logout request to the IdP - customCA *x509.CertPool // Custom TLS configuration, if needed - mappings *AddressMappings // Custom address mappings - logFunc func(...any) // Logging function to log all requests and responses - logBody bool // Whether to log the request and response bodies -} - -// Option is a functional option for configuring the OIDCTestClient. -type Option func(*OIDCTestClient) error - -// WithCustomCA configures the OIDCTestClient to use a custom CA bundle to verify certificates. -func WithCustomCA(caCert string) Option { - return func(o *OIDCTestClient) error { - caCert, err := os.ReadFile(caCert) - if err != nil { - return err - } - - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(caCert) - o.customCA = caCertPool - return nil - } -} - -// WithLoggingOptions configures the OIDCTestClient to log requests and responses. -func WithLoggingOptions(logFunc func(...any), logBody bool) Option { - return func(o *OIDCTestClient) error { - o.logFunc = logFunc - o.logBody = logBody - return nil - } -} - -// AddressMappings is a custom dialer that resolves specific host:port to specific target addresses. -type AddressMappings struct { - addresses map[string]string -} - -// DialContext is a custom dialer that resolves specific host:port to specific target addresses. -func (a *AddressMappings) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { - if resolved, ok := a.addresses[addr]; ok { - addr = resolved - } - return (&net.Dialer{}).DialContext(ctx, network, addr) -} - -// DialTLSContext is a custom dialer that resolves specific host:port to specific target addresses. -func (a *AddressMappings) DialTLSContext(tlsConfig *tls.Config) func(context.Context, string, string) (net.Conn, error) { - return func(ctx context.Context, network, addr string) (net.Conn, error) { - cfg := tlsConfig.Clone() - if resolved, ok := a.addresses[addr]; ok { - // Use the original hostname for the SNI, to avoid certificate verification errors - cfg.ServerName = addr[:strings.Index(addr, ":")] - addr = resolved - } - return tls.Dial(network, addr, cfg) - } -} - -// WithCustomAddressMappings configures the OIDCTestClient to resolve specific host:port to -// specific target addresses. -func WithCustomAddressMappings(mappings map[string]string) Option { - return func(o *OIDCTestClient) error { - o.mappings = &AddressMappings{ - addresses: mappings, - } - return nil - } -} - -// WithBaseURL configures the OIDCTestClient to use the specified IdP base url. -// Required when the form action URL is relative. For example the logout one. -func WithBaseURL(idpBaseURL string) Option { - return func(o *OIDCTestClient) error { - o.idpBaseURL = idpBaseURL - return nil - } -} - -// NewOIDCTestClient creates a new OIDCTestClient. -func NewOIDCTestClient(opts ...Option) (*OIDCTestClient, error) { - var ( - defaultTransport = http.DefaultTransport.(*http.Transport).Clone() - logging = &LoggingRoundTripper{Delegate: defaultTransport} - cookieTracker = &CookieTracker{Cookies: make(map[string]*http.Cookie), Delegate: logging} - client = &OIDCTestClient{http: &http.Client{Transport: cookieTracker}} - ) - - for _, opt := range opts { - if err := opt(client); err != nil { - return nil, err - } - } - - logging.LogFunc = client.logFunc - logging.LogBody = client.logBody - - defaultTransport.TLSClientConfig = &tls.Config{RootCAs: client.customCA} - if client.mappings != nil { - defaultTransport.DialContext = client.mappings.DialContext - defaultTransport.DialTLSContext = client.mappings.DialTLSContext(defaultTransport.TLSClientConfig) - } - - return client, nil -} - -// Get sends a GET request to the specified URL. -func (o *OIDCTestClient) Get(url string) (*http.Response, error) { - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - return o.Send(req) -} - -// Send sends the specified request. -func (o *OIDCTestClient) Send(req *http.Request) (*http.Response, error) { - return o.http.Do(req) -} - -// Login logs in to the IdP using the provided credentials. -func (o *OIDCTestClient) Login(formData map[string]string) (*http.Response, error) { - if o.loginURL == "" { - return nil, fmt.Errorf("login URL is not set") - } - data := url.Values{} - for k, v := range formData { - data.Add(k, v) - } - req, err := http.NewRequest(o.loginMethod, o.loginURL, strings.NewReader(data.Encode())) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - return o.Send(req) -} - -// Logout logs out from the IdP. -func (o *OIDCTestClient) Logout() (*http.Response, error) { - if o.logoutURL == "" { - return nil, fmt.Errorf("logout URL is not set") - } - req, err := http.NewRequest(o.logoutMethod, o.logoutURL, strings.NewReader(o.logoutForm.Encode())) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - return o.Send(req) -} - -// ParseLoginForm parses the HTML response body to get the URL where the login page would post the user-entered credentials. -func (o *OIDCTestClient) ParseLoginForm(responseBody io.ReadCloser, formID string) error { - body, err := io.ReadAll(responseBody) - if err != nil { - return err - } - o.loginURL, o.loginMethod, _, err = extractFromData(string(body), idFormMatcher{formID}, false) - return err -} - -// ParseLogoutForm parses the HTML response body to get the URL where the logout page would post the session logout. -func (o *OIDCTestClient) ParseLogoutForm(responseBody io.ReadCloser) error { - body, err := io.ReadAll(responseBody) - if err != nil { - return err - } - var logoutURL string - logoutURL, o.logoutMethod, o.logoutForm, err = extractFromData(string(body), firstFormMatcher{}, true) - if err != nil { - return err - } - - // If the logout URL is relative, use the host from the OIDCTestClient configuration - if !strings.HasPrefix(logoutURL, "http") { - logoutURL = o.idpBaseURL + logoutURL - } - o.logoutURL = logoutURL - return nil -} - -// extractFromData extracts the form action, method and values from the HTML response body. -func extractFromData(responseBody string, match formMatch, includeFromInputs bool) (string, string, url.Values, error) { - // Parse HTML response - doc, err := html.Parse(strings.NewReader(responseBody)) - if err != nil { - return "", "", nil, err - } - - // Find the form with the specified ID or match criteria - form := findForm(doc, match) - if form == nil { - return "", "", nil, fmt.Errorf("%s not found", match) - } - - var ( - action, method string - formValues = make(url.Values) - ) - - // Get the action and method of the form - for _, a := range form.Attr { - switch a.Key { - case "action": - action = a.Val - case "method": - method = strings.ToUpper(a.Val) - } - } - - // If we want to include inputs, recursively iterate the children - if includeFromInputs { - formValues = findFormInputs(form) - } - - return action, method, formValues, nil -} - -// findForm recursively searches for a form in the HTML response body that matches the specified criteria. -func findForm(n *html.Node, match formMatch) *html.Node { - // Check if the current node is a form and matches the specified criteria - if match.matches(n) { - return n - } - - // Else, recursively search for the form in child nodes - for c := n.FirstChild; c != nil; c = c.NextSibling { - if form := findForm(c, match); form != nil { - return form - } - } - return nil -} - -// findFormInputs recursively searches for input fields in the HTML form node. -func findFormInputs(formNode *html.Node) url.Values { - form := make(url.Values) - for c := formNode.FirstChild; c != nil; c = c.NextSibling { - if c.Type == html.ElementNode && c.Data == "input" { - var name, value string - for _, a := range c.Attr { - switch a.Key { - case "name": - name = a.Val - case "value": - value = a.Val - } - } - form.Add(name, value) - } else { - for k, v := range findFormInputs(c) { - form[k] = append(form[k], v...) - } - } - } - return form -} - -var ( - _ formMatch = idFormMatcher{} - _ formMatch = firstFormMatcher{} -) - -type ( - // formMatch is an interface that defines the criteria to match a form in the HTML response body. - formMatch interface { - matches(*html.Node) bool - String() string - } - - // idFormMatcher matches a form with the specified ID. - idFormMatcher struct { - id string - } - - // firstFormMatcher matches the first form in the HTML response body. - firstFormMatcher struct{} -) - -func (m idFormMatcher) matches(n *html.Node) bool { - if n.Type != html.ElementNode || n.Data != "form" { - return false - } - - for _, a := range n.Attr { - if a.Key == "id" && a.Val == m.id { - return true - } - } - return false -} - -func (m idFormMatcher) String() string { - return fmt.Sprintf("form with ID '%s'", m.id) -} - -func (m firstFormMatcher) matches(n *html.Node) bool { - return n.Type == html.ElementNode && n.Data == "form" -} - -func (m firstFormMatcher) String() string { - return "first form" -} diff --git a/env.mk b/env.mk deleted file mode 100644 index 2920ecc..0000000 --- a/env.mk +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -ROOT := $(shell git rev-parse --show-toplevel) -GO_MODULE := $(shell sed -ne 's/^module //gp' $(ROOT)/go.mod) -NAME ?= authservice - --include $(ROOT)/.makerc # Pick up any local overrides. - -GOLANGCI_LINT ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2 -GOSIMPORTS ?= github.com/rinchsan/gosimports/cmd/gosimports@v0.3.8 -LICENSER ?= github.com/liamawhite/licenser@v0.6.1-0.20210729145742-be6c77bf6a1f -KIND ?= sigs.k8s.io/kind@v0.18.0 -ENVTEST ?= sigs.k8s.io/controller-runtime/tools/setup-envtest@latest - -TARGETS ?= linux-amd64 linux-arm64 #darwin-amd64 darwin-arm64 -FIPS_TARGETS := $(filter linux-%,$(TARGETS)) - -# DOCKER_HUB is exported so that it can be referenced in e2e docker compose files -export DOCKER_HUB ?= $(GO_MODULE:github.com/%=ghcr.io/%) -DOCKER_TARGETS ?= linux-amd64 linux-arm64 -DOCKER_BUILDER_NAME ?= $(NAME)-builder - -REVISION := $(shell git rev-parse HEAD) -ifneq ($(strip $(VERSION)),) -# Remove the suffix as we want N.N.N instead of vN.N.N -DOCKER_TAG ?= $(strip $(VERSION:v%=%)) -else -DOCKER_TAG ?= $(REVISION) -endif - -# Docker metadata -DOCKER_METADATA := \ - org.opencontainers.image.title=$(NAME) \ - org.opencontainers.image.description="Move OIDC token acquisition out of your app code and into the Istio mesh" \ - org.opencontainers.image.licenses="Apache-2.0" \ - org.opencontainers.image.source=https://$(GO_MODULE) \ - org.opencontainers.image.version=$(DOCKER_TAG) \ - org.opencontainers.image.revision=$(REVISION) - -# In non-Linux systems, use Docker to build FIPS-compliant binaries. -OS := $(shell uname) -ifeq ($(OS),Darwin) -BUILD_FIPS_IN_DOCKER ?= true -endif - -export ARCH := $(shell uname -m) -ifeq ($(ARCH),x86_64) -export ARCH := amd64 -endif diff --git a/go.mod b/go.mod deleted file mode 100644 index 6730ce9..0000000 --- a/go.mod +++ /dev/null @@ -1,94 +0,0 @@ -module github.com/tetrateio/authservice-go - -go 1.21.8 - -require ( - github.com/alicebob/miniredis/v2 v2.31.1 - github.com/envoyproxy/go-control-plane v0.12.0 - github.com/envoyproxy/protoc-gen-validate v1.0.4 - github.com/go-logr/logr v1.4.1 - github.com/lestrrat-go/httprc v1.0.5 - github.com/lestrrat-go/jwx/v2 v2.0.21 - github.com/redis/go-redis/v9 v9.5.1 - github.com/stretchr/testify v1.9.0 - github.com/tetratelabs/log v0.2.3 - github.com/tetratelabs/run v0.3.0 - github.com/tetratelabs/telemetry v0.8.2 - golang.org/x/net v0.22.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 - google.golang.org/grpc v1.62.1 - google.golang.org/protobuf v1.33.0 - gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.29.2 - k8s.io/apimachinery v0.29.2 - k8s.io/client-go v0.29.2 - sigs.k8s.io/controller-runtime v0.17.2 -) - -require ( - cloud.google.com/go/compute v1.25.0 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cncf/xds/go v0.0.0-20240306133729-91a88dc4e959 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/emicklei/go-restful/v3 v3.11.3 // indirect - github.com/evanphx/json-patch v5.9.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.9.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-openapi/jsonpointer v0.20.3 // indirect - github.com/go-openapi/jsonreference v0.20.5 // indirect - github.com/go-openapi/swag v0.22.10 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/lestrrat-go/blackmagic v1.0.2 // indirect - github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/option v1.0.1 // indirect - github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.19.0 // indirect - github.com/prometheus/client_model v0.6.0 // indirect - github.com/prometheus/common v0.50.0 // indirect - github.com/prometheus/procfs v0.13.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/segmentio/asm v1.2.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/yuin/gopher-lua v1.1.1 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect - golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.5.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.29.2 // indirect - k8s.io/component-base v0.29.2 // indirect - k8s.io/klog/v2 v2.120.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index be5cedf..0000000 --- a/go.sum +++ /dev/null @@ -1,265 +0,0 @@ -cloud.google.com/go/compute v1.25.0 h1:H1/4SqSUhjPFE7L5ddzHOfY2bCAvjwNRZPNl6Ni5oYU= -cloud.google.com/go/compute v1.25.0/go.mod h1:GR7F0ZPZH8EhChlMo9FkLd7eUTwEymjqQagxzilIxIE= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= -github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y= -github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cncf/xds/go v0.0.0-20240306133729-91a88dc4e959 h1:MIhowmP3TU3Zxnp4PILwgBDTplWiYChEPnJ78PAdJ1o= -github.com/cncf/xds/go v0.0.0-20240306133729-91a88dc4e959/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/emicklei/go-restful/v3 v3.11.3 h1:yagOQz/38xJmcNeZJtrUcKjkHRltIaIFXKWeG1SkWGE= -github.com/emicklei/go-restful/v3 v3.11.3/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= -github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= -github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= -github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= -github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= -github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= -github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= -github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.20.3 h1:jykzYWS/kyGtsHfRt6aV8JTB9pcQAXPIA7qlZ5aRlyk= -github.com/go-openapi/jsonpointer v0.20.3/go.mod h1:c7l0rjoouAuIxCm8v/JWKRgMjDG/+/7UBWsXMrv6PsM= -github.com/go-openapi/jsonreference v0.20.5 h1:hutI+cQI+HbSQaIGSfsBsYI0pHk+CATf8Fk5gCSj0yI= -github.com/go-openapi/jsonreference v0.20.5/go.mod h1:thAqAp31UABtI+FQGKAQfmv7DbFpKNUlva2UPCxKu2Y= -github.com/go-openapi/swag v0.22.10 h1:4y86NVn7Z2yYd6pfS4Z+Nyh3aAUL3Nul+LMbhFKy0gA= -github.com/go-openapi/swag v0.22.10/go.mod h1:Cnn8BYtRlx6BNE3DPN86f/xkapGIcLWzh3CLEb4C1jI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= -github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= -github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk= -github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= -github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0= -github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM= -github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= -github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= -github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= -github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= -github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= -github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= -github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= -github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= -github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ= -github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ= -github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= -github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= -github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= -github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tetratelabs/log v0.2.3 h1:a+0omnV1Y/4PyOSbLXHsba1guq8MZmmSvq+n9YdNuLY= -github.com/tetratelabs/log v0.2.3/go.mod h1:KCZYD/fW3ZlhTWgmGBvsiniEk6TUZEL/Eb6W928+ymM= -github.com/tetratelabs/run v0.3.0 h1:Oc3Ae4I9JRRdslaTV73aiZvkKoPd/02iqCpyVk5QKcA= -github.com/tetratelabs/run v0.3.0/go.mod h1:WYzjp0V1JXlD3n0uZyC/hzRuJtrl7TaSrfQcXs5QM1A= -github.com/tetratelabs/telemetry v0.8.2 h1:VXwSVpfX1yRMo6UdhsLP80GuPzavVWuoJrVM+2lMOSk= -github.com/tetratelabs/telemetry v0.8.2/go.mod h1:jDUcf1A2u4F5V1io5RdipM/bKz/hFCsx/RAgGopC37s= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 h1:IR+hp6ypxjH24bkMfEJ0yHR21+gwPWdV+/IBrPQyn3k= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= -google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= -k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= -k8s.io/apiextensions-apiserver v0.29.2 h1:UK3xB5lOWSnhaCk0RFZ0LUacPZz9RY4wi/yt2Iu+btg= -k8s.io/apiextensions-apiserver v0.29.2/go.mod h1:aLfYjpA5p3OwtqNXQFkhJ56TB+spV8Gc4wfMhUA3/b8= -k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= -k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= -k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= -k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= -k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= -k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= -k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= -sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/authz/handler.go b/internal/authz/handler.go deleted file mode 100644 index e699bde..0000000 --- a/internal/authz/handler.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package authz - -import ( - "context" - - envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" -) - -// Handler is an interface for handling authorization requests. -type Handler interface { - // Process a CheckRequest and populate a CheckResponse. - Process(ctx context.Context, req *envoy.CheckRequest, resp *envoy.CheckResponse) error -} diff --git a/internal/authz/mock.go b/internal/authz/mock.go deleted file mode 100644 index a1691a6..0000000 --- a/internal/authz/mock.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package authz - -import ( - "context" - - envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "github.com/tetratelabs/telemetry" - "google.golang.org/genproto/googleapis/rpc/status" - "google.golang.org/grpc/codes" - - mockv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/mock" - "github.com/tetrateio/authservice-go/internal" -) - -var _ Handler = (*mockHandler)(nil) - -// mockHandler handler is an implementation of the Handler interface. -type mockHandler struct { - log telemetry.Logger - config *mockv1.MockConfig -} - -// NewMockHandler creates a new Mock implementation of the Handler interface. -func NewMockHandler(cfg *mockv1.MockConfig) Handler { - return &mockHandler{ - log: internal.Logger(internal.Authz).With("type", "mockHandler"), - config: cfg, - } -} - -// Process a CheckRequest and populate a CheckResponse according to the mockHandler configuration. -func (m *mockHandler) Process(ctx context.Context, _ *envoy.CheckRequest, resp *envoy.CheckResponse) error { - log := m.log.Context(ctx) - - code := codes.PermissionDenied - if m.config.GetAllow() { - code = codes.OK - } - - log.Debug("process", "status", code.String()) - resp.Status = &status.Status{Code: int32(code)} - return nil -} diff --git a/internal/authz/mock_test.go b/internal/authz/mock_test.go deleted file mode 100644 index 9be0309..0000000 --- a/internal/authz/mock_test.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package authz - -import ( - "context" - "testing" - - envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "github.com/stretchr/testify/require" - "google.golang.org/grpc/codes" - - mockv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/mock" -) - -func TestProcessMock(t *testing.T) { - tests := []struct { - name string - allow bool - want codes.Code - }{ - {"allow", true, codes.OK}, - {"deny", false, codes.PermissionDenied}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var ( - m = NewMockHandler(&mockv1.MockConfig{Allow: tt.allow}) - req = &envoy.CheckRequest{} - resp = &envoy.CheckResponse{} - ) - err := m.Process(context.Background(), req, resp) - require.NoError(t, err) - require.Equal(t, int32(tt.want), resp.Status.Code) - }) - } -} diff --git a/internal/authz/oidc.go b/internal/authz/oidc.go deleted file mode 100644 index 53ff57b..0000000 --- a/internal/authz/oidc.go +++ /dev/null @@ -1,841 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package authz - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/tetratelabs/telemetry" - "google.golang.org/genproto/googleapis/rpc/status" - "google.golang.org/grpc/codes" - - oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" - "github.com/tetrateio/authservice-go/internal" - inthttp "github.com/tetrateio/authservice-go/internal/http" - "github.com/tetrateio/authservice-go/internal/oidc" -) - -var ( - _ Handler = (*oidcHandler)(nil) - - // standardResponseHeaders are the headers that are added to every denied response. - standardResponseHeaders = []*corev3.HeaderValueOption{ - {Header: &corev3.HeaderValue{Key: inthttp.HeaderCacheControl, Value: inthttp.HeaderCacheControlNoCache}}, - {Header: &corev3.HeaderValue{Key: inthttp.HeaderPragma, Value: inthttp.HeaderPragmaNoCache}}, - } -) - -// oidc handler is an implementation of the Handler interface that implements -// the OpenID connect protocol. -type oidcHandler struct { - log telemetry.Logger - config *oidcv1.OIDCConfig - tlsPool internal.TLSConfigPool - jwks oidc.JWKSProvider - sessions oidc.SessionStoreFactory - sessionGen oidc.SessionGenerator - clock oidc.Clock - httpClient *http.Client -} - -// NewOIDCHandler creates a new OIDC implementation of the Handler interface. -func NewOIDCHandler(cfg *oidcv1.OIDCConfig, tlsPool internal.TLSConfigPool, jwks oidc.JWKSProvider, - sessions oidc.SessionStoreFactory, clock oidc.Clock, sessionGen oidc.SessionGenerator) (Handler, error) { - - client, err := getHTTPClient(cfg, tlsPool) - if err != nil { - return nil, err - } - - if err := loadWellKnownConfig(client, cfg); err != nil { - return nil, err - } - - return &oidcHandler{ - log: internal.Logger(internal.Authz).With("type", "oidc"), - config: cfg, - tlsPool: tlsPool, - jwks: jwks, - sessions: sessions, - clock: clock, - sessionGen: sessionGen, - httpClient: client, - }, nil -} - -func getHTTPClient(cfg *oidcv1.OIDCConfig, tlsPool internal.TLSConfigPool) (*http.Client, error) { - transport := http.DefaultTransport.(*http.Transport).Clone() - - var err error - if transport.TLSClientConfig, err = tlsPool.LoadTLSConfig(cfg); err != nil { - return nil, err - } - - if cfg.ProxyUri != "" { - // config validation ensures that the proxy uri is valid - proxyURL, _ := url.Parse(cfg.ProxyUri) - transport.Proxy = http.ProxyURL(proxyURL) - } - - return &http.Client{Transport: transport}, nil -} - -// Process a CheckRequest and populate a CheckResponse according to the mockHandler configuration. -func (o *oidcHandler) Process(ctx context.Context, req *envoy.CheckRequest, resp *envoy.CheckResponse) error { - log := o.log.Context(ctx) - log.Debug("process request", - "source-principal", req.GetAttributes().GetSource().GetPrincipal(), - "source-address", req.GetAttributes().GetSource().GetAddress().GetSocketAddress().GetAddress(), - "destination-principal", req.GetAttributes().GetDestination().GetPrincipal(), - "destination-address", req.GetAttributes().GetDestination().GetAddress().GetSocketAddress().GetAddress(), - ) - defer func() { - log.Debug("process result", "allow", resp.GetDeniedResponse() == nil, "status", codes.Code(resp.Status.GetCode()).String()) - }() - - if req.GetAttributes().GetRequest().GetHttp() == nil { - log.Info("missing http in the request") - setDenyResponse(resp, newDenyResponse(), codes.InvalidArgument) - return nil - } - - headers := req.GetAttributes().GetRequest().GetHttp().GetHeaders() - sessionID := getSessionIDFromCookie(log, headers, o.config) - - // If the request is for the configured logout path, - // then logout and redirect to the configured logout redirect uri. - if matchesLogoutPath(log, o.config, req.GetAttributes().GetRequest().GetHttp()) { - log.Info("handling logout request") - if sessionID != "" { - log.Info("removing session from session store during logout", "session-id", sessionID) - store := o.sessions.Get(o.config) - if err := store.RemoveSession(ctx, sessionID); err != nil { - log.Error("error removing session", err) - setDenyResponse(resp, newSessionErrorResponse(), codes.Unauthenticated) - return nil - } - } - log.Info("logout complete. Redirecting to logout redirect uri") - deny := newDenyResponse() - // add IDP logout location - setRedirect(deny, o.config.GetLogout().GetRedirectUri()) - // add the set-cookie header to delete the session_id cookie - setSetCookieHeader(deny, generateSetCookieHeader(getCookieName(o.config), "deleted", 0)) - setDenyResponse(resp, deny, codes.Unauthenticated) - return nil - } - - // If the request does not have a session_id cookie, - // then generate a session id, put it in a header, and redirect for login. - if sessionID == "" { - log.Info("no session cookie detected. Generating new session and sending user to re-authenticate.") - o.redirectToIDP(ctx, log, resp, req.GetAttributes().GetRequest().GetHttp(), "") - return nil - } - - log = log.With("session-id", sessionID) - - // If the request path is the callback for receiving the authorization code, - // has a session id then exchange it for tokens and redirects end-user back to - // their originally requested URL. - if matchesCallbackPath(log, o.config, req.GetAttributes().GetRequest().GetHttp()) { - log.Debug("handling callback request") - o.retrieveTokens(ctx, log, req, resp, sessionID) - return nil - } - - log.Debug("attempting session retrieval") - - store := o.sessions.Get(o.config) - tokenResponse, err := store.GetTokenResponse(ctx, sessionID) - if err != nil { - log.Error("error retrieving tokens from session store", err) - setDenyResponse(resp, newSessionErrorResponse(), codes.Unauthenticated) - return nil - } - - // If the user has a session_id cookie but there are no required tokens in the - // session store associated with it, then redirect for login. - if tokenResponse == nil { - log.Info("required tokens are not present. Sending user to re-authenticate.") - o.redirectToIDP(ctx, log, resp, req.GetAttributes().GetRequest().GetHttp(), sessionID) - return nil - } - - // If both ID & Access token are still unexpired, - // then allow the request to proceed (no need to intervene). - log.Debug("checking tokens expiration") - expired, err := o.areRequiredTokensExpired(tokenResponse) - if err != nil { - log.Error("error checking token expiration", err) - setDenyResponse(resp, newDenyResponse(), codes.Internal) - return nil - } - if !expired { - log.Info("tokens not expired. Allowing request to proceed.") - o.allowResponse(resp, tokenResponse) - return nil - } - - // If tokens are expired, then: - - // If there is no refresh token, - // then direct the request to the identity provider for authentication - if tokenResponse.RefreshToken == "" { - log.Info("a token was expired, but session did not contain a refresh token. Sending user to re-authenticate.") - o.redirectToIDP(ctx, log, resp, req.GetAttributes().GetRequest().GetHttp(), sessionID) - return nil - } - - // If the user has an unexpired refresh token then use it to request a fresh - // token_response. If successful, allow the request to proceed. If - // unsuccessful, redirect for login. - log.Debug("attempting token refresh") - refreshedTokens := o.refreshToken(ctx, log, tokenResponse, tokenResponse.RefreshToken, sessionID) - if refreshedTokens == nil { - log.Info("token refresh failed. Sending user to re-authenticate.") - o.redirectToIDP(ctx, log, resp, req.GetAttributes().GetRequest().GetHttp(), sessionID) - return nil - } - if err := store.SetTokenResponse(ctx, sessionID, refreshedTokens); err != nil { - log.Error("error saving refreshed tokens to session store", err) - setDenyResponse(resp, newSessionErrorResponse(), codes.Unauthenticated) - return nil - } - - log.Info("token refresh successful. Allowing request to proceed.") - o.allowResponse(resp, refreshedTokens) - return nil -} - -// redirectToIDP redirects the request to the Identity Provider for authentication. -// It sets the appropriate headers and status code in the CheckResponse to notify about the redirect. -// It also removes the session id, if given, from the session store. -func (o *oidcHandler) redirectToIDP(ctx context.Context, log telemetry.Logger, - resp *envoy.CheckResponse, httpRequest *envoy.AttributeContext_HttpRequest, oldSessionID string) { - - store := o.sessions.Get(o.config) - if oldSessionID != "" { - // remove old session and regenerate session_id to prevent session fixation attacks - if err := store.RemoveSession(ctx, oldSessionID); err != nil { - log.Error("error removing old session", err) - setDenyResponse(resp, newSessionErrorResponse(), codes.Unauthenticated) - return - } - } - - var ( - sessionID = o.sessionGen.GenerateSessionID() - nonce = o.sessionGen.GenerateNonce() - state = o.sessionGen.GenerateState() - ) - - // Store the authorization state - requestedURL := httpRequest.GetScheme() + "://" + httpRequest.GetHost() + httpRequest.GetPath() - if httpRequest.GetQuery() != "" { - requestedURL += "?" + httpRequest.GetQuery() - } - if err := store.SetAuthorizationState(ctx, sessionID, &oidc.AuthorizationState{ - State: state, - Nonce: nonce, - RequestedURL: requestedURL, - }); err != nil { - log.Error("error storing the new authorization state", err) - setDenyResponse(resp, newSessionErrorResponse(), codes.Unauthenticated) - return - } - - // Generate the redirect URL - query := url.Values{ - "response_type": []string{"code"}, - "client_id": []string{o.config.GetClientId()}, - "redirect_uri": []string{o.config.GetCallbackUri()}, - "scope": []string{strings.Join(o.config.GetScopes(), " ")}, - "state": []string{state}, - "nonce": []string{nonce}, - } - redirectURL := o.config.GetAuthorizationUri() + "?" + query.Encode() - - // Generate denied response with redirect headers - deny := newDenyResponse() - setRedirect(deny, redirectURL) - - // add the set-cookie header - cookieName := getCookieName(o.config) - setSetCookieHeader(deny, generateSetCookieHeader(cookieName, sessionID, -1)) - setDenyResponse(resp, deny, codes.Unauthenticated) -} - -// retrieveTokens retrieves the tokens from the Identity Provider and redirects the user back to the originally requested URL. -func (o *oidcHandler) retrieveTokens(ctx context.Context, log telemetry.Logger, req *envoy.CheckRequest, resp *envoy.CheckResponse, sessionID string) { - store := o.sessions.Get(o.config) - - _, query, _ := inthttp.GetPathQueryFragment(req.GetAttributes().GetRequest().GetHttp().GetPath()) - queryParams, err := url.ParseQuery(query) - switch { - case err != nil: - log.Error("error parsing query", err, "query", query) - setDenyResponse(resp, newDenyResponse(), codes.InvalidArgument) - return - case len(queryParams) == 0: - log.Info("form data is invalid, no query parameters found", "query", query) - setDenyResponse(resp, newDenyResponse(), codes.InvalidArgument) - return - } - - stateFromReq := queryParams.Get("state") - codeFromReq := queryParams.Get("code") - if stateFromReq == "" || codeFromReq == "" { - log.Info("form data is invalid, missing state or code", "state", stateFromReq, "code", codeFromReq) - setDenyResponse(resp, newDenyResponse(), codes.InvalidArgument) - return - } - - stateFromStore, err := store.GetAuthorizationState(ctx, sessionID) - if err != nil { - log.Error("error retrieving authorization state from session store", err) - setDenyResponse(resp, newSessionErrorResponse(), codes.Unauthenticated) - return - } - - if stateFromStore == nil { - log.Info("missing state, nonce, and original url requested by user in the store. Cannot redirect.") - deny := newDenyResponse() - deny.Body = "Oops, your session has expired. Please try again." - deny.Status = &typev3.HttpStatus{Code: typev3.StatusCode_BadRequest} - setDenyResponse(resp, deny, codes.Unauthenticated) - return - } - - // compare the state from the request with the state from the store - if stateFromReq != stateFromStore.State { - log.Info("state from request does not match state from store", "state-from-request", stateFromReq, "state-from-store", stateFromStore.State) - setDenyResponse(resp, newDenyResponse(), codes.InvalidArgument) - return - } - - // build body - form := url.Values{ - "grant_type": []string{"authorization_code"}, - "code": []string{codeFromReq}, - "redirect_uri": []string{o.config.GetCallbackUri()}, - } - - // build headers - headers := http.Header{ - inthttp.HeaderContentType: []string{inthttp.HeaderContentTypeFormURLEncoded}, - inthttp.HeaderAuthorization: []string{inthttp.BasicAuthHeader(o.config.GetClientId(), o.config.GetClientSecret())}, - } - - log.Info("performing request to retrieve new tokens") - bodyTokens, errCode := performIDPRequest(log, o.httpClient, o.config.GetTokenUri(), form, headers) - if errCode != codes.OK { - setDenyResponse(resp, newDenyResponse(), errCode) - return - } - - // validate IDP tokens response - if !isValidIDPNewTokensResponse(log, o.config, bodyTokens) { - setDenyResponse(resp, newDenyResponse(), codes.InvalidArgument) - return - } - - // validate ID token - if ok, errCode := o.isValidIDToken(ctx, log, bodyTokens.IDToken, stateFromStore.Nonce, true); !ok { - setDenyResponse(resp, newDenyResponse(), errCode) - return - } - - if err := store.ClearAuthorizationState(ctx, sessionID); err != nil { - log.Error("error clearing authorization state", err) - setDenyResponse(resp, newSessionErrorResponse(), codes.Unauthenticated) - return - } - - // Knock 5 seconds off the expiry time to take into account the time it may - // have taken to retrieve the token. - expiresIn := time.Duration(bodyTokens.ExpiresIn)*time.Second - 5 - accessTokenExpiration := o.clock.Now().Add(expiresIn) - - log.Debug("saving tokens to session store") - if err := store.SetTokenResponse(ctx, sessionID, &oidc.TokenResponse{ - IDToken: bodyTokens.IDToken, - AccessToken: bodyTokens.AccessToken, - RefreshToken: bodyTokens.RefreshToken, - AccessTokenExpiresAt: accessTokenExpiration, - }); err != nil { - log.Error("error saving tokens to session store", err) - setDenyResponse(resp, newSessionErrorResponse(), codes.Unauthenticated) - return - } - log.Debug("tokens retrieved successfully") - - deny := newDenyResponse() - deny.Status = &typev3.HttpStatus{Code: typev3.StatusCode_Found} - deny.Headers = append(deny.Headers, &corev3.HeaderValueOption{ - Header: &corev3.HeaderValue{Key: inthttp.HeaderLocation, Value: stateFromStore.RequestedURL}, - }) - setDenyResponse(resp, deny, codes.Unauthenticated) -} - -// refreshToken retrieves new tokens from the Identity Provider using the given refresh token. -func (o *oidcHandler) refreshToken(ctx context.Context, log telemetry.Logger, expiredTokens *oidc.TokenResponse, token, sessionID string) *oidc.TokenResponse { - store := o.sessions.Get(o.config) - - form := url.Values{ - "grant_type": []string{"refresh_token"}, - "refresh_token": []string{token}, - "client_id": []string{o.config.GetClientId()}, - "client_secret": []string{o.config.GetClientSecret()}, - // according to this link, omitting the `scope` param should return new - // tokens with the previously requested `scope` - // https://www.oauth.com/oauth2-servers/access-tokens/refreshing-access-tokens/ - } - - // build headers - headers := http.Header{ - inthttp.HeaderContentType: []string{inthttp.HeaderContentTypeFormURLEncoded}, - } - - log.Info("performing request to refresh access token") - bodyTokens, errCode := performIDPRequest(log, o.httpClient, o.config.GetTokenUri(), form, headers) - - if errCode != codes.OK { - return nil - } - - // validate IDP tokens response - if !isValidIDPRefreshTokenResponse(log, bodyTokens) { - //setDenyResponse(resp, newDenyResponse(), codes.InvalidArgument) - return nil - } - - // merge the new tokens with the stored ones - newTokenResponse := &oidc.TokenResponse{} - - _, err := oidc.ParseToken(bodyTokens.IDToken) - if err != nil { - log.Error("error parsing new id token, using the old one", err) - newTokenResponse.IDToken = expiredTokens.IDToken - } else { - log.Debug("updating id token") - newTokenResponse.IDToken = bodyTokens.IDToken - } - - if bodyTokens.AccessToken != "" { - log.Debug("updating access token") - newTokenResponse.AccessToken = bodyTokens.AccessToken - } else { - newTokenResponse.AccessToken = expiredTokens.AccessToken - } - - if bodyTokens.RefreshToken != "" { - log.Debug("updating refresh token") - newTokenResponse.RefreshToken = bodyTokens.RefreshToken - } else { - newTokenResponse.RefreshToken = expiredTokens.RefreshToken - } - - if bodyTokens.ExpiresIn > 0 { - log.Debug("updating access token expiration") - // Knock 5 seconds off the expiry time to take into account the time it may - // have taken to retrieve the token. - expiresIn := time.Duration(bodyTokens.ExpiresIn)*time.Second - 5 - newTokenResponse.AccessTokenExpiresAt = o.clock.Now().Add(expiresIn) - } else { - newTokenResponse.AccessTokenExpiresAt = expiredTokens.AccessTokenExpiresAt - } - - stateFromStore, err := store.GetAuthorizationState(ctx, sessionID) - if err != nil { - log.Error("error retrieving authorization state from session store", err) - return nil - } - var expectedNonce string - if stateFromStore != nil { - expectedNonce = stateFromStore.Nonce - } - - // validate the id token - if ok, _ := o.isValidIDToken(context.Background(), log, newTokenResponse.IDToken, expectedNonce, false); !ok { - return nil - } - - return newTokenResponse -} - -// idpTokensResponse is the response from the Identity Provider when requesting tokens. -type idpTokensResponse struct { - IDToken string `json:"id_token"` - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - ExpiresIn int `json:"expires_in"` - TokenType string `json:"token_type"` - Scope string `json:"scope"` - DeviceSecret string `json:"device_secret"` -} - -// performIDPRequest performs a request to the Identity Provider to retrieve tokens. -func performIDPRequest(log telemetry.Logger, client *http.Client, uri string, form url.Values, headers http.Header) (*idpTokensResponse, codes.Code) { - oidcReq, err := http.NewRequest("POST", uri, strings.NewReader(form.Encode())) - if err != nil { - log.Error("error creating tokens request to OIDC", err) - return nil, codes.Internal - } - oidcReq.Header = headers - - oidcResp, err := client.Do(oidcReq) - if err != nil { - log.Error("error performing tokens request to OIDC", err) - return nil, codes.Internal - } - - if oidcResp.StatusCode != http.StatusOK { - log.Info("OIDC server returned non-200 status code", "status-code", oidcResp.StatusCode, "url", oidcReq.URL.String()) - return nil, codes.Unknown - } - - respBody, err := io.ReadAll(oidcResp.Body) - _ = oidcResp.Body.Close() - if err != nil { - log.Error("error reading tokens response", err) - return nil, codes.Internal - } - - bodyTokens := &idpTokensResponse{} - err = json.Unmarshal(respBody, &bodyTokens) - if err != nil { - log.Error("error unmarshalling tokens response", err) - return nil, codes.Internal - } - - return bodyTokens, codes.OK -} - -// isValidIDPNewTokensResponse checks if the response from the Identity Provider is valid according to the OpenID Connect specification. -// https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse -func isValidIDPNewTokensResponse(log telemetry.Logger, config *oidcv1.OIDCConfig, tokenResponse *idpTokensResponse) bool { - // token_type must be Bearer - if tokenResponse.TokenType != "Bearer" { - log.Info("token type is not Bearer in token response", "token-type", tokenResponse.TokenType) - return false - } - - // expires_in must be a positive value - if tokenResponse.ExpiresIn < 0 { - log.Info("expires_in is not a positive value in token response", "expires-in", tokenResponse.ExpiresIn) - return false - } - - // If access_token forwarding is configured but there is not an access token - // in the token response then there is a problem - if config.GetAccessToken() != nil && tokenResponse.AccessToken == "" { - log.Info("access token forwarding is configured but no access token was returned") - return false - } - - return true -} - -// isValidIDPRefreshTokenResponse checks if the response from the Identity Provider is valid according to the OpenID Connect specification. -// https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse -func isValidIDPRefreshTokenResponse(log telemetry.Logger, tokenResponse *idpTokensResponse) bool { - // token_type must be Bearer - if tokenResponse.TokenType != "Bearer" { - log.Info("token type is not Bearer in token response", "token-type", tokenResponse.TokenType) - return false - } - - // expires_in must be a positive value - if tokenResponse.ExpiresIn < 0 { - log.Info("expires_in is not a positive value in token response", "expires-in", tokenResponse.ExpiresIn) - return false - } - - return true -} - -// isValidIDToken checks if the id token is valid according to the OpenID Connect specification. -// It checks the nonce, audience, and verifies the signature with the fetched jwks. -// It returns a boolean indicating if the token is valid and a code indicating the reason if it is not. -// If the nonce is not required, it will only check the expectedNonce against the token's nonce if it is present, as OIDC spec defines. -func (o *oidcHandler) isValidIDToken(ctx context.Context, log telemetry.Logger, idTokenString, expectedNonce string, isNonceRequired bool) (bool, codes.Code) { - idToken, err := oidc.ParseToken(idTokenString) - if err != nil { - log.Error("error parsing id token", err) - return false, codes.Internal - } - - oidcNonce, ok := idToken.Get("nonce") - if !ok && isNonceRequired { - log.Info("id token does not have nonce claim") - return false, codes.InvalidArgument - } - if ok { - tokenNonce := oidcNonce.(string) - // if nonce is not required, both token and expected nonce must be present to perform the check - if (isNonceRequired || tokenNonce != "" && expectedNonce != "") && tokenNonce != expectedNonce { - log.Info("id token nonce does not match", "nonce-from-id-token", oidcNonce, "nonce-from-store", expectedNonce) - return false, codes.InvalidArgument - } - } - - var audMatches bool - for _, a := range idToken.Audience() { - if a == o.config.GetClientId() { - audMatches = true - break - } - } - if !audMatches { - log.Info("id token audience does not match", "aud-from-id-token", idToken.Audience(), "aud-from-config", o.config.GetClientId()) - return false, codes.InvalidArgument - } - - jwtSet, err := o.jwks.Get(ctx, o.config) - if err != nil { - log.Error("error fetching jwks", err) - return false, codes.Internal - } - - if _, err := jws.Verify([]byte(idTokenString), jws.WithKeySet(jwtSet)); err != nil { - log.Error("error verifying id token with fetched jwks", err) - return false, codes.Internal - } - - return true, codes.OK -} - -// newDenyResponse creates a new DeniedHttpResponse with the standard headers. -func newDenyResponse() *envoy.DeniedHttpResponse { - deny := &envoy.DeniedHttpResponse{} - deny.Headers = append(deny.Headers, standardResponseHeaders...) - return deny -} - -// newSessionErrorResponse creates a new DeniedHttpResponse with the proper data to notify about a session error. -func newSessionErrorResponse() *envoy.DeniedHttpResponse { - return &envoy.DeniedHttpResponse{ - Body: "There was an error accessing your session data. Try again later.", - } -} - -// setDenyResponse populates the CheckResponse as a Denied response with the given code and headers. -func setDenyResponse(resp *envoy.CheckResponse, deny *envoy.DeniedHttpResponse, code codes.Code) { - resp.HttpResponse = &envoy.CheckResponse_DeniedResponse{DeniedResponse: deny} - resp.Status = &status.Status{Code: int32(code)} -} - -// setRedirect populates the DeniedHttpResponse with the given location and a 302 status code. -func setRedirect(deny *envoy.DeniedHttpResponse, location string) { - deny.Status = &typev3.HttpStatus{Code: typev3.StatusCode_Found} - deny.Headers = append(deny.Headers, &corev3.HeaderValueOption{ - Header: &corev3.HeaderValue{Key: inthttp.HeaderLocation, Value: location}, - }) -} - -// setSetCookieHeader populates the DeniedHttpResponse with the given cookie. -func setSetCookieHeader(deny *envoy.DeniedHttpResponse, cookie string) { - deny.Headers = append(deny.Headers, &corev3.HeaderValueOption{ - Header: &corev3.HeaderValue{Key: inthttp.HeaderSetCookie, Value: cookie}, - }) -} - -// allowResponse populates the CheckResponse as an OK response with the required tokens. -func (o *oidcHandler) allowResponse(resp *envoy.CheckResponse, tokens *oidc.TokenResponse) { - ok := resp.GetOkResponse() - if ok == nil { - ok = &envoy.OkHttpResponse{} - } - - for key, value := range o.encodeTokensToHeaders(tokens) { - ok.Headers = append(ok.Headers, &corev3.HeaderValueOption{Header: &corev3.HeaderValue{Key: key, Value: value}}) - } - - resp.HttpResponse = &envoy.CheckResponse_OkResponse{OkResponse: ok} - resp.Status = &status.Status{Code: int32(codes.OK)} -} - -// matchesCallbackPath checks if the request matches the configured callback uri. -// Request done by the IDP directly to the authservice to exchange the authorization code for tokens. -func matchesCallbackPath(log telemetry.Logger, config *oidcv1.OIDCConfig, httpReq *envoy.AttributeContext_HttpRequest) bool { - reqFullPath := httpReq.GetPath() - reqHost := httpReq.GetHost() - reqPath, _, _ := inthttp.GetPathQueryFragment(reqFullPath) - - // no need to handle the error since config validation already checks for this - confURI, _ := url.Parse(config.GetCallbackUri()) - confPort := confURI.Port() - confHost := confURI.Hostname() - confScheme := confURI.Scheme - confPath := confURI.Path - confHostAndPort := confHost - if confPort != "" { - confHostAndPort += ":" + confPort - } - - hostMatches := reqHost == confHostAndPort || - (confScheme == "https" && confPort == "443" && reqHost == confHost) || // default https port - (confScheme == "http" && confPort == "80" && reqHost == confHost) // default http port - - pathMatches := reqPath == confPath - - if pathMatches && hostMatches { - log.Debug("request matches configured callback uri") - return true - } - return false -} - -// matchesLogoutPath checks if the request matches the configured logout uri. -// Request done by the end-user to log out. -func matchesLogoutPath(log telemetry.Logger, config *oidcv1.OIDCConfig, httpReq *envoy.AttributeContext_HttpRequest) bool { - if config.GetLogout() == nil { - return false - } - - reqPath, _, _ := inthttp.GetPathQueryFragment(httpReq.GetPath()) - confPath := config.GetLogout().GetPath() - - if reqPath == confPath { - log.Debug("request matches configured logout uri") - return true - } - return false -} - -// encodeTokensToHeaders encodes the tokens to the headers according to the configuration. -func (o *oidcHandler) encodeTokensToHeaders(tokens *oidc.TokenResponse) map[string]string { - headers := make(map[string]string) - - // Always add the ID token to the headers - headers[o.config.GetIdToken().GetHeader()] = encodeHeaderValue(o.config.IdToken.GetPreamble(), tokens.IDToken) - - if o.config.GetAccessToken() == nil || tokens.AccessToken == "" { - return headers - } - - // If there is an access token and config enables it, add it to the headers - headers[o.config.GetAccessToken().GetHeader()] = encodeHeaderValue(o.config.GetAccessToken().GetPreamble(), tokens.AccessToken) - - return headers -} - -// encodeHeaderValue encodes the value with the given preamble, if any -func encodeHeaderValue(preamble string, value string) string { - if preamble != "" { - return preamble + " " + value - } - return value -} - -// areRequiredTokensExpired checks if the required tokens are expired. -func (o *oidcHandler) areRequiredTokensExpired(tokens *oidc.TokenResponse) (bool, error) { - idToken, err := tokens.ParseIDToken() - if err != nil { - return false, fmt.Errorf("parsing id token: %w", err) - } - - if idToken.Expiration().Before(o.clock.Now()) { - return true, nil - } - if o.config.GetAccessToken() != nil && tokens.AccessToken != "" && !tokens.AccessTokenExpiresAt.IsZero() { - return tokens.AccessTokenExpiresAt.Before(o.clock.Now()), nil - } - return false, nil -} - -// generateSetCookieHeader generates the Set-Cookie header value with the given cookie name, value, and timeout. -func generateSetCookieHeader(cookieName, cookieValue string, timeout time.Duration) string { - directives := getCookieDirectives(timeout) - return inthttp.EncodeCookieHeader(cookieName, cookieValue, directives) -} - -// getCookieDirectives returns the directives to use in the Set-Cookie header depending on the timeout. -func getCookieDirectives(timeout time.Duration) []string { - directives := []string{inthttp.HeaderSetCookieHTTPOnly, inthttp.HeaderSetCookieSecure, inthttp.HeaderSetCookieSameSiteLax, "Path=/"} - if timeout >= 0 { - directives = append(directives, fmt.Sprintf("%s=%d", inthttp.HeaderSetCookieMaxAge, int(timeout.Seconds()))) - } - return directives -} - -// getSessionIDFromCookie retrieves the session id from the cookie in the headers. -func getSessionIDFromCookie(log telemetry.Logger, headers map[string]string, config *oidcv1.OIDCConfig) string { - cookieName := getCookieName(config) - - value := headers[inthttp.HeaderCookie] - if value == "" { - log.Info("session id cookie is missing", "cookie-name", cookieName) - return "" - } - - for name, value := range inthttp.DecodeCookiesHeader(value) { - if name == cookieName { - return value - } - } - - log.Info("session id cookie is missing", "cookie-name", cookieName) - return "" -} - -const ( - prefixCookieName = "__Host-" - suffixCookieName = "-authservice-session-id-cookie" - defaultCookieName = "__Host-authservice-session-id-cookie" -) - -// getCookieName returns the cookie name to use for the session id. -func getCookieName(config *oidcv1.OIDCConfig) string { - if prefix := config.GetCookieNamePrefix(); prefix != "" { - return prefixCookieName + prefix + suffixCookieName - } - return defaultCookieName -} - -// loadWellKnownConfig loads the OIDC well-known configuration into the given OIDCConfig. -func loadWellKnownConfig(client *http.Client, cfg *oidcv1.OIDCConfig) error { - if cfg.GetConfigurationUri() == "" { - return nil - } - - wellKnownConfig, err := oidc.GetWellKnownConfig(client, cfg.GetConfigurationUri()) - if err != nil { - return err - } - - cfg.AuthorizationUri = wellKnownConfig.AuthorizationEndpoint - cfg.TokenUri = wellKnownConfig.TokenEndpoint - if cfg.GetJwksFetcher() == nil { - cfg.JwksConfig = &oidcv1.OIDCConfig_JwksFetcher{ - JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{}, - } - } - cfg.GetJwksFetcher().JwksUri = wellKnownConfig.JWKSURL - - return nil -} diff --git a/internal/authz/oidc_test.go b/internal/authz/oidc_test.go deleted file mode 100644 index c8d8f37..0000000 --- a/internal/authz/oidc_test.go +++ /dev/null @@ -1,1712 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package authz - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "encoding/json" - "errors" - "fmt" - "net" - "net/http" - "net/url" - "testing" - "time" - - envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/stretchr/testify/require" - "github.com/tetratelabs/telemetry" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/test/bufconn" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" - "github.com/tetrateio/authservice-go/internal" - inthttp "github.com/tetrateio/authservice-go/internal/http" - "github.com/tetrateio/authservice-go/internal/oidc" -) - -var ( - callbackRequest = &envoy.CheckRequest{ - Attributes: &envoy.AttributeContext{ - Request: &envoy.AttributeContext_Request{ - Http: &envoy.AttributeContext_HttpRequest{ - Id: "request-id", - Scheme: "https", Host: "localhost:443", Path: "/callback?code=auth-code&state=new-state", - Method: "GET", - Headers: map[string]string{ - inthttp.HeaderCookie: defaultCookieName + "=test-session-id", - }, - }, - }, - }, - } - - noSessionRequest = &envoy.CheckRequest{ - Attributes: &envoy.AttributeContext{ - Request: &envoy.AttributeContext_Request{ - Http: &envoy.AttributeContext_HttpRequest{ - Id: "request-id", - Scheme: "https", Host: "example.com", Path: "/", - Method: "GET", - }, - }, - }, - } - - withSessionHeader = &envoy.CheckRequest{ - Attributes: &envoy.AttributeContext{ - Request: &envoy.AttributeContext_Request{ - Http: &envoy.AttributeContext_HttpRequest{ - Id: "request-id", - Scheme: "https", Host: "example.com", Path: "/", - Method: "GET", - Headers: map[string]string{ - inthttp.HeaderCookie: defaultCookieName + "=test-session-id", - }, - }, - }, - }, - } - - logoutWithNoSession = &envoy.CheckRequest{ - Attributes: &envoy.AttributeContext{ - Request: &envoy.AttributeContext_Request{ - Http: &envoy.AttributeContext_HttpRequest{ - Id: "request-id", - Scheme: "https", Host: "example.com", Path: "/logout?some-params", - Method: "GET", - }, - }, - }, - } - - logoutWithSession = &envoy.CheckRequest{ - Attributes: &envoy.AttributeContext{ - Request: &envoy.AttributeContext_Request{ - Http: &envoy.AttributeContext_HttpRequest{ - Id: "request-id", - Scheme: "https", Host: "example.com", Path: "/logout?some-params", - Method: "GET", - Headers: map[string]string{ - inthttp.HeaderCookie: defaultCookieName + "=test-session-id", - }, - }, - }, - }, - } - - requestedAppURL = "https://localhost:443/final-app" - validAuthState = &oidc.AuthorizationState{ - Nonce: newNonce, - State: newState, - RequestedURL: requestedAppURL, - } - - yesterday = time.Now().Add(-24 * time.Hour) - tomorrow = time.Now().Add(24 * time.Hour) - - sessionID = "test-session-id" - newSessionID = "new-session-id" - newNonce = "new-nonce" - newState = "new-state" - - basicOIDCConfig = &oidcv1.OIDCConfig{ - IdToken: &oidcv1.TokenConfig{ - Header: "Authorization", - Preamble: "Bearer", - }, - AccessToken: &oidcv1.TokenConfig{ - Header: "X-Access-Token", - Preamble: "Bearer", - }, - TokenUri: "http://idp-test-server/token", - AuthorizationUri: "http://idp-test-server/auth", - CallbackUri: "https://localhost:443/callback", - ClientId: "test-client-id", - ClientSecretConfig: &oidcv1.OIDCConfig_ClientSecret{ - ClientSecret: "test-client-secret", - }, - Scopes: []string{"openid", "email"}, - Logout: &oidcv1.LogoutConfig{ - Path: "/logout", - RedirectUri: "http://idp-test-server/logout?with-params", - }, - } - - dynamicOIDCConfig = &oidcv1.OIDCConfig{ - IdToken: &oidcv1.TokenConfig{ - Header: "Authorization", - Preamble: "Bearer", - }, - AccessToken: &oidcv1.TokenConfig{ - Header: "X-Access-Token", - Preamble: "Bearer", - }, - ConfigurationUri: "http://idp-test-server/.well-known/openid-configuration", - CallbackUri: "https://localhost:443/callback", - ClientId: "test-client-id", - ClientSecretConfig: &oidcv1.OIDCConfig_ClientSecret{ - ClientSecret: "test-client-secret", - }, - Scopes: []string{"openid", "email"}, - } - - wellKnownURIs = ` -{ - "issuer": "http://idp-test-server", - "authorization_endpoint": "http://idp-test-server/authorize", - "token_endpoint": "http://idp-test-server/token", - "jwks_uri": "http://idp-test-server/jwks" -}` - - wantRedirectParams = url.Values{ - "response_type": {"code"}, - "client_id": {"test-client-id"}, - "redirect_uri": {"https://localhost:443/callback"}, - "scope": {"openid email"}, - "state": {newState}, - "nonce": {newNonce}, - } - - wantRedirectBaseURI = "http://idp-test-server/auth" -) - -func TestOIDCProcess(t *testing.T) { - - unknownJWKPriv, _ := newKeyPair(t) - jwkPriv, jwkPub := newKeyPair(t) - bytes, err := json.Marshal(newKeySet(t, jwkPub)) - require.NoError(t, err) - basicOIDCConfig.JwksConfig = &oidcv1.OIDCConfig_Jwks{ - Jwks: string(bytes), - } - - clock := oidc.Clock{} - sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&clock, time.Hour, time.Hour)} - store := sessions.Get(basicOIDCConfig) - tlsPool := internal.NewTLSConfigPool(context.Background()) - h, err := NewOIDCHandler(basicOIDCConfig, tlsPool, - oidc.NewJWKSProvider(newConfigFor(basicOIDCConfig), tlsPool), sessions, clock, - oidc.NewStaticGenerator(newSessionID, newNonce, newState)) - require.NoError(t, err) - - ctx := context.Background() - - // The following subset of tests is testing the requests to the app, not any callback or auth flow. - // So there's no expected communication with any external server. - - requestToAppTests := []struct { - name string - req *envoy.CheckRequest - storedTokenResponse *oidc.TokenResponse - responseVerify func(*testing.T, *envoy.CheckResponse) - }{ - { - name: "invalid request with missing http", - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - }, - }, - { - name: "request with no sessionID", - req: noSessionRequest, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - // A new authorization state should have been set in the store - requireStoredState(t, store, newSessionID, true) - }, - }, - { - name: "request with no existing sessionID", - req: withSessionHeader, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - // A new authorization state should have been set in the store - requireStoredState(t, store, newSessionID, true) - // The old one should have been removed - requireStoredState(t, store, sessionID, false) - }, - }, - { - name: "request with an existing sessionID expired with no refresh token", - req: withSessionHeader, - storedTokenResponse: &oidc.TokenResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(yesterday)), - AccessToken: "access-token", - AccessTokenExpiresAt: yesterday, - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - // A new authorization state should have been set in the store - requireStoredState(t, store, newSessionID, true) - // The old one should have been removed - requireStoredState(t, store, sessionID, false) - }, - }, - { - name: "request with an existing sessionID not expired", - req: withSessionHeader, - storedTokenResponse: &oidc.TokenResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(tomorrow)), - AccessToken: "access-token", - AccessTokenExpiresAt: tomorrow, - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.OK), resp.GetStatus().GetCode()) - require.NotNil(t, resp.GetOkResponse()) - requireTokensInResponse(t, resp.GetOkResponse(), basicOIDCConfig, newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(tomorrow)), "access-token") - // The sessionID should not have been changed - requireStoredTokens(t, store, sessionID, true) - requireStoredState(t, store, newSessionID, false) - requireStoredTokens(t, store, newSessionID, false) - }, - }, - { - name: "matches logout: request with no sessionId", - req: logoutWithNoSession, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), "http://idp-test-server/logout", url.Values{"with-params": {""}}) - requireDeleteCookie(t, resp.GetDeniedResponse()) - }, - }, - { - name: "matches logout: request with sessionId", - req: logoutWithSession, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), "http://idp-test-server/logout", url.Values{"with-params": {""}}) - requireDeleteCookie(t, resp.GetDeniedResponse()) - requireStoredState(t, store, sessionID, false) - }, - }, - } - - for _, tt := range requestToAppTests { - t.Run(tt.name, func(t *testing.T) { - t.Cleanup(func() { - require.NoError(t, store.RemoveSession(ctx, sessionID)) - require.NoError(t, store.RemoveSession(ctx, newSessionID)) - }) - - if tt.storedTokenResponse != nil { - require.NoError(t, store.SetTokenResponse(ctx, sessionID, tt.storedTokenResponse)) - } - - resp := &envoy.CheckResponse{} - require.NoError(t, h.Process(ctx, tt.req, resp)) - tt.responseVerify(t, resp) - }) - } - - // The following subset of tests is testing the callback requests, so there's expected communication with the IDP server. - - idpServer := newServer() - h.(*oidcHandler).httpClient = idpServer.newHTTPClient() - - callbackTests := []struct { - name string - req *envoy.CheckRequest - storedAuthState *oidc.AuthorizationState - mockTokensResponse *idpTokensResponse - mockStatusCode int - responseVerify func(*testing.T, *envoy.CheckResponse) - }{ - { - name: "successfully retrieve new tokens", - req: callbackRequest, - storedAuthState: validAuthState, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Audience([]string{"test-client-id"}).Claim("nonce", newNonce)), - AccessToken: "access-token", - TokenType: "Bearer", - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), requestedAppURL, nil) - requireStoredTokens(t, store, sessionID, true) - requireStoredTokens(t, store, newSessionID, false) - }, - }, - { - name: "request is invalid, query parameters are missing", - req: modifyCallbackRequestPath("/callback?"), - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "request is invalid, query has invalid format", - req: modifyCallbackRequestPath("/callback?invalid;format"), - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "request is invalid, state is missing", - req: modifyCallbackRequestPath("/callback?code=auth-code"), - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "request is invalid, code is missing", - req: modifyCallbackRequestPath("/callback?state=new-state"), - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "session state not found in the store", - req: callbackRequest, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - require.Equal(t, typev3.StatusCode_BadRequest, response.GetDeniedResponse().GetStatus().GetCode()) - require.Equal(t, "Oops, your session has expired. Please try again.", response.GetDeniedResponse().GetBody()) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "session state stored does not match the request", - req: callbackRequest, - storedAuthState: &oidc.AuthorizationState{ - Nonce: newNonce, - State: "non-matching-state", - RequestedURL: requestedAppURL, - }, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "idp server returns non-200 status code", - req: callbackRequest, - storedAuthState: validAuthState, - mockStatusCode: http.StatusInternalServerError, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unknown), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "idp server returns empty body", - req: callbackRequest, - storedAuthState: validAuthState, - mockStatusCode: http.StatusOK, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.Internal), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "idp returned non-bearer token type", - req: callbackRequest, - storedAuthState: validAuthState, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Claim("nonce", newNonce).Audience([]string{"test-client-id"})), - TokenType: "not-bearer", - }, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "idp returned invalid expires_in for access token", - req: callbackRequest, - storedAuthState: validAuthState, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Claim("nonce", newNonce).Audience([]string{"test-client-id"})), - TokenType: "Bearer", - ExpiresIn: -1, - }, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "idp didn't return access token", - req: callbackRequest, - storedAuthState: validAuthState, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Claim("nonce", newNonce).Audience([]string{"test-client-id"})), - TokenType: "Bearer", - ExpiresIn: 3600, - }, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "idp server returns invalid JWT id-token", - req: callbackRequest, - storedAuthState: validAuthState, - mockStatusCode: http.StatusOK, - mockTokensResponse: &idpTokensResponse{ - IDToken: "not-a-jwt", - TokenType: "Bearer", - ExpiresIn: 3600, - AccessToken: "access-token", - }, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.Internal), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "idp server returns JWT signed with unknown key", - req: callbackRequest, - storedAuthState: validAuthState, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, unknownJWKPriv, jwt.NewBuilder().Audience([]string{"test-client-id"}).Claim("nonce", newNonce)), - TokenType: "Bearer", - ExpiresIn: 3600, - AccessToken: "access-token", - }, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.Internal), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "idp didn't return nonce", - req: callbackRequest, - storedAuthState: &oidc.AuthorizationState{ - Nonce: "old-nonce", - State: newState, - RequestedURL: requestedAppURL, - }, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder()), - TokenType: "Bearer", - ExpiresIn: 3600, - AccessToken: "access-token", - }, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "session nonce stored does not match idp returned nonce", - req: callbackRequest, - storedAuthState: &oidc.AuthorizationState{ - Nonce: "old-nonce", - State: newState, - RequestedURL: requestedAppURL, - }, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Claim("nonce", "non-matching-nonce")), - TokenType: "Bearer", - ExpiresIn: 3600, - AccessToken: "access-token", - }, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "idp returned empty audience", - req: callbackRequest, - storedAuthState: validAuthState, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Claim("nonce", newNonce)), - TokenType: "Bearer", - ExpiresIn: 3600, - AccessToken: "access-token", - }, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - { - name: "idp returned non-matching audience", - req: callbackRequest, - storedAuthState: validAuthState, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Claim("nonce", newNonce).Audience([]string{"non-matching-audience"})), - TokenType: "Bearer", - ExpiresIn: 3600, - AccessToken: "access-token", - }, - responseVerify: func(t *testing.T, response *envoy.CheckResponse) { - require.Equal(t, int32(codes.InvalidArgument), response.GetStatus().GetCode()) - requireStandardResponseHeaders(t, response) - requireStoredTokens(t, store, sessionID, false) - }, - }, - } - - for _, tt := range callbackTests { - t.Run("matches callback: "+tt.name, func(t *testing.T) { - idpServer.Start() - t.Cleanup(func() { - idpServer.Stop() - require.NoError(t, store.RemoveSession(ctx, sessionID)) - }) - - idpServer.tokensResponse = tt.mockTokensResponse - idpServer.statusCode = tt.mockStatusCode - if tt.mockStatusCode <= 0 { - idpServer.statusCode = http.StatusOK - } - - // Set the authorization state in the store, so it can be found by the handler - require.NoError(t, store.SetAuthorizationState(ctx, sessionID, tt.storedAuthState)) - - resp := &envoy.CheckResponse{} - err = h.Process(ctx, tt.req, resp) - require.NoError(t, err) - - tt.responseVerify(t, resp) - }) - } - - validIDToken := newJWT(t, jwkPriv, jwt.NewBuilder().Audience([]string{"test-client-id"}).Claim("nonce", newNonce)) - validIDTokenWithoutNonce := newJWT(t, jwkPriv, jwt.NewBuilder().Audience([]string{"test-client-id"})) - - expiredTokenResponse := &oidc.TokenResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(yesterday).Audience([]string{"test-client-id"}).Claim("nonce", newNonce)), - RefreshToken: "refresh-token", - AccessToken: "access-token", - AccessTokenExpiresAt: yesterday, - } - - refreshTokensTests := []struct { - name string - req *envoy.CheckRequest - storedAuthState *oidc.AuthorizationState - storedTokenResponse *oidc.TokenResponse - mockTokensResponse *idpTokensResponse - mockStatusCode int - responseVerify func(*testing.T, *envoy.CheckResponse) - }{ - { - name: "IDP server returns empty body", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - requireStoredState(t, store, newSessionID, true) - requireStoredState(t, store, sessionID, false) - }, - }, - { - name: "IDP server returns an non-200 status", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - mockStatusCode: http.StatusInternalServerError, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - requireStoredState(t, store, newSessionID, true) - requireStoredState(t, store, sessionID, false) - }, - }, - { - name: "IDP server returns response with an invalid token_type", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - mockTokensResponse: &idpTokensResponse{ - IDToken: validIDToken, - AccessToken: "access-token", - TokenType: "invalid-token-type", - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - requireStoredState(t, store, newSessionID, true) - requireStoredState(t, store, sessionID, false) - }, - }, - { - name: "IDP server returns a response with an invalid expires_at", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - mockTokensResponse: &idpTokensResponse{ - IDToken: validIDToken, - AccessToken: "access-token", - TokenType: "Bearer", - ExpiresIn: -1, - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - requireStoredState(t, store, newSessionID, true) - requireStoredState(t, store, sessionID, false) - }, - }, - { - name: "IDP server returns a response with no access token - succeeds using the stored access token", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - mockTokensResponse: &idpTokensResponse{ - IDToken: validIDToken, - TokenType: "Bearer", - ExpiresIn: 10, - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.OK), resp.GetStatus().GetCode()) - require.NotNil(t, resp.GetOkResponse()) - requireTokensInResponse(t, resp.GetOkResponse(), basicOIDCConfig, validIDToken, "access-token") - requireStoredTokens(t, store, sessionID, true) - requireStoredTokens(t, store, newSessionID, false) - }, - }, - { - name: "IDP server doesn't return an id-token - succeeds using the stored id-token", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - mockTokensResponse: &idpTokensResponse{ - TokenType: "Bearer", - ExpiresIn: 10, - AccessToken: "access-token", - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.OK), resp.GetStatus().GetCode()) - require.NotNil(t, resp.GetOkResponse()) - requireTokensInResponse(t, resp.GetOkResponse(), basicOIDCConfig, expiredTokenResponse.IDToken, "access-token") - requireStoredTokens(t, store, sessionID, true) - requireStoredTokens(t, store, newSessionID, false) - }, - }, - { - name: "IDP server returns an invalid JWT as id-token - succeeds using the stored id-token", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - mockTokensResponse: &idpTokensResponse{ - IDToken: "not-a-jwt", - TokenType: "Bearer", - ExpiresIn: 10, - AccessToken: "access-token", - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.OK), resp.GetStatus().GetCode()) - require.NotNil(t, resp.GetOkResponse()) - requireTokensInResponse(t, resp.GetOkResponse(), basicOIDCConfig, expiredTokenResponse.IDToken, "access-token") - requireStoredTokens(t, store, sessionID, true) - requireStoredTokens(t, store, newSessionID, false) - }, - }, - { - name: "IDP server returns an id-token signed with unknown key", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, unknownJWKPriv, jwt.NewBuilder().Audience([]string{"test-client-id"}).Claim("nonce", newNonce)), - AccessToken: "access-token", - TokenType: "Bearer", - ExpiresIn: 10, - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - requireStoredState(t, store, newSessionID, true) - requireStoredState(t, store, sessionID, false) - }, - }, - { - name: "IDP server returns an id-token with non-matching nonce", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Audience([]string{"test-client-id"}).Claim("nonce", "non-matching-nonce")), - AccessToken: "access-token", - TokenType: "Bearer", - ExpiresIn: 10, - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - requireStoredState(t, store, newSessionID, true) - requireStoredState(t, store, sessionID, false) - }, - }, - { - name: "IDP server returns an id-token with no nonce claim - succeeds as it is not required", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - mockTokensResponse: &idpTokensResponse{ - IDToken: validIDTokenWithoutNonce, - AccessToken: "access-token", - TokenType: "Bearer", - ExpiresIn: 10, - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.OK), resp.GetStatus().GetCode()) - require.NotNil(t, resp.GetOkResponse()) - requireTokensInResponse(t, resp.GetOkResponse(), basicOIDCConfig, validIDTokenWithoutNonce, "access-token") - requireStoredTokens(t, store, sessionID, true) - requireStoredTokens(t, store, newSessionID, false) - }, - }, - { - name: "IDP server returns an id-token with non-matching audience", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - mockTokensResponse: &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Audience([]string{"non-matching-audience"}).Claim("nonce", newNonce)), - AccessToken: "access-token", - TokenType: "Bearer", - ExpiresIn: 10, - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - requireStoredState(t, store, newSessionID, true) - requireStoredState(t, store, sessionID, false) - }, - }, - { - name: "succeed", - req: withSessionHeader, - storedTokenResponse: expiredTokenResponse, - mockTokensResponse: &idpTokensResponse{ - IDToken: validIDToken, - AccessToken: "access-token", - TokenType: "Bearer", - ExpiresIn: 10, - }, - responseVerify: func(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.OK), resp.GetStatus().GetCode()) - require.NotNil(t, resp.GetOkResponse()) - requireTokensInResponse(t, resp.GetOkResponse(), basicOIDCConfig, validIDToken, "access-token") - requireStoredTokens(t, store, sessionID, true) - requireStoredTokens(t, store, newSessionID, false) - }, - }, - } - - for _, tt := range refreshTokensTests { - t.Run("refresh tokens: "+tt.name, func(t *testing.T) { - idpServer.Start() - t.Cleanup(func() { - idpServer.Stop() - require.NoError(t, store.RemoveSession(ctx, sessionID)) - require.NoError(t, store.RemoveSession(ctx, newSessionID)) - }) - - idpServer.tokensResponse = tt.mockTokensResponse - idpServer.statusCode = tt.mockStatusCode - if tt.mockStatusCode <= 0 { - idpServer.statusCode = http.StatusOK - } - - if tt.storedAuthState == nil { - tt.storedAuthState = validAuthState - } - require.NoError(t, store.SetAuthorizationState(ctx, sessionID, tt.storedAuthState)) - if tt.storedTokenResponse != nil { - require.NoError(t, store.SetTokenResponse(ctx, sessionID, tt.storedTokenResponse)) - } - - resp := &envoy.CheckResponse{} - require.NoError(t, h.Process(ctx, tt.req, resp)) - tt.responseVerify(t, resp) - }) - } -} - -func TestOIDCProcessWithFailingSessionStore(t *testing.T) { - store := &storeMock{delegate: oidc.NewMemoryStore(&oidc.Clock{}, time.Hour, time.Hour)} - sessions := &mockSessionStoreFactory{store: store} - tlsPool := internal.NewTLSConfigPool(context.Background()) - - jwkPriv, jwkPub := newKeyPair(t) - bytes, err := json.Marshal(newKeySet(t, jwkPub)) - require.NoError(t, err) - basicOIDCConfig.JwksConfig = &oidcv1.OIDCConfig_Jwks{ - Jwks: string(bytes), - } - - h, err := NewOIDCHandler(basicOIDCConfig, tlsPool, oidc.NewJWKSProvider(newConfigFor(basicOIDCConfig), tlsPool), - sessions, oidc.Clock{}, oidc.NewStaticGenerator(newSessionID, newNonce, newState)) - require.NoError(t, err) - - ctx := context.Background() - - // The following subset of tests is testing the requests to the app, not any callback or auth flow. - // So there's no expected communication with any external server. - requestToAppTests := []struct { - name string - req *envoy.CheckRequest - storeErrors map[int]bool - }{ - { - name: "app request - fails to get token response from given session ID", - req: withSessionHeader, - storeErrors: map[int]bool{getTokenResponse: true}, - }, - { - name: "app request (redirect to IDP) - fails to remove old session", - req: withSessionHeader, - storeErrors: map[int]bool{removeSession: true}, - }, - { - name: "app request (redirect to IDP) - fails to set new authorization state", - req: withSessionHeader, - storeErrors: map[int]bool{setAuthorizationState: true}, - }, - { - name: "logout request - fails to remove session", - req: logoutWithSession, - storeErrors: map[int]bool{removeSession: true}, - }, - } - - for _, tt := range requestToAppTests { - t.Run(tt.name, func(t *testing.T) { - store.errs = tt.storeErrors - t.Cleanup(func() { store.errs = nil }) - resp := &envoy.CheckResponse{} - require.NoError(t, h.Process(ctx, tt.req, resp)) - requireSessionErrorResponse(t, resp) - }) - } - - idpServer := newServer() - idpServer.statusCode = http.StatusOK - idpServer.tokensResponse = &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Audience([]string{"test-client-id"}).Claim("nonce", newNonce)), - AccessToken: "access-token", - TokenType: "Bearer", - } - idpServer.Start() - t.Cleanup(idpServer.Stop) - h.(*oidcHandler).httpClient = idpServer.newHTTPClient() - - // The following subset of tests is testing the callback requests, so there's expected communication with the IDP server. - // The store is expected to fail in some way, so the handler should return an error response. - callbackTests := []struct { - name string - storeCallsToError map[int]bool - }{ - { - name: "callback request - fails to get authorization state", - storeCallsToError: map[int]bool{getAuthorizationState: true}, - }, - { - name: "callback request - fails to clear old authorization state", - storeCallsToError: map[int]bool{clearAuthorizationState: true}, - }, - { - name: "callback request - fails to set new token response", - storeCallsToError: map[int]bool{setTokenResponse: true}, - }, - } - - for _, tt := range callbackTests { - t.Run(tt.name, func(t *testing.T) { - require.NoError(t, store.SetAuthorizationState(ctx, sessionID, validAuthState)) - - store.errs = tt.storeCallsToError - t.Cleanup(func() { store.errs = nil }) - - resp := &envoy.CheckResponse{} - require.NoError(t, h.Process(ctx, callbackRequest, resp)) - requireSessionErrorResponse(t, resp) - }) - } - - // The following subset of tests is testing the refresh tokens requests, so there's expected communication with the IDP server. - // The store is expected to fail in some way, so the handler should return an error response. - refreshTokensTests := []struct { - name string - storeCallsToError map[int]bool - wantRedirect bool - }{ - { - name: "refresh tokens - fails to get the authorization state", - storeCallsToError: map[int]bool{getAuthorizationState: true}, - wantRedirect: true, - }, - { - name: "refresh tokens - fails to set new token response", - storeCallsToError: map[int]bool{setTokenResponse: true}, - wantRedirect: false, - }, - } - - for _, tt := range refreshTokensTests { - t.Run(tt.name, func(t *testing.T) { - require.NoError(t, store.SetAuthorizationState(ctx, sessionID, validAuthState)) - require.NoError(t, store.SetTokenResponse(ctx, sessionID, &oidc.TokenResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(yesterday).Audience([]string{"test-client-id"}).Claim("nonce", newNonce)), - RefreshToken: "refresh-token", - AccessToken: "access-token", - AccessTokenExpiresAt: yesterday, - })) - - store.errs = tt.storeCallsToError - t.Cleanup(func() { store.errs = nil }) - - resp := &envoy.CheckResponse{} - require.NoError(t, h.Process(ctx, withSessionHeader, resp)) - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - if tt.wantRedirect { - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - } else { - requireSessionErrorResponse(t, resp) - } - }) - } -} - -func TestOIDCProcessWithFailingJWKSProvider(t *testing.T) { - funcJWKSProvider := jwksProviderFunc(func() (jwk.Set, error) { - return nil, errors.New("test jwks provider error") - }) - - jwkPriv, _ := newKeyPair(t) - - clock := oidc.Clock{} - sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&clock, time.Hour, time.Hour)} - store := sessions.Get(basicOIDCConfig) - tlsPool := internal.NewTLSConfigPool(context.Background()) - h, err := NewOIDCHandler(basicOIDCConfig, tlsPool, funcJWKSProvider, sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState)) - require.NoError(t, err) - - idpServer := newServer() - h.(*oidcHandler).httpClient = idpServer.newHTTPClient() - - ctx := context.Background() - - idpServer.Start() - t.Cleanup(func() { - idpServer.Stop() - require.NoError(t, store.RemoveSession(ctx, sessionID)) - }) - - idpServer.tokensResponse = &idpTokensResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Audience([]string{"test-client-id"}).Claim("nonce", newNonce)), - AccessToken: "access-token", - TokenType: "Bearer", - } - idpServer.statusCode = http.StatusOK - - expiredTokenResponse := &oidc.TokenResponse{ - IDToken: newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(yesterday).Audience([]string{"test-client-id"}).Claim("nonce", newNonce)), - RefreshToken: "refresh-token", - AccessToken: "access-token", - AccessTokenExpiresAt: yesterday, - } - - require.NoError(t, store.SetAuthorizationState(ctx, sessionID, validAuthState)) - - t.Run("callback request ", func(t *testing.T) { - resp := &envoy.CheckResponse{} - require.NoError(t, h.Process(ctx, callbackRequest, resp)) - require.Equal(t, int32(codes.Internal), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireStoredTokens(t, store, sessionID, false) - }) - - require.NoError(t, store.SetTokenResponse(ctx, sessionID, expiredTokenResponse)) - - t.Run("refresh tokens - redirect to reauthenticate", func(t *testing.T) { - resp := &envoy.CheckResponse{} - require.NoError(t, h.Process(ctx, withSessionHeader, resp)) - - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - requireStandardResponseHeaders(t, resp) - requireRedirectResponse(t, resp.GetDeniedResponse(), wantRedirectBaseURI, wantRedirectParams) - requireCookie(t, resp.GetDeniedResponse()) - requireStoredState(t, store, newSessionID, true) - }) -} - -func TestMatchesCallbackPath(t *testing.T) { - tests := []struct { - callback string - host, path string - want bool - }{ - {"https://example.com", "example.com", "/", false}, - {"https://example.com/callback", "example.com", "/callback", true}, - {"http://example.com/callback", "example.com", "/callback", true}, - {"https://example.com/callback", "example.com", "/callback/", false}, - {"http://example.com/callback", "example.com", "/callback/", false}, - {"https://example.com/callback", "example.com", "/callback?query#fragment", true}, - {"http://example.com/callback", "example.com", "/callback?query#fragment", true}, - {"https://example.com:443/callback", "example.com", "/callback", true}, - {"https://example.com:8443/callback", "example.com", "/callback", false}, - {"https://example.com:8443/callback", "example.com:8443", "/callback", true}, - {"http://example.com/callback", "example.com", "/callback", true}, - {"http://example.com:80/callback", "example.com", "/callback", true}, - {"http://example.com:8080/callback", "example.com", "/callback", false}, - {"http://example.com:8080/callback", "example.com:8080", "/callback", true}, - } - - for _, tt := range tests { - t.Run(tt.callback, func(t *testing.T) { - got := matchesCallbackPath(telemetry.NoopLogger(), - &oidcv1.OIDCConfig{CallbackUri: tt.callback}, - &envoy.AttributeContext_HttpRequest{Host: tt.host, Path: tt.path}) - require.Equal(t, tt.want, got) - }) - } -} - -func TestMatchesLogoutPath(t *testing.T) { - var ( - logoutPathConfig = &oidcv1.LogoutConfig{Path: "/logout"} - emptyLogoutPathConfig = &oidcv1.LogoutConfig{} - ) - - tests := []struct { - name string - logoutConfig *oidcv1.LogoutConfig - reqPath string - want bool - }{ - {"with-config", logoutPathConfig, "/logout", true}, - {"with-config", logoutPathConfig, "/logout/", false}, - {"with-config", logoutPathConfig, "/logout?query#fragment", true}, - {"with-config", logoutPathConfig, "/other", false}, - {"with-config", logoutPathConfig, "/logout-nope", false}, - {"empty-config", emptyLogoutPathConfig, "/logout", false}, - {"empty-config", emptyLogoutPathConfig, "/logout/", false}, - {"empty-config", emptyLogoutPathConfig, "/logout?query#fragment", false}, - {"empty-config", emptyLogoutPathConfig, "/other", false}, - {"empty-config", emptyLogoutPathConfig, "/logout-nope", false}, - {"nil-config", nil, "/logout", false}, - {"nil-config", nil, "/logout/", false}, - {"nil-config", nil, "/logout?query#fragment", false}, - {"nil-config", nil, "/other", false}, - {"nil-config", nil, "/logout-nope", false}, - } - - for _, tt := range tests { - t.Run(tt.name+" "+tt.reqPath, func(t *testing.T) { - got := matchesLogoutPath(telemetry.NoopLogger(), - &oidcv1.OIDCConfig{Logout: tt.logoutConfig}, - &envoy.AttributeContext_HttpRequest{Path: tt.reqPath}) - require.Equal(t, tt.want, got) - }) - } - -} - -func TestEncodeTokensToHeaders(t *testing.T) { - const ( - idToken = "id-token" - accessToken = "access-token" - ) - - tests := []struct { - name string - config *oidcv1.OIDCConfig - idToken, accessToken string - want map[string]string - }{ - { - name: "only id token", - config: &oidcv1.OIDCConfig{ - IdToken: &oidcv1.TokenConfig{Header: "Authorization", Preamble: "Bearer"}, - }, - idToken: idToken, accessToken: "", - want: map[string]string{ - "Authorization": "Bearer " + idToken, - }, - }, - { - name: "id token and access token", - config: &oidcv1.OIDCConfig{ - IdToken: &oidcv1.TokenConfig{Header: "Authorization", Preamble: "Bearer"}, - AccessToken: &oidcv1.TokenConfig{Header: "X-Access-Token", Preamble: "Bearer"}, - }, - idToken: idToken, accessToken: accessToken, - want: map[string]string{ - "Authorization": "Bearer " + idToken, - "X-Access-Token": "Bearer " + accessToken, - }, - }, - { - name: "not default config", - config: &oidcv1.OIDCConfig{ - IdToken: &oidcv1.TokenConfig{Header: "X-Id-Token", Preamble: "Other"}, - AccessToken: &oidcv1.TokenConfig{Header: "X-Access-Token-Other", Preamble: "Other"}, - }, - idToken: idToken, accessToken: accessToken, - want: map[string]string{ - "X-Id-Token": "Other " + idToken, - "X-Access-Token-Other": "Other " + accessToken, - }, - }, - { - name: "config with access token but no access token in response", - config: &oidcv1.OIDCConfig{ - IdToken: &oidcv1.TokenConfig{Header: "Authorization", Preamble: "Bearer"}, - AccessToken: &oidcv1.TokenConfig{Header: "X-Access-Token", Preamble: "Bearer"}, - }, - idToken: idToken, accessToken: "", - want: map[string]string{ - "Authorization": "Bearer " + idToken, - }, - }, - { - name: "config with no access token but access token in response", - config: &oidcv1.OIDCConfig{ - IdToken: &oidcv1.TokenConfig{Header: "Authorization", Preamble: "Bearer"}, - }, - idToken: idToken, accessToken: accessToken, - want: map[string]string{ - "Authorization": "Bearer " + idToken, - }, - }, - { - name: "config with out preamble", - config: &oidcv1.OIDCConfig{ - IdToken: &oidcv1.TokenConfig{Header: "X-ID-Token"}, - AccessToken: &oidcv1.TokenConfig{Header: "X-Access-Token"}, - }, - idToken: idToken, accessToken: accessToken, - want: map[string]string{ - "X-ID-Token": idToken, - "X-Access-Token": accessToken, - }, - }, - } - - sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&oidc.Clock{}, time.Hour, time.Hour)} - tlsPool := internal.NewTLSConfigPool(context.Background()) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h, err := NewOIDCHandler(tt.config, tlsPool, nil, sessions, oidc.Clock{}, nil) - require.NoError(t, err) - - tokResp := &oidc.TokenResponse{ - IDToken: tt.idToken, - } - if tt.accessToken != "" { - tokResp.AccessToken = tt.accessToken - } - - got := h.(*oidcHandler).encodeTokensToHeaders(tokResp) - require.Equal(t, tt.want, got) - }) - } -} - -func TestAreTokensExpired(t *testing.T) { - jwkPriv, _ := newKeyPair(t) - - tests := []struct { - name string - config *oidcv1.OIDCConfig - idToken string - accessTokenExpiration time.Time - want bool - }{ - { - name: "no expiration - only id token", - config: &oidcv1.OIDCConfig{}, - idToken: newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(tomorrow)), - want: false, - }, - { - name: "no expiration - id token and access token", - config: &oidcv1.OIDCConfig{AccessToken: &oidcv1.TokenConfig{}}, - idToken: newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(tomorrow)), - accessTokenExpiration: tomorrow, - want: false, - }, - { - name: "expired - only id token", - config: &oidcv1.OIDCConfig{}, - idToken: newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(yesterday)), - want: true, - }, - { - name: "expired - id token and access token", - config: &oidcv1.OIDCConfig{AccessToken: &oidcv1.TokenConfig{}}, - idToken: newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(yesterday)), - accessTokenExpiration: yesterday, - want: true, - }, - { - name: "id token not expired, access token expired", - config: &oidcv1.OIDCConfig{AccessToken: &oidcv1.TokenConfig{}}, - idToken: newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(tomorrow)), - accessTokenExpiration: yesterday, - want: true, - }, - { - name: "id token not expired, access token expired - but access token not in config", - config: &oidcv1.OIDCConfig{}, - idToken: newJWT(t, jwkPriv, jwt.NewBuilder().Expiration(tomorrow)), - accessTokenExpiration: yesterday, - want: false, - }, - } - - sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&oidc.Clock{}, time.Hour, time.Hour)} - tlsPool := internal.NewTLSConfigPool(context.Background()) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h, err := NewOIDCHandler(tt.config, tlsPool, nil, sessions, oidc.Clock{}, nil) - require.NoError(t, err) - - tokResp := &oidc.TokenResponse{ - IDToken: tt.idToken, - } - if !tt.accessTokenExpiration.IsZero() { - tokResp.AccessToken = "access-token" - tokResp.AccessTokenExpiresAt = tt.accessTokenExpiration - } - - got, err := h.(*oidcHandler).areRequiredTokensExpired(tokResp) - require.NoError(t, err) - require.Equal(t, tt.want, got) - }) - } -} - -func TestLoadWellKnownConfig(t *testing.T) { - idpServer := newServer() - idpServer.Start() - t.Cleanup(idpServer.Stop) - - require.NoError(t, loadWellKnownConfig(idpServer.newHTTPClient(), dynamicOIDCConfig)) - require.Equal(t, dynamicOIDCConfig.AuthorizationUri, "http://idp-test-server/authorize") - require.Equal(t, dynamicOIDCConfig.TokenUri, "http://idp-test-server/token") - require.Equal(t, dynamicOIDCConfig.GetJwksFetcher().GetJwksUri(), "http://idp-test-server/jwks") -} - -func TestLoadWellKnownConfigError(t *testing.T) { - clock := oidc.Clock{} - tlsPool := internal.NewTLSConfigPool(context.Background()) - sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&clock, time.Hour, time.Hour)} - _, err := NewOIDCHandler(dynamicOIDCConfig, tlsPool, oidc.NewJWKSProvider(newConfigFor(basicOIDCConfig), tlsPool), - sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState)) - require.Error(t, err) // Fail to retrieve the dynamic config since the test server is not running -} - -func TestNewOIDCHandler(t *testing.T) { - clock := oidc.Clock{} - tlsPool := internal.NewTLSConfigPool(context.Background()) - sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&clock, time.Hour, time.Hour)} - - tests := []struct { - name string - config *oidcv1.OIDCConfig - wantErr bool - }{ - {"empty", &oidcv1.OIDCConfig{}, false}, - {"proxy uri", &oidcv1.OIDCConfig{ProxyUri: "http://proxy"}, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - _, err := NewOIDCHandler(tt.config, tlsPool, oidc.NewJWKSProvider(newConfigFor(basicOIDCConfig), tlsPool), - sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState)) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - - }) - } -} - -func modifyCallbackRequestPath(path string) *envoy.CheckRequest { - return &envoy.CheckRequest{ - Attributes: &envoy.AttributeContext{ - Request: &envoy.AttributeContext_Request{ - Http: &envoy.AttributeContext_HttpRequest{ - Id: "request-id", - Scheme: "https", Host: "localhost:443", Path: path, - Method: "GET", - Headers: map[string]string{ - inthttp.HeaderCookie: defaultCookieName + "=test-session-id", - }, - }, - }, - }, - } -} - -const ( - keyID = "test" - keyAlg = jwa.RS256 -) - -func newKeySet(t *testing.T, keys ...jwk.Key) jwk.Set { - jwks := jwk.NewSet() - for _, k := range keys { - require.NoError(t, jwks.AddKey(k)) - } - return jwks -} - -func newKeyPair(t *testing.T) (jwk.Key, jwk.Key) { - rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - priv, err := jwk.FromRaw(rsaKey) - require.NoError(t, err) - err = priv.Set(jwk.KeyIDKey, keyID) - require.NoError(t, err) - - pub, err := jwk.FromRaw(rsaKey.PublicKey) - require.NoError(t, err) - - err = pub.Set(jwk.KeyIDKey, keyID) - require.NoError(t, err) - err = pub.Set(jwk.AlgorithmKey, keyAlg) - require.NoError(t, err) - - return priv, pub -} - -func newJWT(t *testing.T, key jwk.Key, builder *jwt.Builder) string { - token, err := builder.Claim(jwk.KeyIDKey, key.KeyID()).Build() - require.NoError(t, err) - signed, err := jwt.Sign(token, jwt.WithKey(keyAlg, key)) - require.NoError(t, err) - return string(signed) -} - -func requireSessionErrorResponse(t *testing.T, resp *envoy.CheckResponse) { - require.Equal(t, int32(codes.Unauthenticated), resp.GetStatus().GetCode()) - require.Equal(t, "There was an error accessing your session data. Try again later.", resp.GetDeniedResponse().GetBody()) -} - -func requireStoredTokens(t *testing.T, store oidc.SessionStore, sessionID string, wantExists bool) { - got, err := store.GetTokenResponse(context.Background(), sessionID) - require.NoError(t, err) - if wantExists { - require.NotNil(t, got) - } else { - require.Nil(t, got) - } -} - -func requireStoredState(t *testing.T, store oidc.SessionStore, sessionID string, wantExists bool) { - got, err := store.GetAuthorizationState(context.Background(), sessionID) - require.NoError(t, err) - if wantExists { - require.NotNil(t, got) - } else { - require.Nil(t, got) - } -} - -func requireRedirectResponse(t *testing.T, response *envoy.DeniedHttpResponse, wantRedirectBaseURI string, wantRedirectParams url.Values) { - var locationHeader string - for _, header := range response.GetHeaders() { - if header.GetHeader().GetKey() == inthttp.HeaderLocation { - locationHeader = header.GetHeader().GetValue() - } - } - - require.Equal(t, typev3.StatusCode_Found, response.GetStatus().GetCode()) - got, err := url.Parse(locationHeader) - require.NoError(t, err) - - require.Equal(t, wantRedirectBaseURI, got.Scheme+"://"+got.Host+got.Path) - - gotParams := got.Query() - for k, v := range wantRedirectParams { - require.Equal(t, v, gotParams[k]) - } - require.Len(t, gotParams, len(wantRedirectParams)) -} - -func requireCookie(t *testing.T, response *envoy.DeniedHttpResponse) { - var cookieHeader string - for _, header := range response.GetHeaders() { - if header.GetHeader().GetKey() == inthttp.HeaderSetCookie { - cookieHeader = header.GetHeader().GetValue() - } - } - require.Equal(t, "__Host-authservice-session-id-cookie=new-session-id; HttpOnly; Secure; SameSite=Lax; Path=/", cookieHeader) -} - -func requireDeleteCookie(t *testing.T, response *envoy.DeniedHttpResponse) { - var cookieHeader string - for _, header := range response.GetHeaders() { - if header.GetHeader().GetKey() == inthttp.HeaderSetCookie { - cookieHeader = header.GetHeader().GetValue() - } - } - require.Equal(t, "__Host-authservice-session-id-cookie=deleted; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0", cookieHeader) -} - -func requireTokensInResponse(t *testing.T, resp *envoy.OkHttpResponse, cfg *oidcv1.OIDCConfig, idToken, accessToken string) { - var ( - gotIDToken, gotAccessToken string - wantIDToken, wantAccessToken string - ) - - wantIDToken = encodeHeaderValue(cfg.GetIdToken().GetPreamble(), idToken) - if cfg.GetAccessToken() != nil { - wantAccessToken = encodeHeaderValue(cfg.GetAccessToken().GetPreamble(), accessToken) - } - - for _, header := range resp.GetHeaders() { - if header.GetHeader().GetKey() == cfg.GetIdToken().GetHeader() { - gotIDToken = header.GetHeader().GetValue() - } - if header.GetHeader().GetKey() == cfg.GetAccessToken().GetHeader() { - gotAccessToken = header.GetHeader().GetValue() - } - } - - require.Equal(t, wantIDToken, gotIDToken) - if cfg.GetAccessToken() != nil { - require.Equal(t, wantAccessToken, gotAccessToken) - } else { - require.Empty(t, gotAccessToken) - } -} - -func requireStandardResponseHeaders(t *testing.T, resp *envoy.CheckResponse) { - for _, header := range resp.GetDeniedResponse().GetHeaders() { - if header.GetHeader().GetKey() == inthttp.HeaderCacheControl { - require.EqualValues(t, inthttp.HeaderCacheControlNoCache, header.GetHeader().GetValue()) - } - if header.GetHeader().GetKey() == inthttp.HeaderPragma { - require.EqualValues(t, inthttp.HeaderPragmaNoCache, header.GetHeader().GetValue()) - } - } -} - -func newConfigFor(oidc *oidcv1.OIDCConfig) *configv1.Config { - return &configv1.Config{ - Chains: []*configv1.FilterChain{ - {Filters: []*configv1.Filter{{Type: &configv1.Filter_Oidc{Oidc: oidc}}}}, - }, - } -} - -// idpServer is a mock IDP server that can be used to test the OIDC handler. -// It listens on a bufconn.Listener and provides a http.Client that can be used to make requests to it. -// It returns a predefined response when the /token endpoint is called, that can be set using the tokensResponse field. -type idpServer struct { - server *http.Server - listener *bufconn.Listener - tokensResponse *idpTokensResponse - statusCode int -} - -func newServer() *idpServer { - s := &http.Server{} - idpServer := &idpServer{server: s, listener: bufconn.Listen(1024)} - - handler := http.NewServeMux() - handler.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(idpServer.statusCode) - - if idpServer.statusCode == http.StatusOK && idpServer.tokensResponse != nil { - err := json.NewEncoder(w).Encode(idpServer.tokensResponse) - if err != nil { - http.Error(w, fmt.Errorf("cannot json encode id_token: %w", err).Error(), http.StatusInternalServerError) - } - } - }) - handler.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(wellKnownURIs)) - }) - s.Handler = handler - return idpServer -} - -// Start starts the server in a goroutine. -func (s *idpServer) Start() { - go func() { _ = s.server.Serve(s.listener) }() -} - -// Stop stops the server. -func (s *idpServer) Stop() { - _ = s.listener.Close() -} - -// newHTTPClient returns a new http.Client that can be used to make requests to the server via the bufconn.Listener. -func (s *idpServer) newHTTPClient() *http.Client { - return &http.Client{ - Transport: &http.Transport{ - DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) { - return s.listener.DialContext(ctx) - }, - }, - } -} - -const ( - setTokenResponse = iota - getTokenResponse - setAuthorizationState - getAuthorizationState - clearAuthorizationState - removeSession - removeAllExpired -) - -var ( - _ oidc.SessionStore = &storeMock{} - - errStore = errors.New("store error") -) - -// storeMock is a mock implementation of oidc.SessionStore that allows to configure when a method must fail with an error. -type storeMock struct { - delegate oidc.SessionStore - errs map[int]bool -} - -// SetTokenResponse Implements oidc.SessionStore. -func (s *storeMock) SetTokenResponse(ctx context.Context, sessionID string, tokenResponse *oidc.TokenResponse) error { - if s.errs[setTokenResponse] { - return errStore - } - return s.delegate.SetTokenResponse(ctx, sessionID, tokenResponse) -} - -// GetTokenResponse Implements oidc.SessionStore. -func (s *storeMock) GetTokenResponse(ctx context.Context, sessionID string) (*oidc.TokenResponse, error) { - if s.errs[getTokenResponse] { - return nil, errStore - } - return s.delegate.GetTokenResponse(ctx, sessionID) -} - -// SetAuthorizationState Implements oidc.SessionStore. -func (s *storeMock) SetAuthorizationState(ctx context.Context, sessionID string, authorizationState *oidc.AuthorizationState) error { - if s.errs[setAuthorizationState] { - return errStore - } - return s.delegate.SetAuthorizationState(ctx, sessionID, authorizationState) -} - -// GetAuthorizationState Implements oidc.SessionStore. -func (s *storeMock) GetAuthorizationState(ctx context.Context, sessionID string) (*oidc.AuthorizationState, error) { - if s.errs[getAuthorizationState] { - return nil, errStore - } - return s.delegate.GetAuthorizationState(ctx, sessionID) -} - -// ClearAuthorizationState Implements oidc.SessionStore. -func (s *storeMock) ClearAuthorizationState(ctx context.Context, sessionID string) error { - if s.errs[clearAuthorizationState] { - return errStore - } - return s.delegate.ClearAuthorizationState(ctx, sessionID) -} - -// RemoveSession Implements oidc.SessionStore. -func (s *storeMock) RemoveSession(ctx context.Context, sessionID string) error { - if s.errs[removeSession] { - return errStore - } - return s.delegate.RemoveSession(ctx, sessionID) -} - -// RemoveAllExpired Implements oidc.SessionStore. -func (s *storeMock) RemoveAllExpired(ctx context.Context) error { - if s.errs[removeAllExpired] { - return errStore - } - return s.delegate.RemoveAllExpired(ctx) -} - -var _ oidc.SessionStoreFactory = &mockSessionStoreFactory{} - -// mockSessionStoreFactory is a mock implementation of oidc.SessionStoreFactory that returns a predefined store. -type mockSessionStoreFactory struct { - store oidc.SessionStore -} - -func (m mockSessionStoreFactory) Get(_ *oidcv1.OIDCConfig) oidc.SessionStore { - return m.store -} - -var _ oidc.JWKSProvider = jwksProviderFunc(nil) - -type jwksProviderFunc func() (jwk.Set, error) - -func (j jwksProviderFunc) Get(context.Context, *oidcv1.OIDCConfig) (jwk.Set, error) { - return j() -} diff --git a/internal/boolstr.go b/internal/boolstr.go deleted file mode 100644 index ed88c0a..0000000 --- a/internal/boolstr.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "strconv" - - "google.golang.org/protobuf/types/known/structpb" -) - -// BoolStrValue returns the bool value of a structpb.Value. -// It expects the input to be a structpb.Value of type string or bool that -// represents a boolean value. -// This method is a convenience method for backwards-compatibility with the -// previous versions of the authservice. -func BoolStrValue(v *structpb.Value) bool { - if v.GetStringValue() != "" { - b, _ := strconv.ParseBool(v.GetStringValue()) - return b - } - return v.GetBoolValue() -} diff --git a/internal/boolstr_test.go b/internal/boolstr_test.go deleted file mode 100644 index d01dd19..0000000 --- a/internal/boolstr_test.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "testing" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/structpb" -) - -func TestBoolStrValue(t *testing.T) { - tests := []struct { - name string - in *structpb.Value - want bool - }{ - {"empty", &structpb.Value{}, false}, - {"bool", &structpb.Value{Kind: &structpb.Value_BoolValue{BoolValue: true}}, true}, - {"string-true", &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "true"}}, true}, - {"string-false", &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "false"}}, false}, - {"string-invalid", &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "invalid"}}, false}, - {"type-mismatch", &structpb.Value{Kind: &structpb.Value_ListValue{}}, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, BoolStrValue(tt.in)) - }) - } -} diff --git a/internal/config.go b/internal/config.go deleted file mode 100644 index da82941..0000000 --- a/internal/config.go +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "errors" - "fmt" - "net/url" - "os" - "strings" - - "github.com/redis/go-redis/v9" - "github.com/tetratelabs/run" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" -) - -const ScopeOIDC = "openid" - -var ( - _ run.Config = (*LocalConfigFile)(nil) - - ErrInvalidPath = errors.New("invalid path") - ErrInvalidOIDCOverride = errors.New("invalid OIDC override") - ErrDuplicateOIDCConfig = errors.New("duplicate OIDC configuration") - ErrMultipleOIDCConfig = errors.New("multiple OIDC configurations") - ErrInvalidURL = errors.New("invalid URL") - ErrRequiredURL = errors.New("required URL") - ErrHealthPortInUse = errors.New("health port is already in use by listen port") - ErrMustNotBeRootPath = errors.New("must not be root path") - ErrMustBeDifferentPath = errors.New("must be different path") -) - -// LocalConfigFile is a run.Config that loads the configuration file. -type LocalConfigFile struct { - path string - Config configv1.Config -} - -// Name returns the name of the unit in the run.Group. -func (l *LocalConfigFile) Name() string { return "Local configuration file" } - -// FlagSet returns the flags used to customize the config file location. -func (l *LocalConfigFile) FlagSet() *run.FlagSet { - flags := run.NewFlagSet("Local Config File flags") - flags.StringVar(&l.path, "config-path", "/etc/authservice/config.json", "configuration file path") - return flags -} - -// Validate and load the configuration file. -func (l *LocalConfigFile) Validate() error { - if l.path == "" { - return ErrInvalidPath - } - - content, err := os.ReadFile(l.path) - if err != nil { - return err - } - - if err = protojson.Unmarshal(content, &l.Config); err != nil { - return err - } - - if l.Config.GetListenPort() == l.Config.GetHealthListenPort() { - return ErrHealthPortInUse - } - - // Validate the URLs before merging the OIDC configurations - if err = validateURLs(&l.Config); err != nil { - return err - } - - // Validate OIDC configuration overrides - for _, fc := range l.Config.Chains { - hasOidc := false - for _, f := range fc.Filters { - if l.Config.DefaultOidcConfig != nil && f.GetOidc() != nil { - return fmt.Errorf("%w: in chain %q OIDC filter and default OIDC configuration cannot be used together", - ErrDuplicateOIDCConfig, fc.Name) - } - if l.Config.DefaultOidcConfig == nil && f.GetOidcOverride() != nil { - return fmt.Errorf("%w: in chain %q OIDC override filter requires a default OIDC configuration", - ErrInvalidOIDCOverride, fc.Name) - } - if f.GetOidc() != nil || f.GetOidcOverride() != nil { - if hasOidc { - return fmt.Errorf("%w: only one OIDC configuration is allowed in a chain", ErrMultipleOIDCConfig) - } - hasOidc = true - } - } - } - - // Overrides for non-supported values - l.Config.Threads = 1 - - // Merge the OIDC overrides with the default OIDC configuration so that - // we can properly validate the settings and all filters have only one - // location where the OIDC configuration is defined. - if err = mergeAndValidateOIDCConfigs(&l.Config); err != nil { - return err - } - - // Now that all defaults are set and configurations are merged, validate all final settings - return l.Config.ValidateAll() -} - -// mergeAndValidateOIDCConfigs merges the OIDC overrides with the default OIDC configuration so that -// all filters have only one location where the OIDC configuration is defined. -func mergeAndValidateOIDCConfigs(cfg *configv1.Config) error { - var errs []error - - for _, fc := range cfg.Chains { - for _, f := range fc.Filters { - if _, ok := f.Type.(*configv1.Filter_Mock); ok { - continue - } - - // Merge the OIDC overrides and populate the normal OIDC field instead so that - // consumers of the config always have an up-to-date object - if f.GetOidcOverride() != nil { - oidc := proto.Clone(cfg.DefaultOidcConfig).(*oidcv1.OIDCConfig) - proto.Merge(oidc, f.GetOidcOverride()) - f.Type = &configv1.Filter_Oidc{Oidc: oidc} - } - - if f.GetOidc().GetConfigurationUri() == "" { - if f.GetOidc().GetAuthorizationUri() == "" { - errs = append(errs, fmt.Errorf("%w: missing authorization URI in chain %q", ErrRequiredURL, fc.Name)) - } - if f.GetOidc().GetTokenUri() == "" { - errs = append(errs, fmt.Errorf("%w: missing token URI in chain %q", ErrRequiredURL, fc.Name)) - } - if f.GetOidc().GetJwks() == "" && f.GetOidc().GetJwksFetcher().GetJwksUri() == "" { - errs = append(errs, fmt.Errorf("%w: missing JWKS URI in chain %q", ErrRequiredURL, fc.Name)) - } - } - - // Set the defaults - applyOIDCDefaults(f.GetOidc()) - - // validate the logout path is not the root path - if f.GetOidc().GetLogout() != nil { - if isRootPath(f.GetOidc().GetLogout().GetPath()) { - return fmt.Errorf("%w: invalid logout path", ErrMustNotBeRootPath) - } - } - - // validate the callback and the logout path are different - callbackURI, _ := url.Parse(f.GetOidc().GetCallbackUri()) - if f.GetOidc().GetLogout() != nil && callbackURI.Path == f.GetOidc().GetLogout().GetPath() { - errs = append(errs, fmt.Errorf("%w: callback and logout paths must be different in chain %q", ErrMustBeDifferentPath, fc.Name)) - } - } - } - // Clear the default config as it has already been merged. This way there is only one - // location for the OIDC settings. - cfg.DefaultOidcConfig = nil - - return errors.Join(errs...) -} - -func applyOIDCDefaults(config *oidcv1.OIDCConfig) { - if config.GetScopes() == nil { - config.Scopes = []string{ScopeOIDC} - } - for _, s := range config.GetScopes() { - if s == ScopeOIDC { - return - } - } - config.Scopes = append(config.Scopes, ScopeOIDC) -} - -func ConfigToJSONString(c *configv1.Config) string { - b, _ := protojson.Marshal(c) - return string(b) -} - -func validateURLs(config *configv1.Config) error { - if err := validateOIDCConfigURLs(config.DefaultOidcConfig); err != nil { - return fmt.Errorf("invalid default OIDC config: %w", err) - } - - for _, fc := range config.Chains { - for fi, f := range fc.Filters { - if f.GetOidc() != nil { - err := validateOIDCConfigURLs(f.GetOidc()) - if err != nil { - return fmt.Errorf("invalid OIDC config from chain[%s].filter[%d]: %w", fc.GetName(), fi, err) - } - } - if f.GetOidcOverride() != nil { - err := validateOIDCConfigURLs(f.GetOidcOverride()) - if err != nil { - return fmt.Errorf("invalid OIDC override from chain[%s].filter[%d]: %w", fc.GetName(), fi, err) - } - } - } - } - - return nil -} - -func validateOIDCConfigURLs(c *oidcv1.OIDCConfig) error { - if err := validateURL(c.GetProxyUri()); err != nil { - return fmt.Errorf("%w: invalid proxy URL: %w", ErrInvalidURL, err) - } - if err := validateURL(c.GetTokenUri()); err != nil { - return fmt.Errorf("%w: invalid token URL: %w", ErrInvalidURL, err) - } - if err := validateURL(c.GetConfigurationUri()); err != nil { - return fmt.Errorf("%w: invalid configuration URL: %w", ErrInvalidURL, err) - } - if err := validateURL(c.GetAuthorizationUri()); err != nil { - return fmt.Errorf("%w: invalid authorization URL: %w", ErrInvalidURL, err) - } - if err := validateURL(c.GetCallbackUri()); err != nil { - return fmt.Errorf("%w: invalid callback URL: %w", ErrInvalidURL, err) - } - if err := validateURL(c.GetJwksFetcher().GetJwksUri()); err != nil { - return fmt.Errorf("%w: invalid JWKS Fetcher URL: %w", ErrInvalidURL, err) - } - - // Backwards compatibility with redis tcp:// URIs used in the old authservice - if redisURI := c.GetRedisSessionStoreConfig().GetServerUri(); redisURI != "" { - c.GetRedisSessionStoreConfig().ServerUri = strings.Replace(redisURI, "tcp://", "redis://", 1) - } - - if redisURL := c.GetRedisSessionStoreConfig().GetServerUri(); redisURL != "" { - if _, err := redis.ParseURL(redisURL); err != nil { - return fmt.Errorf("%w: invalid Redis session store URL: %w", ErrInvalidURL, err) - } - } - - if hasRootPath(c.GetCallbackUri()) { - return fmt.Errorf("%w: invalid callback URL", ErrMustNotBeRootPath) - } - return nil -} - -func validateURL(u string) error { - if u == "" { - return nil - } - _, err := url.Parse(u) - return err -} - -// hasRootPath returns true if the path of the given URL is "/" or empty. -// prerequisite: u is a valid URL. -func hasRootPath(uri string) bool { - if uri == "" { - return false - } - parsed, _ := url.Parse(uri) - return isRootPath(parsed.Path) -} - -// isRootPath returns true if the path is "/" or empty. -func isRootPath(path string) bool { - return path == "/" || path == "" -} diff --git a/internal/config_test.go b/internal/config_test.go deleted file mode 100644 index ea69244..0000000 --- a/internal/config_test.go +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "fmt" - "os" - "testing" - - "github.com/stretchr/testify/require" - "github.com/tetratelabs/run" - "github.com/tetratelabs/telemetry" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/structpb" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - mockv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/mock" - oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" -) - -type errCheck struct { - is error - as error - msg string -} - -func (e errCheck) Check(t *testing.T, err error) { - switch { - case e.as != nil: - require.ErrorAs(t, err, &e.as) - case e.msg != "": - require.ErrorContains(t, err, e.msg) - default: - require.ErrorIs(t, err, e.is) - } -} - -const msgLengthValidation = "value length must be at least 1 runes" - -func TestValidateConfig(t *testing.T) { - tests := []struct { - name string - path string - check errCheck - }{ - {"empty", "", errCheck{is: ErrInvalidPath}}, - {"unexisting", "unexisting", errCheck{is: os.ErrNotExist}}, - {"invalid-config", "testdata/invalid-config.json", errCheck{msg: `unknown field "foo"`}}, - {"invalid-values", "testdata/invalid-values.json", errCheck{as: &configv1.ConfigMultiError{}}}, - {"duplicate-oidc", "testdata/duplicate-oidc.json", errCheck{is: ErrDuplicateOIDCConfig}}, - {"invalid-oidc-override", "testdata/invalid-oidc-override.json", errCheck{is: ErrInvalidOIDCOverride}}, - {"multiple-oidc", "testdata/multiple-oidc.json", errCheck{is: ErrMultipleOIDCConfig}}, - {"invalid-redis", "testdata/invalid-redis.json", errCheck{is: ErrInvalidURL}}, - {"invalid-oidc-uris", "testdata/invalid-oidc-uris.json", errCheck{is: ErrRequiredURL}}, - {"invalid-health-port", "testdata/invalid-health-port.json", errCheck{is: ErrHealthPortInUse}}, - {"invalid-callback-uri", "testdata/invalid-callback.json", errCheck{is: ErrMustNotBeRootPath}}, - {"invalid-logout-path", "testdata/invalid-logout.json", errCheck{is: ErrMustNotBeRootPath}}, - {"valid-logout-override-default", "testdata/valid-logout-override-default.json", errCheck{is: nil}}, - {"invalid-callback-and-logout-path", "testdata/invalid-callback-logout.json", errCheck{is: ErrMustBeDifferentPath}}, - {"oidc-dynamic", "testdata/oidc-dynamic.json", errCheck{is: nil}}, - {"valid", "testdata/mock.json", errCheck{is: nil}}, - {"invalid-oidc-client-secret", "testdata/invalid-oidc-client-secret.json", errCheck{msg: msgLengthValidation}}, - {"invalid-oidc-client-secret-ref", "testdata/invalid-oidc-client-secret-ref.json", errCheck{msg: msgLengthValidation}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := (&LocalConfigFile{path: tt.path}).Validate() - tt.check.Check(t, err) - }) - } -} - -func TestValidateURLs(t *testing.T) { - const ( - validURL = "http://fake/path" - invalidURL = "ht tp://invalid" - validRedisURL = "redis://localhost:6379/0" - ) - - urlTests := []struct { - name string - oidCCfg *oidcv1.OIDCConfig - check errCheck - }{ - {"empty", &oidcv1.OIDCConfig{}, errCheck{is: nil}}, - { - "invalid-redis", - &oidcv1.OIDCConfig{RedisSessionStoreConfig: &oidcv1.RedisConfig{ServerUri: invalidURL}}, - errCheck{is: ErrInvalidURL}, - }, - { - "invalid-jwks-fetcher", - &oidcv1.OIDCConfig{ - JwksConfig: &oidcv1.OIDCConfig_JwksFetcher{ - JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{JwksUri: invalidURL}, - }, - }, - errCheck{is: ErrInvalidURL}, - }, - {"invalid-proxy-uri", &oidcv1.OIDCConfig{ProxyUri: invalidURL}, errCheck{is: ErrInvalidURL}}, - {"invalid-token-uri", &oidcv1.OIDCConfig{TokenUri: invalidURL}, errCheck{is: ErrInvalidURL}}, - {"invalid-authorization-uri", &oidcv1.OIDCConfig{AuthorizationUri: invalidURL}, errCheck{is: ErrInvalidURL}}, - {"invalid-callback-uri", &oidcv1.OIDCConfig{CallbackUri: invalidURL}, errCheck{is: ErrInvalidURL}}, - { - "valid", - &oidcv1.OIDCConfig{ - ProxyUri: validURL, AuthorizationUri: validURL, TokenUri: validURL, CallbackUri: validURL, - JwksConfig: &oidcv1.OIDCConfig_JwksFetcher{JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{JwksUri: validURL}}, - RedisSessionStoreConfig: &oidcv1.RedisConfig{ServerUri: validRedisURL}, - }, - errCheck{is: nil}, - }, - } - - configTests := []struct { - name string - cfg func(*oidcv1.OIDCConfig) *configv1.Config - }{ - { - "default", - func(oidcCfg *oidcv1.OIDCConfig) *configv1.Config { - return &configv1.Config{DefaultOidcConfig: oidcCfg} - }, - }, - { - "chain-oidc", - func(oidcCfg *oidcv1.OIDCConfig) *configv1.Config { - return &configv1.Config{ - Chains: []*configv1.FilterChain{{Filters: []*configv1.Filter{{Type: &configv1.Filter_Oidc{Oidc: oidcCfg}}}}}, - } - }, - }, - { - "chain-oidc-override", - func(oidcCfg *oidcv1.OIDCConfig) *configv1.Config { - return &configv1.Config{ - Chains: []*configv1.FilterChain{{Filters: []*configv1.Filter{{Type: &configv1.Filter_OidcOverride{OidcOverride: oidcCfg}}}}}, - } - }, - }, - } - - for _, ct := range configTests { - t.Run(ct.name, func(t *testing.T) { - for _, tt := range urlTests { - t.Run(tt.name, func(t *testing.T) { - cfg := ct.cfg(tt.oidCCfg) - tt.check.Check(t, validateURLs(cfg)) - }) - } - }) - } -} - -func TestLoadMock(t *testing.T) { - want := &configv1.Config{ - ListenAddress: "0.0.0.0", - ListenPort: 8080, - LogLevel: "debug", - Threads: 1, - Chains: []*configv1.FilterChain{ - { - Name: "mock", - Filters: []*configv1.Filter{ - { - Type: &configv1.Filter_Mock{ - Mock: &mockv1.MockConfig{ - Allow: true, - }, - }, - }, - }, - }, - }, - } - - var cfg LocalConfigFile - g := run.Group{Logger: telemetry.NoopLogger()} - g.Register(&cfg) - err := g.Run("", "--config-path", "testdata/mock.json") - - require.NoError(t, err) - require.True(t, proto.Equal(want, &cfg.Config)) -} - -func TestLoadOIDC(t *testing.T) { - want := &configv1.Config{ - ListenAddress: "0.0.0.0", - ListenPort: 8080, - LogLevel: "debug", - Threads: 1, - Chains: []*configv1.FilterChain{ - { - Name: "oidc", - Filters: []*configv1.Filter{ - { - Type: &configv1.Filter_Oidc{ - Oidc: &oidcv1.OIDCConfig{ - AuthorizationUri: "http://fake", - TokenUri: "http://fake", - CallbackUri: "http://fake/callback", - JwksConfig: &oidcv1.OIDCConfig_JwksFetcher{ - JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{ - JwksUri: "http://fake/jwks", - SkipVerifyPeerCert: structpb.NewStringValue("true"), - }, - }, - ClientId: "fake-client-id", - ClientSecretConfig: &oidcv1.OIDCConfig_ClientSecret{ClientSecret: "fake-client-secret"}, - CookieNamePrefix: "", - IdToken: &oidcv1.TokenConfig{Preamble: "Bearer", Header: "authorization"}, - ProxyUri: "http://fake", - RedisSessionStoreConfig: &oidcv1.RedisConfig{ServerUri: "redis://localhost:6379/0"}, - Scopes: []string{ScopeOIDC}, - Logout: &oidcv1.LogoutConfig{Path: "/logout", RedirectUri: "http://fake"}, - TrustedCaConfig: &oidcv1.OIDCConfig_TrustedCertificateAuthority{TrustedCertificateAuthority: "fake-ca-pem"}, - SkipVerifyPeerCert: structpb.NewBoolValue(true), - }, - }, - }, - }, - }, - }, - } - - for _, tc := range []string{"oidc", "oidc-override"} { - t.Run(tc, func(t *testing.T) { - var cfg LocalConfigFile - g := run.Group{Logger: telemetry.NoopLogger()} - g.Register(&cfg) - err := g.Run("", "--config-path", fmt.Sprintf("testdata/%s.json", tc)) - - require.NoError(t, err) - require.True(t, proto.Equal(want, &cfg.Config)) - }) - } -} - -func TestConfigToJSONString(t *testing.T) { - tests := []struct { - name string - config *configv1.Config - want string - }{ - {"nil", nil, "{}"}, - {"empty", &configv1.Config{}, "{}"}, - {"simple", &configv1.Config{ListenPort: 8080}, `{"listenPort":8080}`}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := ConfigToJSONString(tt.config) - require.JSONEq(t, tt.want, got) - }) - } -} diff --git a/internal/file.go b/internal/file.go deleted file mode 100644 index f183976..0000000 --- a/internal/file.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "context" - "os" - "sync" - "time" - - "github.com/tetratelabs/telemetry" -) - -type ( - // FileWatcher watches multiple files for changes and calls a callback when the file changes. - // It is safe to call WatchFile concurrently. - // To stop watching the files, cancel the context passed to NewFileWatcher. - FileWatcher struct { - ctx context.Context - log telemetry.Logger - - mu sync.Mutex - watchers map[string]*watcher - } - - // watcher watches a file for changes and calls a callback when the file changes. - watcher struct { - ctx context.Context - cancel context.CancelFunc - - log telemetry.Logger - interval time.Duration - callback func([]byte) - reader Reader - data []byte - } - - // Reader is an interface to read the content of a file. - Reader interface { - // ID returns a unique identifier for the file. - ID() string - // Read reads the content of the file. - Read() ([]byte, error) - } -) - -// NewFileWatcher creates a new FileWatcher. -func NewFileWatcher(ctx context.Context) *FileWatcher { - return &FileWatcher{ - ctx: ctx, - log: Logger(Config), - watchers: map[string]*watcher{}, - } -} - -// WatchFile watches a file for changes and calls the callback when the file changes. -// It returns the content of the file and an error if the file cannot be read. -// The callback function is called with the new content of the file. -// If the file is already being watched, the previous watcher is stopped and the new one is started. -func (f *FileWatcher) WatchFile(reader Reader, interval time.Duration, callback func([]byte)) ([]byte, error) { - id := reader.ID() - - f.mu.Lock() - if old, ok := f.watchers[id]; ok { - // stop the current watcher - old.cancel() - } - f.mu.Unlock() - - log := f.log.With("file", id) - - // Load the file data - data, err := reader.Read() - if err != nil { - log.Error("error reading file", err) - return nil, err - } - - // Non-positive interval means no watching for file. - if interval <= 0 { - return data, nil - } - - // Create a new watcher - f.mu.Lock() - ctx, cancel := context.WithCancel(f.ctx) - w := &watcher{ - ctx: ctx, - cancel: cancel, - log: log, - interval: interval, - callback: callback, - reader: reader, - data: data, - } - f.watchers[id] = w - f.mu.Unlock() - - // Start watching the file - w.start() - return data, nil -} - -func (w *watcher) start() { - go func() { - w.log.Info("start file watcher") - - ticker := time.NewTicker(w.interval) - defer ticker.Stop() - for { - select { - case <-w.ctx.Done(): - w.log.Info("stop file watcher") - return - - case <-ticker.C: - data, err := w.reader.Read() - if err != nil { - w.log.Error("error reading file", err) - continue - } - if string(data) != string(w.data) { - w.log.Info("file changed, invoking callback") - w.data = data - go w.callback(data) - } - } - } - }() -} - -var _ Reader = (*FileReader)(nil) - -// FileReader is a Reader that reads the content of a file given its path. -type FileReader struct { - filePath string -} - -// NewFileReader creates a new FileReader. -func NewFileReader(filePath string) *FileReader { - return &FileReader{filePath: filePath} -} - -// ID returns the file path. -func (f *FileReader) ID() string { - return f.filePath -} - -// Read reads the content of the file. -func (f *FileReader) Read() ([]byte, error) { - return os.ReadFile(f.filePath) -} diff --git a/internal/file_test.go b/internal/file_test.go deleted file mode 100644 index 20291d5..0000000 --- a/internal/file_test.go +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "context" - "errors" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestFileWatcher_WatchFile(t *testing.T) { - const watcherInterval = 500 * time.Millisecond - - tests := []struct { - name string - fileReader *mockReader - genUpdates func(reader *mockReader) - interval time.Duration - - wantCallbacks int - want string - wantUpdates []string - wantErr bool - }{ - { - name: "no updates happening", - fileReader: newMockReader("test", "original", nil), - interval: watcherInterval, - wantCallbacks: 0, - want: "original", - }, - { - name: "all updates notified", - fileReader: newMockReader("test", "original", nil), - genUpdates: func(reader *mockReader) { - reader.setData([]byte("update 1")) - reader.waitForRead() - reader.setData([]byte("update 2")) - reader.waitForRead() - }, - interval: watcherInterval, - wantCallbacks: 2, - want: "original", - wantUpdates: []string{"update 1", "update 2"}, - }, - { - name: "no content changes don't notify", - fileReader: newMockReader("test", "original", nil), - genUpdates: func(reader *mockReader) { - reader.setData([]byte("update 1")) - reader.waitForRead() - reader.setData([]byte("update 2")) - reader.waitForRead() - reader.setData([]byte("update 2")) - reader.waitForRead() - reader.setData([]byte("update 2")) - reader.waitForRead() - }, - interval: watcherInterval, - wantCallbacks: 2, - want: "original", - wantUpdates: []string{"update 1", "update 2"}, - }, - { - name: "missed update due to slow interval", - fileReader: newMockReader("test", "original", nil), - genUpdates: func(reader *mockReader) { - reader.setData([]byte("update 1")) - // no waiting for the read to happen and performing next update - // reader.waitForRead() - reader.setData([]byte("update 2")) - reader.waitForRead() - }, - interval: watcherInterval, - wantCallbacks: 1, - want: "original", - wantUpdates: []string{"update 2"}, - }, - { - name: "error reading file at start", - fileReader: newMockReader("test", "original", errors.New("error reading file")), - interval: watcherInterval, - wantErr: true, - }, - { - name: "error reading file after start", - fileReader: newMockReader("test", "original", nil), - genUpdates: func(reader *mockReader) { - reader.setData([]byte("update 1")) - reader.waitForRead() - reader.setErr(errors.New("error reading file")) - reader.waitForRead() - // stop error - reader.setErr(nil) - // even if an error happens, next updates should be notified - reader.setData([]byte("update 2")) - reader.waitForRead() - }, - interval: watcherInterval, - wantCallbacks: 2, - want: "original", - wantUpdates: []string{"update 1", "update 2"}, - }, - { - name: "no interval", - fileReader: newMockReader("test", "original", nil), - genUpdates: func(reader *mockReader) { - reader.setData([]byte("update 1")) - reader.setData([]byte("update 2")) - }, - interval: 0, - want: "original", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - fw := NewFileWatcher(ctx) - - mu := sync.Mutex{} - var gotUpdates []string - - wg := sync.WaitGroup{} - wg.Add(tt.wantCallbacks) - - got, err := fw.WatchFile(tt.fileReader, tt.interval, func(data []byte) { - defer wg.Done() - mu.Lock() - gotUpdates = append(gotUpdates, string(data)) - mu.Unlock() - }) - if tt.wantErr { - require.Error(t, err) - return - } - - if tt.interval <= 0 { - // if no interval configured, the watcher shouldn't be registered - _, ok := fw.watchers[tt.fileReader.ID()] - require.False(t, ok) - } - - tt.fileReader.waitForRead() // Wait for the first read to happen, the one synchronous - require.Equal(t, tt.want, string(got)) - require.NoError(t, err) - - if tt.genUpdates != nil { - tt.genUpdates(tt.fileReader) - } - - // ensure no more updates are notified before verifying the results - cancel() - - wg.Wait() // Wait for all callbacks to be notified - require.Equal(t, tt.wantUpdates, gotUpdates) - }) - } - - // This test is to ensure that the file watcher can handle multiple files being watched and - // each callback is notified only for the file it's watching. - t.Run("multiple files watched", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - fw := NewFileWatcher(ctx) - - mu1 := sync.Mutex{} - gotUpdates1 := make([]string, 0) - mu2 := sync.Mutex{} - gotUpdates2 := make([]string, 0) - - wg1 := sync.WaitGroup{} - wg1.Add(2) // 2 callbacks to be notified - wg2 := sync.WaitGroup{} - wg2.Add(1) // 1 callback to be notified - - file1 := newMockReader("test1", "original1", nil) - got1, err := fw.WatchFile(file1, watcherInterval, func(data []byte) { - defer wg1.Done() - mu1.Lock() - gotUpdates1 = append(gotUpdates1, string(data)) - mu1.Unlock() - }) - require.NoError(t, err) - file1.waitForRead() // Wait for the first read to happen - - file2 := newMockReader("test2", "original2", nil) - got2, err := fw.WatchFile(file2, watcherInterval, func(data []byte) { - defer wg2.Done() - mu2.Lock() - gotUpdates2 = append(gotUpdates2, string(data)) - mu2.Unlock() - }) - require.NoError(t, err) - file2.waitForRead() // Wait for the first read to happen - - file1.setData([]byte("update 1-1")) - file1.waitForRead() - file2.setData([]byte("update 2-1")) - file2.waitForRead() - file1.setData([]byte("update 1-2")) - file1.waitForRead() - - // ensure no more updates are notified before verifying the results - cancel() - - wg1.Wait() // Wait for all callbacks to be notified - wg2.Wait() // Wait for all callbacks to be notified - - require.Equal(t, "original1", string(got1)) - require.Equal(t, "original2", string(got2)) - require.Equal(t, []string{"update 1-1", "update 1-2"}, gotUpdates1) - require.Equal(t, []string{"update 2-1"}, gotUpdates2) - }) - - // This test is to ensure that the callback is overridden when a new file is watched - // The first WatchFile sets a callback, that will only receive the first update happening at the WatchFile call. - // Then the second WatchFile sets a new callback, that will receive all updates happening after the WatchFile call. - t.Run("override file watcher overrides callback too", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - fw := NewFileWatcher(ctx) - - muU := sync.Mutex{} - gotUpdates := make([]string, 0) - muO := sync.Mutex{} - gotOverride := make([]string, 0) - - file1 := newMockReader("test1", "original", nil) - got, err := fw.WatchFile(file1, watcherInterval, func(data []byte) { - muU.Lock() - gotUpdates = append(gotUpdates, string(data)) - muU.Unlock() - }) - require.NoError(t, err) - file1.waitForRead() // Wait for the first read to happen - - wg := sync.WaitGroup{} - wg.Add(2) // 2 callbacks to be notified - - file1.setData([]byte("override")) - gotOvrr, err := fw.WatchFile(file1, watcherInterval/2, func(data []byte) { - defer wg.Done() - muO.Lock() - gotOverride = append(gotOverride, string(data)) - muO.Unlock() - }) - require.NoError(t, err) - file1.waitForRead() // Wait for the first read to happen again - - file1.setData([]byte("update 1")) - file1.waitForRead() - file1.setData([]byte("update 2")) - file1.waitForRead() - - // ensure no more updates are notified before verifying the results - cancel() - - wg.Wait() // Wait for all callbacks to be notified - - require.Equal(t, "original", string(got)) - require.Equal(t, "override", string(gotOvrr)) - require.Equal(t, []string{}, gotUpdates) - require.Equal(t, []string{"update 1", "update 2"}, gotOverride) - }) -} - -var _ Reader = (*mockReader)(nil) - -type mockReader struct { - id string - err error - - m sync.Mutex - fileData []byte - - // reads is used to signal that a read happened, it should be buffered to avoid deadlocks. - // It is used to know if a read happened but there's no need to block reads happening if no one is waiting for them. - reads chan struct{} -} - -func newMockReader(id, data string, err error) *mockReader { - return &mockReader{ - id: id, - fileData: []byte(data), - err: err, - reads: make(chan struct{}, 50), - } -} - -func (m *mockReader) ID() string { - return m.id -} - -func (m *mockReader) Read() ([]byte, error) { - // Notify that a read happened - defer func() { m.reads <- struct{}{} }() - - m.m.Lock() - defer m.m.Unlock() - - if m.err != nil { - return nil, m.err - } - - return m.fileData, nil -} - -func (m *mockReader) setData(data []byte) { - m.m.Lock() - defer m.m.Unlock() - m.fileData = data -} - -func (m *mockReader) setErr(err error) { - m.m.Lock() - defer m.m.Unlock() - m.err = err -} - -func (m *mockReader) waitForRead() { - <-m.reads -} diff --git a/internal/fips_disabled.go b/internal/fips_disabled.go deleted file mode 100644 index 1e00d60..0000000 --- a/internal/fips_disabled.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !boringcrypto - -package internal - -// LogFIPS logs whether FIPS is enabled or not. -func LogFIPS() { - Logger(Default).Info("FIPS: boringcrypto", "enabled", false) -} diff --git a/internal/fips_enabled.go b/internal/fips_enabled.go deleted file mode 100644 index 53b11d8..0000000 --- a/internal/fips_enabled.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build boringcrypto - -package internal - -import "crypto/boring" - -// LogFIPS logs whether FIPS is enabled or not. -func LogFIPS() { - Logger(Default).Info("FIPS: boringcrypto", "enabled", boring.Enabled()) -} diff --git a/internal/http/headers.go b/internal/http/headers.go deleted file mode 100644 index 9a38d60..0000000 --- a/internal/http/headers.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package http - -const ( - HeaderAuthorization = "authorization" - HeaderCacheControl = "cache-control" - HeaderContentType = "content-type" - HeaderCookie = "cookie" - HeaderLocation = "location" - HeaderPragma = "pragma" - HeaderSetCookie = "set-cookie" - - HeaderCacheControlNoCache = "no-cache" - - HeaderContentTypeFormURLEncoded = "application/x-www-form-urlencoded" - - HeaderPragmaNoCache = "no-cache" - - HeaderSetCookieSecure = "Secure" - HeaderSetCookieHTTPOnly = "HttpOnly" - HeaderSetCookieSameSiteStrict = "SameSite=Strict" - HeaderSetCookieSameSiteLax = "SameSite=Lax" - HeaderSetCookieMaxAge = "Max-Age" -) diff --git a/internal/http/http.go b/internal/http/http.go deleted file mode 100644 index 647b404..0000000 --- a/internal/http/http.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package http - -import ( - "encoding/base64" - "strings" -) - -// GetPathQueryFragment splits the given path into path, query, and fragment. -// See https://tools.ietf.org/html/rfc3986#section-3.4 and https://tools.ietf.org/html/rfc3986#section-3.5 for more information. -func GetPathQueryFragment(fullPath string) (path string, query string, fragment string) { - // inter and hash hold the index of the first `?` and `#` respectively - // `?` must be present before `#` if both are present to consider the query - var inter, hash int - - hash = strings.Index(fullPath, "#") - if hash != -1 { - inter = strings.Index(fullPath[:hash], "?") - } else { - inter = strings.Index(fullPath, "?") - } - - switch { - case inter != -1 && hash != -1: - // both query and fragment defined - path = fullPath[:inter] - query = fullPath[inter+1 : hash] - fragment = fullPath[hash+1:] - case inter != -1: - // only query defined - path = fullPath[:inter] - query = fullPath[inter+1:] - case hash != -1: - // only fragment defined - path = fullPath[:hash] - fragment = fullPath[hash+1:] - default: - // neither query nor fragment defined - path = fullPath - } - - return -} - -// DecodeCookiesHeader parses the value of the Cookie header to find all the cookies set. -// It returns a map of name->value for all the found valid cookies. -func DecodeCookiesHeader(headerValue string) map[string]string { - cookies := make(map[string]string, 0) - for _, c := range strings.Split(headerValue, ";") { - parts := strings.Split(strings.TrimSpace(c), "=") - if len(parts) != 2 { - // invalid cookie it must be Name=Value - continue - } - cookies[parts[0]] = parts[1] - } - return cookies -} - -// EncodeCookieHeader builds the value of the Set-Cookie header from the given cookie name, value and directives. -func EncodeCookieHeader(name string, value string, directives []string) string { - b := strings.Builder{} - _, _ = b.WriteString(name + "=" + value) - for _, directive := range directives { - _, _ = b.WriteString("; " + directive) - } - return b.String() -} - -func BasicAuthHeader(id string, secret string) string { - return "Basic " + base64.StdEncoding.EncodeToString([]byte(id+":"+secret)) -} diff --git a/internal/http/http_test.go b/internal/http/http_test.go deleted file mode 100644 index 2a79502..0000000 --- a/internal/http/http_test.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package http - -import ( - "encoding/base64" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestGetPathQueryFragment(t *testing.T) { - type want struct { - path, query, fragment string - } - - tests := []struct { - path string - want want - }{ - {"/", want{path: "/", query: "", fragment: ""}}, - {"/path?query#fragment", want{path: "/path", query: "query", fragment: "fragment"}}, - {"/path", want{path: "/path", query: "", fragment: ""}}, - {"/path?query", want{path: "/path", query: "query", fragment: ""}}, - {"/path#fragment", want{path: "/path", query: "", fragment: "fragment"}}, - {"/?query#fragment", want{path: "/", query: "query", fragment: "fragment"}}, - {"/#fragment", want{path: "/", query: "", fragment: "fragment"}}, - {"/?query", want{path: "/", query: "query", fragment: ""}}, - {"/path?", want{path: "/path", query: "", fragment: ""}}, - {"/path?#", want{path: "/path", query: "", fragment: ""}}, - {"/path?#fragment", want{path: "/path", query: "", fragment: "fragment"}}, - {"/path?query#", want{path: "/path", query: "query", fragment: ""}}, - {"/?que/ry", want{path: "/", query: "que/ry", fragment: ""}}, - {"/#frag/?ment", want{path: "/", query: "", fragment: "frag/?ment"}}, - {"/?query#frag/?ment", want{path: "/", query: "query", fragment: "frag/?ment"}}, - {"/?#", want{path: "/", query: "", fragment: ""}}, - {"/path#fragment?fragment/fragment", want{path: "/path", query: "", fragment: "fragment?fragment/fragment"}}, - {"/path?query/query#fragment/fragment?fragment", want{path: "/path", query: "query/query", fragment: "fragment/fragment?fragment"}}, - {"/path#fragment/fragment?fragment", want{path: "/path", query: "", fragment: "fragment/fragment?fragment"}}, - {"/#fragment/fragment?fragment", want{path: "/", query: "", fragment: "fragment/fragment?fragment"}}, - } - - for _, tt := range tests { - t.Run(tt.path, func(t *testing.T) { - path, query, fragment := GetPathQueryFragment(tt.path) - require.Equal(t, tt.want, want{path, query, fragment}) - }) - } -} - -func TestEncodeCookieHeader(t *testing.T) { - tests := []struct { - name, value string - directives []string - want string - }{ - { - "simple", "value", []string{}, - "simple=value", - }, - { - "with-directives", "value", []string{"1", "2", "3"}, - "with-directives=value; 1; 2; 3", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := EncodeCookieHeader(tt.name, tt.value, tt.directives) - require.Equal(t, tt.want, got) - }) - } -} - -func TestDecodeCookiesHeader(t *testing.T) { - tests := []struct { - cookies string - want map[string]string - }{ - { - "single=value", - map[string]string{"single": "value"}, - }, - { - "multiple=multiple-value; invalid; other=other-value", - map[string]string{ - "multiple": "multiple-value", - "other": "other-value", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.cookies, func(t *testing.T) { - got := DecodeCookiesHeader(tt.cookies) - require.Equal(t, tt.want, got) - }) - } -} - -func TestBasicAuthHeader(t *testing.T) { - tests := []struct { - id, secret string - want string - }{ - {"user", "password", "Basic dXNlcjpwYXNzd29yZA=="}, - {"user", "password with spaces", "Basic dXNlcjpwYXNzd29yZCB3aXRoIHNwYWNlcw=="}, - {"user with spaces", "password", "Basic dXNlciB3aXRoIHNwYWNlczpwYXNzd29yZA=="}, - {"user with spaces", "password with spaces", "Basic dXNlciB3aXRoIHNwYWNlczpwYXNzd29yZCB3aXRoIHNwYWNlcw=="}, - } - - for _, tt := range tests { - t.Run(tt.id+":"+tt.secret, func(t *testing.T) { - got := BasicAuthHeader(tt.id, tt.secret) - require.Equal(t, tt.want, got) - - got2, err := base64.StdEncoding.DecodeString(tt.want[6:]) - require.NoError(t, err) - require.Equal(t, tt.id+":"+tt.secret, string(got2)) - }) - } -} diff --git a/internal/k8s/secret_controller.go b/internal/k8s/secret_controller.go deleted file mode 100644 index f720551..0000000 --- a/internal/k8s/secret_controller.go +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package k8s - -import ( - "context" - "errors" - "fmt" - "os" - - "github.com/tetratelabs/run" - "github.com/tetratelabs/telemetry" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/config" - "sigs.k8s.io/controller-runtime/pkg/manager" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" - "github.com/tetrateio/authservice-go/internal" -) - -const clientSecretKey = "client-secret" - -var ( - _ run.PreRunner = (*SecretController)(nil) - _ run.ServiceContext = (*SecretController)(nil) - - ErrLoadingConfig = errors.New("error loading kube config") - ErrCrossNamespaceSecretRef = errors.New("cross-namespace secret reference is not allowed") -) - -// SecretController watches secrets for updates and updates the configuration with the loaded data. -type SecretController struct { - log telemetry.Logger - config *configv1.Config - secrets map[string][]*oidcv1.OIDCConfig - restConf *rest.Config - manager manager.Manager - k8sClient client.Client - namespace string -} - -// NewSecretController creates a new k8s Controller that loads secrets from -// Kubernetes and updates the configuration with the loaded data. -func NewSecretController(cfg *configv1.Config) *SecretController { - return &SecretController{ - log: internal.Logger(internal.Config), - config: cfg, - } -} - -// Name implements run.PreRunner -func (s *SecretController) Name() string { return "Secret controller" } - -// PreRun saves the original configuration in PreRun phase because the -// configuration is loaded from the file in the Config Validate phase. -func (s *SecretController) PreRun() error { - var ( - needWatchSecrets = false - err error - ) - - // Check if there are any k8s secrets to watch - for _, c := range s.config.Chains { - for _, f := range c.Filters { - oidcCfg, isOIDCConf := f.Type.(*configv1.Filter_Oidc) - if isOIDCConf && oidcCfg.Oidc.GetClientSecretRef() != nil { - needWatchSecrets = true - break - } - } - } - - // If there are no secrets to watch, we can skip starting the controller manager - if !needWatchSecrets { - return nil - } - - // Load the current namespace from the service account directory - if s.namespace == "" { - const namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" - var data []byte - data, err = os.ReadFile(namespaceFile) - if err != nil { - return fmt.Errorf("error reading namespace file %s: %w", namespaceFile, err) - } - s.namespace = string(data) - } - - // Collect the k8s secrets that are used in the configuration - if err = s.loadSecrets(); err != nil { - return err - } - - // Load the k8s configuration from in-cluster environment - if s.restConf == nil { - s.restConf, err = config.GetConfig() - if err != nil { - return fmt.Errorf("%w: %w", ErrLoadingConfig, err) - } - } - - // The controller manager is encapsulated in the secret controller because we - // only need it to watch secrets and update the configuration. - //TODO: Add manager options, like metrics, healthz, leader election, etc. - s.manager, err = ctrl.NewManager(s.restConf, ctrl.Options{ - Cache: cache.Options{ - DefaultNamespaces: map[string]cache.Config{ - s.namespace: {}, - }, - }, - }) - s.k8sClient = s.manager.GetClient() - if err != nil { - return fmt.Errorf("error creating controller manager: %w", err) - } - - if err = ctrl.NewControllerManagedBy(s.manager). - For(&corev1.Secret{}). - Complete(s); err != nil { - return fmt.Errorf("error creating secret controller:%w", err) - } - - return nil -} - -// ServeContext starts the controller manager and watches secrets for updates. -func (s *SecretController) ServeContext(ctx context.Context) error { - // If there are no secrets to watch, we can skip starting the controller manager - needWatchSecrets := len(s.secrets) != 0 - if !needWatchSecrets { - <-ctx.Done() - return nil - } - - if err := s.manager.Start(ctx); err != nil { - return fmt.Errorf("error starting controller manager: %w", err) - } - - return nil -} - -// loadSecrets loads the secrets from the configuration and stores them in the secrets map. -func (s *SecretController) loadSecrets() error { - s.secrets = make(map[string][]*oidcv1.OIDCConfig) - for _, c := range s.config.Chains { - for _, f := range c.Filters { - oidcCfg, isOIDCConf := f.Type.(*configv1.Filter_Oidc) - if !isOIDCConf || - oidcCfg.Oidc.GetClientSecretRef() == nil || - oidcCfg.Oidc.GetClientSecretRef().GetName() == "" { - continue - } - - ref := oidcCfg.Oidc.GetClientSecretRef() - if ref.Namespace != "" && ref.Namespace != s.namespace { - return fmt.Errorf("%w: secret reference namespace %s does not match the current namespace %s", - ErrCrossNamespaceSecretRef, ref.Namespace, s.namespace) - } - - key := secretNamespacedName(ref, s.namespace).String() - s.secrets[key] = append(s.secrets[key], oidcCfg.Oidc) - } - } - return nil -} - -func secretNamespacedName(secretRef *oidcv1.OIDCConfig_SecretReference, currentNamespace string) types.NamespacedName { - return types.NamespacedName{ - Namespace: currentNamespace, - Name: secretRef.GetName(), - } -} - -func (s *SecretController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - changedSecret := req.NamespacedName.String() - - oidcConfigs, exist := s.secrets[changedSecret] - - // If the secret is not used in the configuration, we can ignore it - if !exist { - return ctrl.Result{}, nil - } - - secret := new(corev1.Secret) - if err := s.k8sClient.Get(ctx, req.NamespacedName, secret); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } - - if !secret.DeletionTimestamp.IsZero() { - // Secret is being deleted, ignore it - return ctrl.Result{}, nil - } - - clientSecretBytes, ok := secret.Data[clientSecretKey] - if !ok || len(clientSecretBytes) == 0 { - s.log.Error("", errors.New("client-secret not found in secret"), "secret", secret) - // Do not return an error here, as trying to process the secret again - // will not help when the data is not present. - return ctrl.Result{}, nil - } - - for _, oidcConfig := range oidcConfigs { - s.log.Info("updating client-secret data from secret", - "secret", secret, "client-id", oidcConfig.GetClientId()) - - // Update the configuration with the loaded client secret - oidcConfig.ClientSecretConfig = &oidcv1.OIDCConfig_ClientSecret{ - ClientSecret: string(clientSecretBytes), - } - } - - return ctrl.Result{}, nil -} diff --git a/internal/k8s/secret_controller_lifecycle_test.go b/internal/k8s/secret_controller_lifecycle_test.go deleted file mode 100644 index 94c3ff3..0000000 --- a/internal/k8s/secret_controller_lifecycle_test.go +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package k8s - -import ( - "context" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/require" - "github.com/tetratelabs/run" - runtest "github.com/tetratelabs/run/pkg/test" - "github.com/tetratelabs/telemetry" - "github.com/tetratelabs/telemetry/scope" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/envtest" - "sigs.k8s.io/controller-runtime/pkg/manager" - - "github.com/tetrateio/authservice-go/internal" -) - -const ( - defaultWait = time.Second * 10 - defaultTick = time.Millisecond * 20 - defaultNamespace = "default" -) - -func TestErrorLoadingConfig(t *testing.T) { - t.Setenv("KUBECONFIG", "non-existent-file") - sc := NewSecretController(loadTestConf(t, "testdata/oidc-with-secret-ref.json")) - sc.namespace = defaultNamespace - - require.ErrorIs(t, sc.PreRun(), ErrLoadingConfig) -} - -func TestManagerStarts(t *testing.T) { - scope.UseLogger(telemetry.NoopLogger()) - ctrl.SetLogger(internal.NewLogrAdapter(telemetry.NoopLogger())) - - var ( - g run.Group - - irq = runtest.NewIRQService(func() {}) - cfg = internal.LocalConfigFile{} - controller = NewSecretController(&cfg.Config) - ) - - manual := &manualPreRun{ - preRunStarted: new(atomic.Bool), - finishPreRun: make(chan struct{}), - } - - controller.restConf = startEnv(t) - controller.namespace = defaultNamespace - g.Register(irq, &cfg, controller, manual) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - err := g.Run("", "--config-path", "testdata/oidc-with-secret-ref.json") - require.NoError(t, err) - }() - - // eventually, the controller's PreRun should be done - require.Eventually(t, func() bool { return manual.preRunStarted.Load() }, - defaultWait, defaultTick, "controller PreRun not done") - - // once preRun is done, the manager should initialize, and we can add our own test manager.Runnable - mgrStarted := &atomic.Bool{} - err := controller.manager.Add(manager.RunnableFunc(func(ctx context.Context) error { - mgrStarted.Store(true) - <-ctx.Done() - return nil - })) - require.NoError(t, err) - - // signale the prerun phase to complete - close(manual.finishPreRun) - - // at some point of serve phase, the manager should be started - t.Run("manager is started", func(t *testing.T) { - require.Eventually(t, func() bool { return mgrStarted.Load() }, - defaultWait, defaultTick, "manager not started") - }) - - // signal group termination and wait for it - require.NoError(t, irq.Close()) - wg.Wait() -} - -func TestManagerNotInitializedIfNothingToWatch(t *testing.T) { - scope.UseLogger(telemetry.NoopLogger()) - ctrl.SetLogger(internal.NewLogrAdapter(telemetry.NoopLogger())) - - var ( - g run.Group - - irq = runtest.NewIRQService(func() {}) - cfg = internal.LocalConfigFile{} - controller = NewSecretController(&cfg.Config) - ) - - manual := &manualService{ - serveStarted: new(atomic.Bool), - } - - controller.restConf = startEnv(t) - controller.namespace = defaultNamespace - g.Register(irq, &cfg, controller, manual) - - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - defer wg.Done() - _ = g.Run("", "--config-path", "testdata/oidc-without-secret-ref-in.json") - }() - - // wait for the run group to be fully started - require.Eventually(t, func() bool { return manual.serveStarted.Load() }, - defaultWait, defaultTick, "run group not fully started") - - // signal group termination and wait for it - require.NoError(t, irq.Close()) - wg.Wait() - - // Verify that the manager was not set - require.Nil(t, controller.manager) -} - -func startEnv(t *testing.T) *rest.Config { - env := &envtest.Environment{} - cfg, err := env.Start() - require.NoError(t, err) - - t.Cleanup(func() { - require.NoError(t, env.Stop()) - }) - return cfg -} - -var ( - _ run.PreRunner = (*manualPreRun)(nil) - _ run.ServiceContext = (*manualService)(nil) -) - -type ( - manualPreRun struct { - preRunStarted *atomic.Bool - finishPreRun chan struct{} - } - - manualService struct { - serveStarted *atomic.Bool - } -) - -func (l *manualPreRun) Name() string { - return "manual preRun" -} - -func (l *manualPreRun) PreRun() error { - l.preRunStarted.Store(true) - <-l.finishPreRun - return nil -} - -func (l *manualService) Name() string { - return "manual service" -} - -func (l *manualService) ServeContext(ctx context.Context) error { - l.serveStarted.Store(true) - <-ctx.Done() - return ctx.Err() -} diff --git a/internal/k8s/secret_controller_reconcile_test.go b/internal/k8s/secret_controller_reconcile_test.go deleted file mode 100644 index 273e9d1..0000000 --- a/internal/k8s/secret_controller_reconcile_test.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package k8s - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" -) - -func TestOIDCProcessWithKubernetesSecret(t *testing.T) { - tests := []struct { - name string - testFile string - err error - }{ - {"multiple secret refs", "oidc-with-multiple-secret-refs", nil}, - {"no secret ref", "oidc-without-secret-ref", nil}, - {"secret ref without data", "oidc-with-secret-ref-without-data", nil}, - {"secret ref deleting", "oidc-with-secret-ref-deleting", nil}, - {"secret ref not found", "oidc-with-secret-ref-not-found", nil}, - {"cross namespace secret ref", "oidc-with-cross-ns-secret-ref", ErrCrossNamespaceSecretRef}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // load test data - originalConf := loadTestConf(t, fmt.Sprintf("testdata/%s-in.json", tt.testFile)) - expectedConf := loadTestConf(t, fmt.Sprintf("testdata/%s-out.json", tt.testFile)) - - // create secret controller - secrets := secretsForTest() - kubeClient := fake.NewClientBuilder().WithLists(secrets).Build() - controller := NewSecretController(originalConf) - controller.namespace = "default" - controller.k8sClient = kubeClient // set the k8s client with the fake client for testing - require.ErrorIs(t, controller.loadSecrets(), tt.err) - - // reconcile the secrets - for _, secret := range secrets.Items { - _, err := controller.Reconcile(context.Background(), ctrl.Request{ - NamespacedName: types.NamespacedName{ - Namespace: secret.Namespace, - Name: secret.Name, - }, - }) - require.NoError(t, err) - } - _, err := controller.Reconcile(context.Background(), ctrl.Request{ - NamespacedName: types.NamespacedName{ - Namespace: "default", - Name: "non-existing-secret", - }, - }) - require.NoError(t, err) - - // check if the configuration is updated - require.True(t, proto.Equal(originalConf, expectedConf)) - }) - } -} - -func secretsForTest() *corev1.SecretList { - return &corev1.SecretList{ - Items: []corev1.Secret{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-secret-1", - }, - Data: map[string][]byte{ - clientSecretKey: []byte("fake-client-secret-1"), - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-secret-2", - }, - Data: map[string][]byte{ - clientSecretKey: []byte("fake-client-secret-2"), - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-secret-without-data", - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "test-secret-deleting", - DeletionTimestamp: &metav1.Time{Time: time.Now()}, - Finalizers: []string{"kubernetes"}, - }, - }, - }, - } -} - -func loadTestConf(t *testing.T, file string) *configv1.Config { - var conf = &configv1.Config{} - content, err := os.ReadFile(file) - require.NoError(t, err) - err = protojson.Unmarshal(content, conf) - require.NoError(t, err) - return conf -} diff --git a/internal/k8s/testdata/oidc-with-cross-ns-secret-ref-in.json b/internal/k8s/testdata/oidc-with-cross-ns-secret-ref-in.json deleted file mode 100644 index 8bbd7e8..0000000 --- a/internal/k8s/testdata/oidc-with-cross-ns-secret-ref-in.json +++ /dev/null @@ -1,43 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "namespace": "test", - "name": "test-secret-1" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-with-cross-ns-secret-ref-out.json b/internal/k8s/testdata/oidc-with-cross-ns-secret-ref-out.json deleted file mode 100644 index 8bbd7e8..0000000 --- a/internal/k8s/testdata/oidc-with-cross-ns-secret-ref-out.json +++ /dev/null @@ -1,43 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "namespace": "test", - "name": "test-secret-1" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-with-multiple-secret-refs-in.json b/internal/k8s/testdata/oidc-with-multiple-secret-refs-in.json deleted file mode 100644 index 553cc5d..0000000 --- a/internal/k8s/testdata/oidc-with-multiple-secret-refs-in.json +++ /dev/null @@ -1,121 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "namespace": "default", - "name": "test-secret-1" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - }, - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "name": "test-secret-2" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-with-multiple-secret-refs-out.json b/internal/k8s/testdata/oidc-with-multiple-secret-refs-out.json deleted file mode 100644 index 8b3b171..0000000 --- a/internal/k8s/testdata/oidc-with-multiple-secret-refs-out.json +++ /dev/null @@ -1,116 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret-1", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - }, - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret-2", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-with-secret-ref-deleting-in.json b/internal/k8s/testdata/oidc-with-secret-ref-deleting-in.json deleted file mode 100644 index c69d3d4..0000000 --- a/internal/k8s/testdata/oidc-with-secret-ref-deleting-in.json +++ /dev/null @@ -1,43 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "namespace": "default", - "name": "test-secret-deleting" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-with-secret-ref-deleting-out.json b/internal/k8s/testdata/oidc-with-secret-ref-deleting-out.json deleted file mode 100644 index c69d3d4..0000000 --- a/internal/k8s/testdata/oidc-with-secret-ref-deleting-out.json +++ /dev/null @@ -1,43 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "namespace": "default", - "name": "test-secret-deleting" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-with-secret-ref-not-found-in.json b/internal/k8s/testdata/oidc-with-secret-ref-not-found-in.json deleted file mode 100644 index ca34f15..0000000 --- a/internal/k8s/testdata/oidc-with-secret-ref-not-found-in.json +++ /dev/null @@ -1,43 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "namespace": "default", - "name": "non-existing-secret" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-with-secret-ref-not-found-out.json b/internal/k8s/testdata/oidc-with-secret-ref-not-found-out.json deleted file mode 100644 index ca34f15..0000000 --- a/internal/k8s/testdata/oidc-with-secret-ref-not-found-out.json +++ /dev/null @@ -1,43 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "namespace": "default", - "name": "non-existing-secret" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-with-secret-ref-without-data-in.json b/internal/k8s/testdata/oidc-with-secret-ref-without-data-in.json deleted file mode 100644 index 751c29a..0000000 --- a/internal/k8s/testdata/oidc-with-secret-ref-without-data-in.json +++ /dev/null @@ -1,43 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "namespace": "default", - "name": "test-secret-without-data" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-with-secret-ref-without-data-out.json b/internal/k8s/testdata/oidc-with-secret-ref-without-data-out.json deleted file mode 100644 index 751c29a..0000000 --- a/internal/k8s/testdata/oidc-with-secret-ref-without-data-out.json +++ /dev/null @@ -1,43 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "namespace": "default", - "name": "test-secret-without-data" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-with-secret-ref.json b/internal/k8s/testdata/oidc-with-secret-ref.json deleted file mode 100644 index f199c34..0000000 --- a/internal/k8s/testdata/oidc-with-secret-ref.json +++ /dev/null @@ -1,43 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "namespace": "default", - "name": "test-secret-1" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-without-secret-ref-in.json b/internal/k8s/testdata/oidc-without-secret-ref-in.json deleted file mode 100644 index b2b58c6..0000000 --- a/internal/k8s/testdata/oidc-without-secret-ref-in.json +++ /dev/null @@ -1,40 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/k8s/testdata/oidc-without-secret-ref-out.json b/internal/k8s/testdata/oidc-without-secret-ref-out.json deleted file mode 100644 index b2b58c6..0000000 --- a/internal/k8s/testdata/oidc-without-secret-ref-out.json +++ /dev/null @@ -1,40 +0,0 @@ - -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "mock": { - "allow": true - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/logging.go b/internal/logging.go deleted file mode 100644 index e0ee5bc..0000000 --- a/internal/logging.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "errors" - "fmt" - "strings" - - "github.com/tetratelabs/run" - "github.com/tetratelabs/telemetry" - "github.com/tetratelabs/telemetry/scope" - ctrl "sigs.k8s.io/controller-runtime" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" -) - -const ( - Authz = "authz" - Config = "config" - Default = "default" - Health = "health" - JWKS = "jwks" - Requests = "requests" - Server = "server" - Session = "session" - K8s = "k8s" -) - -// scopes contains the list of all logging scopes -var scopes = map[string]string{ - Authz: "Envoy ext-authz filter implementation messages", - Config: "Configuration messages", - Default: "Default", - Health: "Health server messages", - JWKS: "JWKS update and parse messages", - Requests: "Logs all requests and responses received by the server", - Server: "Server request handling messages", - Session: "Session store messages", - K8s: "Kubernetes controller messages", -} - -// ErrInvalidLogLevel is returned when the configured log level is invalid. -var ErrInvalidLogLevel = errors.New("invalid log level") - -// Logger gets the given logging scope, or return the Noop logger if no scope -// has been registered with the given name. -func Logger(name string) telemetry.Logger { - s, ok := scope.Find(name) - if !ok { - return telemetry.NoopLogger() - } - return s -} - -var _ run.PreRunner = (*setupLogging)(nil) - -// setupLogging is a run.Config that sets up the logging system. -type setupLogging struct { - logger telemetry.Logger - cfg *configv1.Config -} - -// NewLogSystem returns a new run.Unit that sets up the logging system. -func NewLogSystem(log telemetry.Logger, cfg *configv1.Config) run.Unit { - // Set the defaults in the constructor to make sure this runs as early as possible, - // not even as part of hte run.Group phases - scope.UseLogger(log) - scope.SetAllScopes(telemetry.LevelInfo) - for name, description := range scopes { - scope.Register(name, description) - } - return &setupLogging{ - logger: Logger(Server), - cfg: cfg, - } -} - -// Name returns the name of the run.Unit. -func (s *setupLogging) Name() string { return "Logging" } - -// PreRun initializes the logging system. -func (s *setupLogging) PreRun() error { - if s.cfg.LogLevel == "" { - s.cfg.LogLevel = "info" - } - levels, err := parseLogLevels(s.cfg.LogLevel) - if err != nil { - return err - } - ctrl.SetLogger(NewLogrAdapter(Logger(K8s))) - setLogLevels(s.logger, levels) - return nil -} - -// parseLogLevels reads the given string and configures the log levels accordingly. -// The string must have the format: "logger:level,logger,level,..." where "logger' -// is the name of an existing logger, such as 'pdp', and level is one of the values -// supported in the `log.Level` type. -// In addition to specific logger names, the "all" keyword can be used to configure all -// registered loggers to the configured level. For example: "all:debug". -func parseLogLevels(logLevels string) (map[string]telemetry.Level, error) { - res := map[string]telemetry.Level{} - levels := strings.Split(logLevels, ",") - singleLevel := len(levels) == 1 - - for _, l := range levels { - parts := strings.Split(l, ":") - if len(parts) == 1 && singleLevel { // Assume we're setting the level globally - lvl, err := readLogLevel(parts[0]) - if err != nil { - return nil, err - } - return map[string]telemetry.Level{"all": lvl}, nil - } - - if len(parts) != 2 { - return res, errors.New("must be in the form of :") - } - - logger := strings.TrimSpace(parts[0]) - level := strings.TrimSpace(parts[1]) - - if logger == "" { - return res, errors.New("logger must be specified") - } - - if level == "" { - return res, errors.New("level must be specified") - } - - lvl, err := readLogLevel(parts[1]) - if err != nil { - return nil, err - } - - res[logger] = lvl - } - - return res, nil -} - -// setLogLevels sets the log levels for the given loggers. -func setLogLevels(log telemetry.Logger, logLevelMap map[string]telemetry.Level) { - if level, ok := logLevelMap["all"]; ok { - for _, logger := range scope.List() { - logger.SetLevel(level) - } - } else { - for k, l := range logLevelMap { - logger, ok := scope.Find(k) - if ok { - logger.SetLevel(l) - } else { - log.Info("invalid logger", "logger", k) - } - } - } -} - -// readLogLevel parses the log level and adapts the legacy loggers from the original -// auth service project to the new telemetry logger supported levels. -func readLogLevel(level string) (telemetry.Level, error) { - switch level { - case "trace": - return telemetry.LevelDebug, nil - case "critical": - return telemetry.LevelError, nil - default: - l, ok := telemetry.FromLevel(level) - if !ok { - return telemetry.LevelNone, fmt.Errorf("%w: %s", ErrInvalidLogLevel, level) - } - return l, nil - } -} diff --git a/internal/logging_test.go b/internal/logging_test.go deleted file mode 100644 index 36fe441..0000000 --- a/internal/logging_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/tetratelabs/run" - "github.com/tetratelabs/telemetry" - "github.com/tetratelabs/telemetry/scope" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" -) - -func TestGetLogger(t *testing.T) { - var ( - logger1Name = "l1" - // do not reuse this name in other tests, otherwise multiple runs of the test may fail due find it registered - noLoggerName = "lnoop" - ) - l1 := scope.Register(logger1Name, "test logger one") - - NewLogSystem(telemetry.NoopLogger(), nil) - - require.Equal(t, l1, Logger(logger1Name)) - require.Equal(t, telemetry.NoopLogger(), Logger(noLoggerName)) -} - -func TestLoggingSetup(t *testing.T) { - l1 := scope.Register("l1", "test logger one") - l2 := scope.Register("l2", "test logger two") - - tests := []struct { - levels string - l1 telemetry.Level - l2 telemetry.Level - expectErr bool - }{ - // backwards compat log levels - {"trace", telemetry.LevelDebug, telemetry.LevelDebug, false}, - {"critical", telemetry.LevelError, telemetry.LevelError, false}, - {"all:trace", telemetry.LevelDebug, telemetry.LevelDebug, false}, - {"all:critical", telemetry.LevelError, telemetry.LevelError, false}, - {"l1:trace,l2:critical", telemetry.LevelDebug, telemetry.LevelError, false}, - // telemetry log levels - {"l1:debug", telemetry.LevelDebug, telemetry.LevelInfo, false}, - {"l1:debug,l2:debug", telemetry.LevelDebug, telemetry.LevelDebug, false}, - {"invalid:debug,l2:error", telemetry.LevelInfo, telemetry.LevelError, false}, - {"all:none,l1:debug", telemetry.LevelNone, telemetry.LevelNone, false}, - {"", telemetry.LevelInfo, telemetry.LevelInfo, false}, - {",", telemetry.LevelInfo, telemetry.LevelInfo, true}, - {":", telemetry.LevelInfo, telemetry.LevelInfo, true}, - {"invalid", telemetry.LevelInfo, telemetry.LevelInfo, true}, - {"l1:,l2:info", telemetry.LevelInfo, telemetry.LevelInfo, true}, - {"l1:debug,l2:invalid", telemetry.LevelInfo, telemetry.LevelInfo, true}, - } - - for _, tt := range tests { - t.Run(tt.levels, func(t *testing.T) { - g := run.Group{Logger: telemetry.NoopLogger()} - g.Register(NewLogSystem(telemetry.NoopLogger(), &configv1.Config{LogLevel: tt.levels})) - require.Equal(t, tt.expectErr, g.Run() != nil) - - require.Equal(t, tt.l1, l1.Level()) - require.Equal(t, tt.l2, l2.Level()) - }) - } -} diff --git a/internal/logr.go b/internal/logr.go deleted file mode 100644 index b4311fb..0000000 --- a/internal/logr.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "github.com/go-logr/logr" - "github.com/tetratelabs/telemetry" -) - -var _ logr.LogSink = (*logrAdapter)(nil) - -const debugLevelThreshold = 5 - -// logrAdapter is a type that adapts the log.Logger interface so it can be used with our loggers -type logrAdapter struct { - scope telemetry.Logger - kvs map[string]interface{} -} - -// NewLogrAdapter creates a new logger to bridge the logr.Logger to our logging system -func NewLogrAdapter(s telemetry.Logger) logr.Logger { - k := logrAdapter{scope: s} - return logr.New(&k) -} - -func (l *logrAdapter) Init(_ logr.RuntimeInfo) {} - -func (l *logrAdapter) Enabled(level int) bool { - return int(l.scope.Level()) >= level -} - -func (l *logrAdapter) Info(_ int, msg string, kvs ...interface{}) { - if len(kvs)%2 != 0 { - kvs = append(kvs, "(MISSING)") - } - logger := l.scope.With(kvs...) - - if l.scope.Level() > debugLevelThreshold { - logger.Debug(msg) - } else { - logger.Info(msg) - } -} - -func (l *logrAdapter) Error(err error, msg string, kvs ...interface{}) { - if len(kvs)%2 != 0 { - kvs = append(kvs, "(MISSING)") - } - logger := l.scope.With(kvs...) - logger.Error(msg, err) -} - -func (l *logrAdapter) WithName(string) logr.LogSink { return l } - -func (l *logrAdapter) WithValues(kvs ...interface{}) logr.LogSink { - if len(kvs) == 0 { - return l - } - - if len(kvs)%2 != 0 { - kvs = append(kvs, "(MISSING)") - } - all := make(map[string]interface{}, len(l.kvs)+len(kvs)/2) - for k, v := range l.kvs { - all[k] = v - } - for i := 0; i < len(kvs); i += 2 { - all[kvs[i].(string)] = kvs[i+1] - } - return &logrAdapter{ - scope: l.scope, - kvs: all, - } -} diff --git a/internal/logr_test.go b/internal/logr_test.go deleted file mode 100644 index 8027d19..0000000 --- a/internal/logr_test.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "errors" - "io" - "os" - "testing" - - "github.com/go-logr/logr" - "github.com/stretchr/testify/require" - "github.com/tetratelabs/log" - "github.com/tetratelabs/telemetry" -) - -func TestWithValues(t *testing.T) { - var a logr.LogSink = &logrAdapter{} - a = a.WithValues("one", 1, "two", 2) - a = a.WithValues("three") - a = a.WithValues() - - require.Equal(t, map[string]interface{}{"one": 1, "two": 2, "three": "(MISSING)"}, a.(*logrAdapter).kvs) -} - -func TestWithName(t *testing.T) { - var a logr.LogSink = &logrAdapter{} - n := a.WithName("test") - - require.Equal(t, a, n) -} - -func TestEnable(t *testing.T) { - logger := log.New() - logger.SetLevel(telemetry.LevelInfo) - a := logrAdapter{ - scope: logger, - } - require.False(t, a.Enabled(int(telemetry.LevelDebug))) - require.True(t, a.Enabled(int(telemetry.LevelInfo))) - require.True(t, a.Enabled(int(telemetry.LevelError))) -} - -func TestInfo(t *testing.T) { - rescueStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - logger := log.New() - logger.SetLevel(telemetry.LevelInfo) - a := logrAdapter{ - scope: logger, - } - - a.Info(0, "test", "one", 1, "two", 2) - - _ = w.Close() - out, _ := io.ReadAll(r) - os.Stdout = rescueStdout - require.Contains(t, string(out), "level=info msg=\"test\" one=1 two=2") -} - -func TestDebug(t *testing.T) { - rescueStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - logger := log.New() - logger.SetLevel(telemetry.LevelDebug) - a := logrAdapter{ - scope: logger, - } - - a.Info(0, "test", "one", 1, "two", 2) - - _ = w.Close() - out, _ := io.ReadAll(r) - os.Stdout = rescueStdout - require.Contains(t, string(out), "level=debug msg=\"test\" one=1 two=2") -} - -func TestError(t *testing.T) { - rescueStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - logger := log.New() - logger.SetLevel(telemetry.LevelError) - a := logrAdapter{ - scope: logger, - } - - a.Error(errors.New("error"), "test", "one", 1, "two", 2) - - _ = w.Close() - out, _ := io.ReadAll(r) - os.Stdout = rescueStdout - require.Contains(t, string(out), "level=error msg=\"test\" one=1 two=2 error=\"error\"") -} - -func TestMissingKV(t *testing.T) { - rescueStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - logger := log.New() - logger.SetLevel(telemetry.LevelInfo) - a := logrAdapter{ - scope: logger, - } - - a.Info(0, "test", "one", 1, "two") - a.Error(errors.New("failed"), "got an error", "last") - - _ = w.Close() - out, _ := io.ReadAll(r) - os.Stdout = rescueStdout - require.Contains(t, string(out), "level=info msg=\"test\" one=1 two=\"(MISSING)\"") - require.Contains(t, string(out), "level=error msg=\"got an error\" last=\"(MISSING)\" error=\"failed\"") -} diff --git a/internal/oidc/discovery.go b/internal/oidc/discovery.go deleted file mode 100644 index 71f0682..0000000 --- a/internal/oidc/discovery.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "encoding/json" - "fmt" - "net/http" -) - -// WellKnownConfig represents the OIDC well-known configuration -type WellKnownConfig struct { - Issuer string `json:"issuer"` - AuthorizationEndpoint string `json:"authorization_endpoint"` - TokenEndpoint string `json:"token_endpoint"` - JWKSURL string `json:"jwks_uri"` - ResponseTypesSupported []string `json:"response_types_supported"` - SubjectTypesSupported []string `json:"subject_types_supported"` - IDTokenSigningAlgorithms []string `json:"id_token_signing_alg_values_supported"` - TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported"` - UserInfoEndpoint string `json:"userinfo_endpoint"` - EndSessionEndpoint string `json:"end_session_endpoint"` - RevocationEndpoint string `json:"revocation_endpoint"` - IntrospectionEndpoint string `json:"introspection_endpoint"` - ScopesSupported []string `json:"scopes_supported"` - ClaimsSupported []string `json:"claims_supported"` - CodeChallengeMethods []string `json:"code_challenge_methods_supported"` - TokenRevocationEndpoint string `json:"token_revocation_endpoint"` -} - -// GetWellKnownConfig retrieves the OIDC well-known configuration from the given issuer URL. -func GetWellKnownConfig(client *http.Client, url string) (WellKnownConfig, error) { - // Make a GET request to the well-known configuration endpoint - response, err := client.Get(url) - if err != nil { - return WellKnownConfig{}, err - } - defer func() { _ = response.Body.Close() }() - - // Check if the response status code is successful - if response.StatusCode != http.StatusOK { - return WellKnownConfig{}, fmt.Errorf("failed to retrieve OIDC config: %s", response.Status) - } - - // Decode the JSON response into the OIDCConfig struct - var cfg WellKnownConfig - if err = json.NewDecoder(response.Body).Decode(&cfg); err != nil { - return WellKnownConfig{}, err - } - - return cfg, nil -} diff --git a/internal/oidc/discovery_test.go b/internal/oidc/discovery_test.go deleted file mode 100644 index c966578..0000000 --- a/internal/oidc/discovery_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "context" - "net" - "net/http" - "testing" - - "github.com/stretchr/testify/require" - "google.golang.org/grpc/test/bufconn" -) - -var ( - invalidWellKnownJSON = `{]]}` - validWellKnownJSON = ` -{ - "issuer": "http://example.com", - "authorization_endpoint": "http://example.com/authorize", - "token_endpoint": "http://example.com/token", - "jwks_uri": "http://example.com/jwks" -}` - - validWellKnown = WellKnownConfig{ - Issuer: "http://example.com", - AuthorizationEndpoint: "http://example.com/authorize", - TokenEndpoint: "http://example.com/token", - JWKSURL: "http://example.com/jwks", - } -) - -func TestWellKnownConfig(t *testing.T) { - tests := []struct { - name string - url string - cfg string - wantError bool - wantConfig WellKnownConfig - }{ - {"ok", "http://example.com/.well-known/openid-configuration", validWellKnownJSON, false, validWellKnown}, - {"not-found", "http://example.com/not-found", validWellKnownJSON, true, WellKnownConfig{}}, - {"invalid-url", "invalid", validWellKnownJSON, true, WellKnownConfig{}}, - {"invalid-json", "http://example.com/.well-known/openid-configuration", invalidWellKnownJSON, true, WellKnownConfig{}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := newServer() - s.Start() - t.Cleanup(s.Stop) - s.wellKnownConfig = tt.cfg - - got, err := GetWellKnownConfig(s.newHTTPClient(), tt.url) - if tt.wantError { - require.Error(t, err) - } else { - require.NoError(t, err) - } - require.Equal(t, tt.wantConfig, got) - }) - } -} - -type idpServer struct { - server *http.Server - listener *bufconn.Listener - wellKnownConfig string -} - -func newServer() *idpServer { - s := &http.Server{} - idpServer := &idpServer{server: s, listener: bufconn.Listen(1024)} - - handler := http.NewServeMux() - handler.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if idpServer.wellKnownConfig != "" { - _, _ = w.Write([]byte(idpServer.wellKnownConfig)) - } - }) - s.Handler = handler - return idpServer -} - -// Start starts the server in a goroutine. -func (s *idpServer) Start() { - go func() { _ = s.server.Serve(s.listener) }() -} - -// Stop stops the server. -func (s *idpServer) Stop() { - _ = s.listener.Close() -} - -// newHTTPClient returns a new http.Client that can be used to make requests to the server via the bufconn.Listener. -func (s *idpServer) newHTTPClient() *http.Client { - return &http.Client{ - Transport: &http.Transport{ - DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) { - return s.listener.DialContext(ctx) - }, - }, - } -} diff --git a/internal/oidc/jwks.go b/internal/oidc/jwks.go deleted file mode 100644 index 30a5ffb..0000000 --- a/internal/oidc/jwks.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/lestrrat-go/httprc" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/tetratelabs/run" - "github.com/tetratelabs/telemetry" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" - "github.com/tetrateio/authservice-go/internal" -) - -var ( - // ErrJWKSParse is returned when the JWKS document cannot be parsed. - ErrJWKSParse = errors.New("error parsing JWKS document") - // ErrJWKSFetch is returned when the JWKS document cannot be fetched. - ErrJWKSFetch = errors.New("error fetching JWKS document") - - _ run.ServiceContext = (*DefaultJWKSProvider)(nil) -) - -// DefaultFetchInterval is the default interval to use when none is set. -const DefaultFetchInterval = 1200 * time.Second - -// JWKSProvider provides a JWKS set for a given OIDC configuration. -type JWKSProvider interface { - // Get the JWKS for the given OIDC configuration - Get(context.Context, *oidcv1.OIDCConfig) (jwk.Set, error) -} - -// DefaultJWKSProvider provides a JWKS set -type DefaultJWKSProvider struct { - log telemetry.Logger - cache *jwk.Cache - config *configv1.Config - tlsPool internal.TLSConfigPool - started chan struct{} -} - -// NewJWKSProvider returns a new JWKSProvider. -func NewJWKSProvider(cfg *configv1.Config, tlsPool internal.TLSConfigPool) *DefaultJWKSProvider { - return &DefaultJWKSProvider{ - log: internal.Logger(internal.JWKS), - config: cfg, - tlsPool: tlsPool, - started: make(chan struct{}), - } -} - -// Name of the JWKSProvider run.Unit -func (j *DefaultJWKSProvider) Name() string { return "JWKS" } - -func (j *DefaultJWKSProvider) ServeContext(ctx context.Context) error { - errSink := httprc.ErrSinkFunc(func(err error) { - j.log.Debug("jwks auto refresh error", "error", err) - }) - j.cache = jwk.NewCache(ctx, - jwk.WithErrSink(errSink), - jwk.WithRefreshWindow(getRefreshWindow(j.config)), - ) - - close(j.started) // signal channel start - <-ctx.Done() - return nil -} - -// Get the JWKS for the given OIDC configuration -func (j *DefaultJWKSProvider) Get(ctx context.Context, config *oidcv1.OIDCConfig) (jwk.Set, error) { - if config.GetJwksFetcher() != nil { - <-j.started // wait until the service is fully started - return j.fetchDynamic(ctx, config) - } - return j.fetchStatic(config.GetJwks()) -} - -// fetchDynamic fetches the JWKS from the given URI. If the JWKS URI is already know, the JWKS will be returned from -// the cache. Otherwise, the JWKS will be fetched from the URI and the cache will be configured to periodically -// refresh the JWKS. -func (j *DefaultJWKSProvider) fetchDynamic(ctx context.Context, config *oidcv1.OIDCConfig) (jwk.Set, error) { - log := j.log.Context(ctx) - jwksConfig := config.GetJwksFetcher() - - if !j.cache.IsRegistered(jwksConfig.JwksUri) { - transport := http.DefaultTransport.(*http.Transport).Clone() - - var err error - if transport.TLSClientConfig, err = j.tlsPool.LoadTLSConfig(config); err != nil { - return nil, fmt.Errorf("error loading TLS config: %w", err) - } - - client := &http.Client{Transport: transport} - refreshInterval := time.Duration(jwksConfig.PeriodicFetchIntervalSec) * time.Second - if refreshInterval == 0 { - refreshInterval = DefaultFetchInterval - } - - log.Info("configuring JWKS auto refresh", "jwks", jwksConfig.JwksUri, "interval", refreshInterval, "skip_verify", config.GetSkipVerifyPeerCert()) - - if err = j.cache.Register(jwksConfig.JwksUri, - jwk.WithHTTPClient(client), - jwk.WithRefreshInterval(refreshInterval), - ); err != nil { - return nil, fmt.Errorf("error registering JWKS: %w", err) - } - } - - log.Debug("fetching JWKS", "jwks", jwksConfig.JwksUri) - - jwks, err := j.cache.Get(ctx, jwksConfig.JwksUri) - if err != nil { - return nil, fmt.Errorf("%w: %v", ErrJWKSFetch, err) - } - return jwks, nil -} - -// fetchStatic parses the given raw JWKS document. -func (j *DefaultJWKSProvider) fetchStatic(raw string) (jwk.Set, error) { - j.log.Debug("parsing static JWKS", "jwks", raw) - - jwks, err := jwk.Parse([]byte(raw)) - if err != nil { - return nil, fmt.Errorf("%w: %v", ErrJWKSParse, err) - } - return jwks, nil -} - -// getRefreshWindow returns the smallest refresh window for all the OIDC configurations. -// This is needed because the cache needs to be initialized with a default window small enough to -// accommodate all the configured intervals. -func getRefreshWindow(cfg *configv1.Config) time.Duration { - refreshWindow := DefaultFetchInterval - - for _, fc := range cfg.Chains { - for _, f := range fc.Filters { - if f.GetOidc() == nil { - continue - } - - interval := time.Duration(f.GetOidc().GetJwksFetcher().GetPeriodicFetchIntervalSec()) * time.Second - if interval > 0 && interval < refreshWindow { - refreshWindow = interval - } - } - } - - return refreshWindow -} diff --git a/internal/oidc/jwks_test.go b/internal/oidc/jwks_test.go deleted file mode 100644 index 18c00b2..0000000 --- a/internal/oidc/jwks_test.go +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/stretchr/testify/require" - "github.com/tetratelabs/run" - "github.com/tetratelabs/telemetry" - "google.golang.org/protobuf/types/known/structpb" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - mockv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/mock" - oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" - "github.com/tetrateio/authservice-go/internal" -) - -// nolint: lll -var ( - keys = ` -{ - "keys": [ - { - "kty": "RSA", - "alg": "RS256", - "use": "sig", - "kid": "62a93512c9ee4c7f8067b5a216dade2763d32a47", - "n": - "up97uqrF9MWOPaPkwSaBeuAPLOr9FKcaWGdVEGzQ4f3Zq5WKVZowx9TCBxmImNJ1qmUi13pB8otwM_l5lfY1AFBMxVbQCUXntLovhDaiSvYp4wGDjFzQiYA-pUq8h6MUZBnhleYrkU7XlCBwNVyN8qNMkpLA7KFZYz-486GnV2NIJJx_4BGa3HdKwQGxi2tjuQsQvao5W4xmSVaaEWopBwMy2QmlhSFQuPUpTaywTqUcUq_6SfAHhZ4IDa_FxEd2c2z8gFGtfst9cY3lRYf-c_ZdboY3mqN9Su3-j3z5r2SHWlhB_LNAjyWlBGsvbGPlTqDziYQwZN4aGsqVKQb9Vw", - "e": "AQAB" - }, - { - "kty": "RSA", - "alg": "RS256", - "use": "sig", - "kid": "b3319a147514df7ee5e4bcdee51350cc890cc89e", - "n": - "up97uqrF9MWOPaPkwSaBeuAPLOr9FKcaWGdVEGzQ4f3Zq5WKVZowx9TCBxmImNJ1qmUi13pB8otwM_l5lfY1AFBMxVbQCUXntLovhDaiSvYp4wGDjFzQiYA-pUq8h6MUZBnhleYrkU7XlCBwNVyN8qNMkpLA7KFZYz-486GnV2NIJJx_4BGa3HdKwQGxi2tjuQsQvao5W4xmSVaaEWopBwMy2QmlhSFQuPUpTaywTqUcUq_6SfAHhZ4IDa_FxEd2c2z8gFGtfst9cY3lRYf-c_ZdboY3mqN9Su3-j3z5r2SHWlhB_LNAjyWlBGsvbGPlTqDziYQwZN4aGsqVKQb9Vw", - "e": "AQAB" - } - ] -} -` - - singleKey = ` -{ - "kty": "RSA", - "alg": "RS256", - "use": "sig", - "kid": "62a93512c9ee4c7f8067b5a216dade2763d32a47", - "n": - "up97uqrF9MWOPaPkwSaBeuAPLOr9FKcaWGdVEGzQ4f3Zq5WKVZowx9TCBxmImNJ1qmUi13pB8otwM_l5lfY1AFBMxVbQCUXntLovhDaiSvYp4wGDjFzQiYA-pUq8h6MUZBnhleYrkU7XlCBwNVyN8qNMkpLA7KFZYz-486GnV2NIJJx_4BGa3HdKwQGxi2tjuQsQvao5W4xmSVaaEWopBwMy2QmlhSFQuPUpTaywTqUcUq_6SfAHhZ4IDa_FxEd2c2z8gFGtfst9cY3lRYf-c_ZdboY3mqN9Su3-j3z5r2SHWlhB_LNAjyWlBGsvbGPlTqDziYQwZN4aGsqVKQb9Vw", - "e": "AQAB" -} -` -) - -func TestStaticJWKSProvider(t *testing.T) { - tlsPool := internal.NewTLSConfigPool(context.Background()) - cfg := &configv1.Config{} - - t.Run("invalid", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - cache := NewJWKSProvider(cfg, tlsPool) - go func() { require.NoError(t, cache.ServeContext(ctx)) }() - t.Cleanup(cancel) - - _, err := cache.Get(context.Background(), &oidcv1.OIDCConfig{ - JwksConfig: &oidcv1.OIDCConfig_Jwks{ - Jwks: "{aaa}", - }, - }) - - require.ErrorIs(t, err, ErrJWKSParse) - }) - - t.Run("single-key", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - cache := NewJWKSProvider(cfg, tlsPool) - go func() { require.NoError(t, cache.ServeContext(ctx)) }() - t.Cleanup(cancel) - - jwks, err := cache.Get(context.Background(), &oidcv1.OIDCConfig{ - JwksConfig: &oidcv1.OIDCConfig_Jwks{ - Jwks: singleKey, - }, - }) - - require.NoError(t, err) - require.Equal(t, 1, jwks.Len()) - - key, ok := jwks.LookupKeyID("62a93512c9ee4c7f8067b5a216dade2763d32a47") - require.True(t, ok) - require.Equal(t, jwa.RS256, key.Algorithm()) - require.Equal(t, jwa.KeyType("RSA"), key.KeyType()) - require.Equal(t, "62a93512c9ee4c7f8067b5a216dade2763d32a47", key.KeyID()) - }) - - t.Run("multiple-keys", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - cache := NewJWKSProvider(cfg, tlsPool) - go func() { require.NoError(t, cache.ServeContext(ctx)) }() - t.Cleanup(cancel) - - jwks, err := cache.Get(context.Background(), &oidcv1.OIDCConfig{ - JwksConfig: &oidcv1.OIDCConfig_Jwks{ - Jwks: keys, - }, - }) - - require.NoError(t, err) - require.Equal(t, 2, jwks.Len()) - - key, ok := jwks.LookupKeyID("62a93512c9ee4c7f8067b5a216dade2763d32a47") - require.True(t, ok) - require.Equal(t, jwa.RS256, key.Algorithm()) - require.Equal(t, jwa.KeyType("RSA"), key.KeyType()) - require.Equal(t, "62a93512c9ee4c7f8067b5a216dade2763d32a47", key.KeyID()) - - key, ok = jwks.LookupKeyID("b3319a147514df7ee5e4bcdee51350cc890cc89e") - require.True(t, ok) - require.Equal(t, jwa.RS256, key.Algorithm()) - require.Equal(t, jwa.KeyType("RSA"), key.KeyType()) - require.Equal(t, "b3319a147514df7ee5e4bcdee51350cc890cc89e", key.KeyID()) - }) -} - -func TestDynamicJWKSProvider(t *testing.T) { - var ( - pub = newKey(t) - jwks = newKeySet(t, pub) - - tlsPool = internal.NewTLSConfigPool(context.Background()) - newCache = func(t *testing.T, oidc *oidcv1.OIDCConfig) JWKSProvider { - cfg := &configv1.Config{ - Chains: []*configv1.FilterChain{ - { - Filters: []*configv1.Filter{ - {Type: &configv1.Filter_Mock{Mock: &mockv1.MockConfig{}}}, - {Type: &configv1.Filter_Oidc{Oidc: oidc}}, - }, - }, - }, - } - - cache := NewJWKSProvider(cfg, tlsPool) - g := run.Group{Logger: telemetry.NoopLogger()} - g.Register(cache) - go func() { _ = g.Run() }() - - // Block until the cache is initialized - <-cache.started - return cache - } - ) - - t.Run("invalid url", func(t *testing.T) { - server := newTestServer(t, jwks) - config := &oidcv1.OIDCConfig{ - JwksConfig: &oidcv1.OIDCConfig_JwksFetcher{ - JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{ - JwksUri: server.URL + "/not-found", - }, - }, - } - cache := newCache(t, config) - - _, err := cache.Get(context.Background(), config) - - require.ErrorIs(t, err, ErrJWKSFetch) - require.Equal(t, int32(1), atomic.LoadInt32(server.requestCount)) // The attempt to load the JWKS is made, but fails - }) - - t.Run("cache load", func(t *testing.T) { - server := newTestServer(t, jwks) - config := &oidcv1.OIDCConfig{ - JwksConfig: &oidcv1.OIDCConfig_JwksFetcher{ - JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{ - JwksUri: server.URL, - PeriodicFetchIntervalSec: 1, - }, - }, - SkipVerifyPeerCert: structpb.NewBoolValue(true), - } - cache := newCache(t, config) - - keys, err := cache.Get(context.Background(), config) - require.NoError(t, err) - require.Equal(t, jwks, keys) - require.Equal(t, int32(1), atomic.LoadInt32(server.requestCount)) - }) - - t.Run("cached results", func(t *testing.T) { - server := newTestServer(t, jwks) - config := &oidcv1.OIDCConfig{ - JwksConfig: &oidcv1.OIDCConfig_JwksFetcher{ - JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{ - JwksUri: server.URL, - PeriodicFetchIntervalSec: 60, - }, - }, - } - cache := newCache(t, config) - - for i := 0; i < 5; i++ { - keys, err := cache.Get(context.Background(), config) - require.NoError(t, err) - require.Equal(t, jwks, keys) - require.Equal(t, int32(1), atomic.LoadInt32(server.requestCount)) // Cached results after the first request - } - }) - - t.Run("cache refresh", func(t *testing.T) { - server := newTestServer(t, jwks) - config := &oidcv1.OIDCConfig{ - JwksConfig: &oidcv1.OIDCConfig_JwksFetcher{ - JwksFetcher: &oidcv1.OIDCConfig_JwksFetcherConfig{ - JwksUri: server.URL, - PeriodicFetchIntervalSec: 1, - }, - }, - } - cache := newCache(t, config) - - // Load the entry in the cache and remove it to let the background refresher refresh it - _, err := cache.Get(context.Background(), config) - require.NoError(t, err) - require.NoError(t, jwks.RemoveKey(pub)) - - // Wait for the refresh period and check that the JWKS has been refreshed - require.Eventually(t, func() bool { - return atomic.LoadInt32(server.requestCount) > 1 - }, 3*time.Second, 1*time.Second) - }) -} - -type server struct { - *httptest.Server - requestCount *int32 -} - -func newTestServer(t *testing.T, jwks jwk.Set) *server { - s := &server{requestCount: new(int32)} - s.Server = httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - atomic.AddInt32(s.requestCount, 1) - - if strings.HasSuffix(req.URL.Path, "/not-found") { - res.WriteHeader(404) - return - } - - bytes, err := json.Marshal(jwks) - require.NoError(t, err) - res.WriteHeader(200) - _, _ = res.Write(bytes) - })) - t.Cleanup(func() { atomic.StoreInt32(s.requestCount, 0) }) - t.Cleanup(s.Close) - return s -} - -const keyID = "test" - -func newKeySet(t *testing.T, keys ...jwk.Key) jwk.Set { - jwks := jwk.NewSet() - for _, k := range keys { - require.NoError(t, jwks.AddKey(k)) - } - return jwks -} - -func newKey(t *testing.T) jwk.Key { - rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - pub, err := jwk.FromRaw(rsaKey.PublicKey) - require.NoError(t, err) - - err = pub.Set(jwk.KeyIDKey, keyID) - require.NoError(t, err) - err = pub.Set(jwk.AlgorithmKey, jwa.RS256) - require.NoError(t, err) - - return pub -} diff --git a/internal/oidc/memory.go b/internal/oidc/memory.go deleted file mode 100644 index 4f20c7a..0000000 --- a/internal/oidc/memory.go +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "context" - "sync" - "time" - - "github.com/tetratelabs/telemetry" - - "github.com/tetrateio/authservice-go/internal" -) - -var _ SessionStore = (*memoryStore)(nil) - -// memoryStore is an in-memory implementation of the SessionStore interface. -type memoryStore struct { - log telemetry.Logger - clock *Clock - absoluteSessionTimeout time.Duration - idleSessionTimeout time.Duration - - mu sync.Mutex - sessions map[string]*session -} - -// NewMemoryStore creates a new in-memory session store. -func NewMemoryStore(clock *Clock, absoluteSessionTimeout, idleSessionTimeout time.Duration) SessionStore { - return &memoryStore{ - log: internal.Logger(internal.Session).With("type", "memory"), - clock: clock, - absoluteSessionTimeout: absoluteSessionTimeout, - idleSessionTimeout: idleSessionTimeout, - sessions: make(map[string]*session), - } -} - -func (m *memoryStore) SetTokenResponse(ctx context.Context, sessionID string, tokenResponse *TokenResponse) error { - log := m.log.Context(ctx).With("session-id", sessionID) - log.Debug("setting token response", "token_response", tokenResponse) - - m.set(ctx, sessionID, func(s *session) { - s.tokenResponse = tokenResponse - }) - return nil -} - -func (m *memoryStore) GetTokenResponse(ctx context.Context, sessionID string) (*TokenResponse, error) { - log := m.log.Context(ctx).With("session-id", sessionID) - log.Debug("getting token response") - - m.mu.Lock() - defer m.mu.Unlock() - - s := m.sessions[sessionID] - if s == nil { - return nil, nil - } - - s.accessed = m.clock.Now() - return s.tokenResponse, nil -} - -func (m *memoryStore) SetAuthorizationState(ctx context.Context, sessionID string, authorizationState *AuthorizationState) error { - log := m.log.Context(ctx).With("session-id", sessionID) - log.Debug("setting authorization state", "state", authorizationState) - - m.set(ctx, sessionID, func(s *session) { - s.authorizationState = authorizationState - }) - return nil -} - -func (m *memoryStore) GetAuthorizationState(ctx context.Context, sessionID string) (*AuthorizationState, error) { - log := m.log.Context(ctx).With("session-id", sessionID) - log.Debug("getting authorization state") - - m.mu.Lock() - defer m.mu.Unlock() - - s := m.sessions[sessionID] - if s == nil { - return nil, nil - } - - s.accessed = m.clock.Now() - return s.authorizationState, nil -} - -func (m *memoryStore) ClearAuthorizationState(ctx context.Context, sessionID string) error { - log := m.log.Context(ctx).With("session-id", sessionID) - log.Debug("clearing authorization state") - - m.mu.Lock() - defer m.mu.Unlock() - - if s := m.sessions[sessionID]; s != nil { - s.accessed = m.clock.Now() - s.authorizationState = nil - } - - return nil -} - -func (m *memoryStore) RemoveSession(ctx context.Context, sessionID string) error { - log := m.log.Context(ctx).With("session-id", sessionID) - log.Debug("removing session") - - m.mu.Lock() - defer m.mu.Unlock() - - delete(m.sessions, sessionID) - - return nil -} - -func (m *memoryStore) RemoveAllExpired(ctx context.Context) error { - log := m.log.Context(ctx) - log.Debug("removing expired sessions") - - var ( - earliestTimeAddedToKeep = m.clock.Now().Add(-m.absoluteSessionTimeout) - earliestTimeIdleToKeep = m.clock.Now().Add(-m.idleSessionTimeout) - shouldCheckAbsoluteTimeout = m.absoluteSessionTimeout > 0 - shouldCheckIdleTimeout = m.idleSessionTimeout > 0 - ) - - m.mu.Lock() - defer m.mu.Unlock() - - for sessionID, s := range m.sessions { - expiredBasedOnTimeAdded := shouldCheckAbsoluteTimeout && s.added.Before(earliestTimeAddedToKeep) - expiredBasedOnIdleTime := shouldCheckIdleTimeout && s.accessed.Before(earliestTimeIdleToKeep) - - if expiredBasedOnTimeAdded || expiredBasedOnIdleTime { - log.Debug("removing expired session", "session-id", sessionID) - delete(m.sessions, sessionID) - } - } - - return nil -} - -// set the given session with the given setter function and record the access time. -func (m *memoryStore) set(ctx context.Context, sessionID string, setter func(s *session)) { - log := m.log.Context(ctx).With("session-id", sessionID) - - m.mu.Lock() - defer m.mu.Unlock() - - s := m.sessions[sessionID] - if s != nil { - s.accessed = m.clock.Now() - setter(s) - } else { - s = newSession(m.clock.Now()) - setter(s) - m.sessions[sessionID] = s - } - - log.Debug("updating last access", "accessed", s.accessed) -} - -// session holds the data of a session stored in the in-memory cache -type session struct { - tokenResponse *TokenResponse - authorizationState *AuthorizationState - added time.Time - accessed time.Time -} - -// newSession creates a new session with the given creation time. -func newSession(t time.Time) *session { - return &session{ - added: t, - accessed: t, - } -} diff --git a/internal/oidc/memory_test.go b/internal/oidc/memory_test.go deleted file mode 100644 index e617159..0000000 --- a/internal/oidc/memory_test.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestMemoryTokenResponse(t *testing.T) { - m := NewMemoryStore(&Clock{}, 0, 0).(*memoryStore) - ctx := context.Background() - - tr, err := m.GetTokenResponse(ctx, "s1") - require.NoError(t, err) - require.Nil(t, tr) - - // Create a session and verify it's added and accessed time - tr = &TokenResponse{} - require.NoError(t, m.SetTokenResponse(ctx, "s1", &TokenResponse{})) - require.Greater(t, m.sessions["s1"].added.Unix(), int64(0)) - require.Equal(t, m.sessions["s1"].added, m.sessions["s1"].accessed) - - // Verify that the right token response is returned and the accessed time is updated - got, err := m.GetTokenResponse(ctx, "s1") - require.NoError(t, err) - require.Equal(t, tr, got) - require.True(t, m.sessions["s1"].accessed.After(m.sessions["s1"].added)) - lastAccessed := m.sessions["s1"].accessed - - // Verify that updating the token response also updates the session access timestamp - require.NoError(t, m.SetTokenResponse(ctx, "s1", &TokenResponse{})) - require.True(t, m.sessions["s1"].accessed.After(lastAccessed)) -} - -func TestMemoryAuthorizationState(t *testing.T) { - m := NewMemoryStore(&Clock{}, 0, 0).(*memoryStore) - ctx := context.Background() - - as, err := m.GetAuthorizationState(ctx, "s1") - require.NoError(t, err) - require.Nil(t, as) - - // Create a session and verify it's added and accessed time - as = &AuthorizationState{} - require.NoError(t, m.SetAuthorizationState(ctx, "s1", as)) - require.Greater(t, m.sessions["s1"].added.Unix(), int64(0)) - require.Equal(t, m.sessions["s1"].added, m.sessions["s1"].accessed) - - // Verify that the right state is returned and the accessed time is updated - got, err := m.GetAuthorizationState(ctx, "s1") - require.NoError(t, err) - require.Equal(t, as, got) - lastAccessed := m.sessions["s1"].accessed - require.True(t, lastAccessed.After(m.sessions["s1"].added)) - - // Verify that updating the authz state also updates the session access timestamp - require.NoError(t, m.SetAuthorizationState(ctx, "s1", &AuthorizationState{})) - require.True(t, m.sessions["s1"].accessed.After(lastAccessed)) - - // Verify that clearing the authz state also updates the session access timestamp - require.NoError(t, m.ClearAuthorizationState(ctx, "s1")) - got, err = m.GetAuthorizationState(ctx, "s1") - require.NoError(t, err) - require.Nil(t, got) - require.True(t, m.sessions["s1"].accessed.After(lastAccessed)) -} - -func TestMemoryRemoveResponse(t *testing.T) { - m := NewMemoryStore(&Clock{}, 0, 0).(*memoryStore) - ctx := context.Background() - - require.NoError(t, m.SetTokenResponse(ctx, "s1", &TokenResponse{})) - require.NotNil(t, m.sessions["s1"]) - - require.NoError(t, m.RemoveSession(ctx, "s1")) - require.Nil(t, m.sessions["s1"]) -} - -func TestMemoryRemoveAllExpired(t *testing.T) { - m := NewMemoryStore(&Clock{}, 0, 0).(*memoryStore) - ctx := context.Background() - - require.NoError(t, m.SetTokenResponse(ctx, "s1", &TokenResponse{})) - require.NoError(t, m.SetTokenResponse(ctx, "s2", &TokenResponse{})) - require.NoError(t, m.SetTokenResponse(ctx, "abs-expired", &TokenResponse{})) - require.NoError(t, m.SetTokenResponse(ctx, "idle-expired", &TokenResponse{})) - - m.sessions["abs-expired"].added = time.Now().Add(-time.Hour) - m.sessions["idle-expired"].accessed = time.Now().Add(-time.Hour) - - t.Run("no-expiration", func(t *testing.T) { - require.NoError(t, m.RemoveAllExpired(ctx)) - - require.Len(t, m.sessions, 4) - require.NotNil(t, m.sessions["s1"]) - require.NotNil(t, m.sessions["s2"]) - require.NotNil(t, m.sessions["abs-expired"]) - require.NotNil(t, m.sessions["idle-expired"]) - }) - - t.Run("expiration", func(t *testing.T) { - m.absoluteSessionTimeout = time.Minute - m.idleSessionTimeout = time.Minute - require.NoError(t, m.RemoveAllExpired(ctx)) - - require.Len(t, m.sessions, 2) - require.NotNil(t, m.sessions["s1"]) - require.NotNil(t, m.sessions["s2"]) - require.Nil(t, m.sessions["abs-expired"]) - require.Nil(t, m.sessions["idle-expired"]) - - }) -} diff --git a/internal/oidc/redis.go b/internal/oidc/redis.go deleted file mode 100644 index bb261c3..0000000 --- a/internal/oidc/redis.go +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/redis/go-redis/v9" - "github.com/tetratelabs/telemetry" - - "github.com/tetrateio/authservice-go/internal" -) - -var ( - _ SessionStore = (*redisStore)(nil) - - ErrRedis = errors.New("redis error") -) - -const ( - keyIDToken = "id_token" - keyAccessToken = "access_token" - keyAccessTokenExpiry = "access_token_expiry" - keyRefreshToken = "refresh_token" - keyState = "state" - keyNonce = "nonce" - keyRequestedURL = "requested_url" - keyTimeAdded = "time_added" -) - -var ( - tokenResponseKeys = []string{keyIDToken, keyAccessToken, keyRefreshToken, keyAccessTokenExpiry, keyTimeAdded} - authorizationStateKeys = []string{keyState, keyNonce, keyRequestedURL, keyTimeAdded} -) - -// redisStore is an in-memory implementation of the SessionStore interface that stores -// the session data in a given Redis server. -type redisStore struct { - log telemetry.Logger - clock *Clock - client redis.Cmdable - absoluteSessionTimeout time.Duration - idleSessionTimeout time.Duration -} - -// NewRedisStore creates a new SessionStore that stores the session data in a given Redis server. -func NewRedisStore(clock *Clock, client redis.Cmdable, absoluteSessionTimeout, idleSessionTimeout time.Duration) (SessionStore, error) { - if err := client.Ping(context.TODO()).Err(); err != nil { - return nil, err - } - - return &redisStore{ - log: internal.Logger(internal.Session).With("type", "redis"), - clock: clock, - client: client, - absoluteSessionTimeout: absoluteSessionTimeout, - idleSessionTimeout: idleSessionTimeout, - }, nil -} - -func (r *redisStore) SetTokenResponse(ctx context.Context, sessionID string, tokenResponse *TokenResponse) error { - log := r.log.Context(ctx).With("session-id", sessionID) - log.Debug("setting token response", "token_response", tokenResponse) - - if err := r.client.HSet(ctx, sessionID, keyIDToken, tokenResponse.IDToken).Err(); err != nil { - return err - } - - var keysToDelete []string - - if tokenResponse.AccessToken != "" { - if err := r.client.HSet(ctx, sessionID, keyAccessToken, tokenResponse.AccessToken).Err(); err != nil { - return err - } - } else { - keysToDelete = append(keysToDelete, keyAccessToken) - } - - if !tokenResponse.AccessTokenExpiresAt.IsZero() { - if err := r.client.HSet(ctx, sessionID, keyAccessTokenExpiry, tokenResponse.AccessTokenExpiresAt).Err(); err != nil { - return err - } - } else { - keysToDelete = append(keysToDelete, keyAccessTokenExpiry) - } - - if tokenResponse.RefreshToken != "" { - if err := r.client.HSet(ctx, sessionID, keyRefreshToken, tokenResponse.RefreshToken).Err(); err != nil { - return err - } - } else { - keysToDelete = append(keysToDelete, keyRefreshToken) - } - - if len(keysToDelete) > 0 { - log.Debug("deleting stale keys", "keys", keysToDelete) - - if err := r.client.HDel(ctx, sessionID, keysToDelete...).Err(); err != nil { - return err - } - } - - now := r.clock.Now() - if err := r.client.HSetNX(ctx, sessionID, keyTimeAdded, now).Err(); err != nil { - return err - } - - return r.refreshExpiration(ctx, sessionID, now) -} - -func (r *redisStore) GetTokenResponse(ctx context.Context, sessionID string) (*TokenResponse, error) { - log := r.log.Context(ctx).With("session-id", sessionID) - log.Debug("getting token response") - - res := r.client.HMGet(ctx, sessionID, tokenResponseKeys...) - if res.Err() != nil { - return nil, res.Err() - } - - var token redisToken - if err := res.Scan(&token); err != nil { - return nil, err - } - - if token.IDToken == "" { - log.Debug("id token not found") - return nil, nil - } - - tokenResponse := token.TokenResponse() - if _, err := tokenResponse.ParseIDToken(); err != nil { - log.Error("failed to parse id token", err, "token", token) - return nil, nil - } - - if err := r.refreshExpiration(ctx, sessionID, token.TimeAdded); err != nil { - return nil, err - } - - return tokenResponse, nil -} - -func (r *redisStore) SetAuthorizationState(ctx context.Context, sessionID string, authorizationState *AuthorizationState) error { - log := r.log.Context(ctx).With("session-id", sessionID) - log.Debug("setting authorization state", "state", authorizationState) - - state := map[string]any{ - keyState: authorizationState.State, - keyNonce: authorizationState.Nonce, - keyRequestedURL: authorizationState.RequestedURL, - } - - if err := r.client.HMSet(ctx, sessionID, state).Err(); err != nil { - return err - } - - now := r.clock.Now() - if err := r.client.HSetNX(ctx, sessionID, keyTimeAdded, now).Err(); err != nil { - return err - } - - return r.refreshExpiration(ctx, sessionID, now) -} - -func (r *redisStore) GetAuthorizationState(ctx context.Context, sessionID string) (*AuthorizationState, error) { - log := r.log.Context(ctx).With("session-id", sessionID) - log.Debug("getting authorization state") - - res := r.client.HMGet(ctx, sessionID, authorizationStateKeys...) - if res.Err() != nil { - return nil, res.Err() - } - - var state redisAuthState - if err := res.Scan(&state); err != nil { - return nil, err - } - - if state.State == "" || state.Nonce == "" || state.RequestedURL == "" { - return nil, nil - } - - if err := r.refreshExpiration(ctx, sessionID, state.TimeAdded); err != nil { - return nil, err - } - - return state.AuthorizationState(), nil -} - -func (r *redisStore) ClearAuthorizationState(ctx context.Context, sessionID string) error { - log := r.log.Context(ctx).With("session-id", sessionID) - log.Debug("clearing authorization state") - - if err := r.client.HDel(ctx, sessionID, keyState, keyNonce, keyRequestedURL).Err(); err != nil { - return err - } - - return r.refreshExpiration(ctx, sessionID, time.Time{}) -} - -func (r *redisStore) RemoveSession(ctx context.Context, sessionID string) error { - r.log.Context(ctx).With("session-id", sessionID).Debug("removing session") - return r.client.Del(ctx, sessionID).Err() -} - -func (r *redisStore) RemoveAllExpired(context.Context) error { - // Sessions are automatically expired by Redis - return nil -} - -func (r *redisStore) refreshExpiration(ctx context.Context, sessionID string, timeAdded time.Time) error { - log := r.log.Context(ctx).With("session-id", sessionID) - - if timeAdded.IsZero() { - timeAdded, _ = r.client.HGet(ctx, sessionID, keyTimeAdded).Time() - } - - if timeAdded.IsZero() { - if err := r.client.Del(ctx, sessionID).Err(); err != nil { - return err - } - return fmt.Errorf("%w: session did not contain creation timestamp", ErrRedis) - } - - if r.absoluteSessionTimeout == 0 && r.idleSessionTimeout == 0 { - return nil - } - - var ( - now = r.clock.Now() - absoluteExpireAt = timeAdded.Add(r.absoluteSessionTimeout) - idleExpireAt = now.Add(r.idleSessionTimeout) - expireAt time.Time - ) - - if r.absoluteSessionTimeout == 0 { - expireAt = idleExpireAt - } else if r.idleSessionTimeout == 0 { - expireAt = absoluteExpireAt - } else { - expireAt = absoluteExpireAt - if idleExpireAt.Before(expireAt) { - expireAt = idleExpireAt - } - } - - log.Debug("updating session expiration", - "expire_at", expireAt.Format(time.DateTime), - "time_added", timeAdded.Format(time.DateTime), - "absolute_session_timeout", r.absoluteSessionTimeout, - "idle_session_timeout", r.idleSessionTimeout, - ) - - return r.client.ExpireAt(ctx, sessionID, expireAt).Err() -} - -type ( - redisToken struct { - IDToken string `redis:"id_token"` - AccessToken string `redis:"access_token"` - AccessTokenExpiresAt time.Time `redis:"access_token_expiry"` - RefreshToken string `redis:"refresh_token"` - TimeAdded time.Time `redis:"time_added"` - } - - redisAuthState struct { - State string `redis:"state"` - Nonce string `redis:"nonce"` - RequestedURL string `redis:"requested_url"` - TimeAdded time.Time `redis:"time_added"` - } -) - -func (r redisToken) TokenResponse() *TokenResponse { - return &TokenResponse{ - IDToken: r.IDToken, - AccessToken: r.AccessToken, - AccessTokenExpiresAt: r.AccessTokenExpiresAt, - RefreshToken: r.RefreshToken, - } -} - -func (r redisAuthState) AuthorizationState() *AuthorizationState { - return &AuthorizationState{ - State: r.State, - Nonce: r.Nonce, - RequestedURL: r.RequestedURL, - } -} diff --git a/internal/oidc/redis_test.go b/internal/oidc/redis_test.go deleted file mode 100644 index 89230c8..0000000 --- a/internal/oidc/redis_test.go +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "context" - "testing" - "time" - - "github.com/alicebob/miniredis/v2" - "github.com/redis/go-redis/v9" - "github.com/stretchr/testify/require" -) - -func TestRedisTokenResponse(t *testing.T) { - mr := miniredis.RunT(t) - client := redis.NewClient(&redis.Options{Addr: mr.Addr()}) - store, err := NewRedisStore(&Clock{}, client, 0, 1*time.Minute) - require.NoError(t, err) - - ctx := context.Background() - - tr, err := store.GetTokenResponse(ctx, "s1") - require.NoError(t, err) - require.Nil(t, tr) - - // Create a session and verify it's added and accessed time is set - tr = &TokenResponse{ - IDToken: newToken(), - AccessToken: newToken(), - AccessTokenExpiresAt: time.Now().Add(30 * time.Minute), - RefreshToken: newToken(), - } - require.NoError(t, store.SetTokenResponse(ctx, "s1", tr)) - - // Verify we can retrieve the token - got, err := store.GetTokenResponse(ctx, "s1") - require.NoError(t, err) - // The testify library doesn't properly compare times, so we need to do it manually - // then set the times in the returned object so that we can compare the rest of the - // fields normally - require.True(t, tr.AccessTokenExpiresAt.Equal(got.AccessTokenExpiresAt)) - got.AccessTokenExpiresAt = tr.AccessTokenExpiresAt - require.Equal(t, tr, got) - - // Verify that the session TTL has been set - added, _ := client.HGet(ctx, "s1", keyTimeAdded).Time() - ttl := client.TTL(ctx, "s1").Val() - require.Greater(t, added.Unix(), int64(0)) - require.Greater(t, ttl, time.Duration(0)) - - // Check keys are deleted - tr.AccessToken = "" - tr.RefreshToken = "" - tr.AccessTokenExpiresAt = time.Time{} - require.NoError(t, store.SetTokenResponse(ctx, "s1", tr)) - - var rt redisToken - vals := client.HMGet(ctx, "s1", keyAccessToken, keyRefreshToken, keyAccessTokenExpiry) - require.NoError(t, vals.Scan(&rt)) - require.Empty(t, rt.AccessToken) - require.True(t, rt.AccessTokenExpiresAt.IsZero()) - require.Empty(t, rt.RefreshToken) -} - -func TestRedisAuthorizationState(t *testing.T) { - mr := miniredis.RunT(t) - client := redis.NewClient(&redis.Options{Addr: mr.Addr()}) - store, err := NewRedisStore(&Clock{}, client, 0, 1*time.Minute) - require.NoError(t, err) - - ctx := context.Background() - - as, err := store.GetAuthorizationState(ctx, "s1") - require.NoError(t, err) - require.Nil(t, as) - - as = &AuthorizationState{ - State: "state", - Nonce: "nonce", - RequestedURL: "requested_url", - } - require.NoError(t, store.SetAuthorizationState(ctx, "s1", as)) - - // Verify that the right state is returned and the expiration time is updated - got, err := store.GetAuthorizationState(ctx, "s1") - require.NoError(t, err) - require.Equal(t, as, got) - - // Verify that the session TTL has been set - added, _ := client.HGet(ctx, "s1", keyTimeAdded).Time() - ttl := client.TTL(ctx, "s1").Val() - require.Greater(t, added.Unix(), int64(0)) - require.Greater(t, ttl, time.Duration(0)) - - // Verify that clearing the authz state also updates the session access timestamp - require.NoError(t, store.ClearAuthorizationState(ctx, "s1")) - - var at redisAuthState - vals := client.HMGet(ctx, "s1", keyState, keyNonce, keyRequestedURL) - require.NoError(t, vals.Scan(&at)) - require.Empty(t, at.State) - require.Empty(t, at.Nonce) - require.Empty(t, at.RequestedURL) - - // Verify that the session TTL is still there - added, _ = client.HGet(ctx, "s1", keyTimeAdded).Time() - ttl = client.TTL(ctx, "s1").Val() - require.Greater(t, added.Unix(), int64(0)) - require.Greater(t, ttl, time.Duration(0)) -} - -func TestRedisRemoveSession(t *testing.T) { - mr := miniredis.RunT(t) - client := redis.NewClient(&redis.Options{Addr: mr.Addr()}) - store, err := NewRedisStore(&Clock{}, client, 0, 1*time.Minute) - require.NoError(t, err) - - ctx := context.Background() - - t.Run("unexisting", func(t *testing.T) { - require.NoError(t, store.RemoveSession(ctx, "s1")) - }) - - t.Run("existing", func(t *testing.T) { - require.NoError(t, client.HSet(ctx, "s1", keyTimeAdded, time.Now()).Err()) - require.NoError(t, store.RemoveSession(ctx, "s1")) - }) -} - -func TestRedisRemoveAllExpired(t *testing.T) { - mr := miniredis.RunT(t) - client := redis.NewClient(&redis.Options{Addr: mr.Addr()}) - store, err := NewRedisStore(&Clock{}, client, 0, 1*time.Minute) - require.NoError(t, err) - - require.NoError(t, store.RemoveAllExpired(context.Background())) -} - -func TestRedisPingError(t *testing.T) { - mr := miniredis.RunT(t) - client := redis.NewClient(&redis.Options{Addr: mr.Addr()}) - mr.SetError("ping error") - - _, err := NewRedisStore(&Clock{}, client, 0, 1*time.Minute) - require.EqualError(t, err, "ping error") -} - -func TestRefreshExpiration(t *testing.T) { - mr := miniredis.RunT(t) - client := redis.NewClient(&redis.Options{Addr: mr.Addr()}) - store, err := NewRedisStore(&Clock{}, client, 0, 0) - require.NoError(t, err) - rs := store.(*redisStore) - - ctx := context.Background() - - t.Run("delete session if no time added", func(t *testing.T) { - require.NoError(t, client.HSet(ctx, "s1", keyAccessToken, "").Err()) - err := rs.refreshExpiration(ctx, "s1", time.Time{}) - require.ErrorIs(t, err, ErrRedis) - require.Equal(t, redis.Nil, client.Get(ctx, "s1").Err()) - }) - - t.Run("no expiration set if no timeouts", func(t *testing.T) { - require.NoError(t, client.HSet(ctx, "s1", keyTimeAdded, time.Now()).Err()) - require.NoError(t, rs.refreshExpiration(ctx, "s1", time.Time{})) - - res, err := client.TTL(ctx, "s1").Result() - require.NoError(t, err) - require.Equal(t, time.Duration(-1), res) - }) - - t.Run("set idle expiration", func(t *testing.T) { - rs.absoluteSessionTimeout = 0 - rs.idleSessionTimeout = 1 * time.Minute - require.NoError(t, client.HSet(ctx, "s1", keyTimeAdded, time.Now()).Err()) - require.NoError(t, rs.refreshExpiration(ctx, "s1", time.Time{})) - - res, err := client.TTL(ctx, "s1").Result() - require.NoError(t, err) - require.Greater(t, res, time.Duration(0)) - require.LessOrEqual(t, res, rs.idleSessionTimeout) - }) - - t.Run("set absolute expiration", func(t *testing.T) { - rs.absoluteSessionTimeout = 30 * time.Second - rs.idleSessionTimeout = 0 - require.NoError(t, client.HSet(ctx, "s1", keyTimeAdded, time.Now()).Err()) - require.NoError(t, rs.refreshExpiration(ctx, "s1", time.Time{})) - - res, err := client.TTL(ctx, "s1").Result() - require.NoError(t, err) - require.Greater(t, res, time.Duration(0)) - require.LessOrEqual(t, res, rs.absoluteSessionTimeout) - }) - - t.Run("set smallest expiration", func(t *testing.T) { - rs.idleSessionTimeout = 10 * time.Second - rs.absoluteSessionTimeout = 20 * time.Second - require.NoError(t, client.HSet(ctx, "s1", keyTimeAdded, time.Now()).Err()) - require.NoError(t, rs.refreshExpiration(ctx, "s1", time.Time{})) - - res, err := client.TTL(ctx, "s1").Result() - require.NoError(t, err) - require.Greater(t, res, time.Duration(0)) - require.LessOrEqual(t, res, rs.idleSessionTimeout) - }) -} diff --git a/internal/oidc/session.go b/internal/oidc/session.go deleted file mode 100644 index 6cb2892..0000000 --- a/internal/oidc/session.go +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "context" - "math/rand" - "time" - - "github.com/redis/go-redis/v9" - "github.com/tetratelabs/run" - "github.com/tetratelabs/telemetry" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" - "github.com/tetrateio/authservice-go/internal" -) - -type ( - // SessionStore is an interface for storing session data. - SessionStore interface { - SetTokenResponse(ctx context.Context, sessionID string, tokenResponse *TokenResponse) error - GetTokenResponse(ctx context.Context, sessionID string) (*TokenResponse, error) - SetAuthorizationState(ctx context.Context, sessionID string, authorizationState *AuthorizationState) error - GetAuthorizationState(ctx context.Context, sessionID string) (*AuthorizationState, error) - ClearAuthorizationState(ctx context.Context, sessionID string) error - RemoveSession(ctx context.Context, sessionID string) error - RemoveAllExpired(ctx context.Context) error - } - - // SessionStoreFactory is a factory for managing multiple SessionStores. - // It uses the OIDC configuration to determine which store to use. - SessionStoreFactory interface { - Get(cfg *oidcv1.OIDCConfig) SessionStore - } - - // SessionStoreFactoryUnit is a combination of a run.PreRunner and a SessionStoreFactory. - SessionStoreFactoryUnit interface { - run.PreRunner - SessionStoreFactory - } -) - -var ( - _ SessionStoreFactoryUnit = (*sessionStoreFactory)(nil) -) - -// SessionStoreFactory is a factory for creating session stores. -// It uses the OIDC configuration to determine which store to use. -type sessionStoreFactory struct { - Config *configv1.Config - - log telemetry.Logger - redis map[string]SessionStore - memory SessionStore -} - -// NewSessionStoreFactory creates a factory for managing session stores. -// It uses the OIDC configuration to determine which store to use. -func NewSessionStoreFactory(cfg *configv1.Config) SessionStoreFactoryUnit { - return &sessionStoreFactory{ - Config: cfg, - } -} - -// Name implements run.Unit. -func (s *sessionStoreFactory) Name() string { return "OIDC session store factory" } - -// PreRun initializes the stores that are defined in the configuration -func (s *sessionStoreFactory) PreRun() error { - s.log = internal.Logger(internal.Session) - - s.redis = make(map[string]SessionStore) - clock := &Clock{} - - for _, fc := range s.Config.Chains { - log := s.log.With("chain", fc.Name) - - for _, f := range fc.Filters { - if f.GetOidc() == nil { - continue - } - - if redisServer := f.GetOidc().GetRedisSessionStoreConfig().GetServerUri(); redisServer != "" { - log.Info("initializing redis session store", "redis-url", redisServer) - // No need to check the errors here as it has already been validated when loading the configuration - opts, _ := redis.ParseURL(redisServer) - client := redis.NewClient(opts) - r, err := NewRedisStore(clock, client, - time.Duration(f.GetOidc().GetAbsoluteSessionTimeout())*time.Second, - time.Duration(f.GetOidc().GetIdleSessionTimeout())*time.Second, - ) - if err != nil { - return err - } - s.redis[redisServer] = r - } else if s.memory == nil { // Use a shared in-memory store for all OIDC configurations - log.Info("initializing in-memory session store") - s.memory = NewMemoryStore(clock, - time.Duration(f.GetOidc().GetAbsoluteSessionTimeout())*time.Second, - time.Duration(f.GetOidc().GetIdleSessionTimeout())*time.Second, - ) - } - } - } - - return nil -} - -// Get returns the appropriate session store for the given OIDC configuration. -func (s *sessionStoreFactory) Get(cfg *oidcv1.OIDCConfig) SessionStore { - if cfg == nil { - return nil - } - store, ok := s.redis[cfg.GetRedisSessionStoreConfig().GetServerUri()] - if !ok { - store = s.memory - } - return store -} - -// SessionGenerator is an interface for generating session data. -type SessionGenerator interface { - GenerateSessionID() string - GenerateNonce() string - GenerateState() string -} - -var ( - _ SessionGenerator = (*randomGenerator)(nil) - _ SessionGenerator = (*staticGenerator)(nil) -) - -type ( - // randomGenerator is a session generator that uses random strings. - randomGenerator struct { - rand *rand.Rand - } - - // staticGenerator is a session generator that uses static strings. - staticGenerator struct { - sessionID string - nonce string - state string - } -) - -// NewRandomGenerator creates a new random session generator. -func NewRandomGenerator() SessionGenerator { - return &randomGenerator{ - rand: rand.New(rand.NewSource(time.Now().UnixNano())), - } -} - -func (r randomGenerator) GenerateSessionID() string { - return r.generate(64) -} - -func (r randomGenerator) GenerateNonce() string { - return r.generate(32) -} - -func (r randomGenerator) GenerateState() string { - return r.generate(32) -} - -func (r *randomGenerator) generate(n int) string { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - b := make([]byte, n) - for i := range b { - b[i] = charset[r.rand.Intn(len(charset))] - } - return string(b) -} - -// NewStaticGenerator creates a new static session generator. -func NewStaticGenerator(sessionID, nonce, state string) SessionGenerator { - return &staticGenerator{ - sessionID: sessionID, - nonce: nonce, - state: state, - } -} - -func (s staticGenerator) GenerateSessionID() string { - return s.sessionID -} - -func (s staticGenerator) GenerateNonce() string { - return s.nonce -} - -func (s staticGenerator) GenerateState() string { - return s.state -} diff --git a/internal/oidc/session_test.go b/internal/oidc/session_test.go deleted file mode 100644 index 36185e0..0000000 --- a/internal/oidc/session_test.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "testing" - - "github.com/alicebob/miniredis/v2" - "github.com/redis/go-redis/v9" - "github.com/stretchr/testify/require" - "github.com/tetratelabs/run" - "github.com/tetratelabs/telemetry" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - mockv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/mock" - oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" -) - -func TestSessionStoreFactory(t *testing.T) { - redis1 := miniredis.RunT(t) - redis2 := miniredis.RunT(t) - - config := &configv1.Config{ - ListenAddress: "0.0.0.0", - ListenPort: 8080, - LogLevel: "debug", - Threads: 1, - Chains: []*configv1.FilterChain{ - { - Name: "memory1", - Filters: []*configv1.Filter{ - {Type: &configv1.Filter_Mock{Mock: &mockv1.MockConfig{}}}, - {Type: &configv1.Filter_Oidc{Oidc: &oidcv1.OIDCConfig{}}}, - }, - }, - { - Name: "memory2", - Filters: []*configv1.Filter{ - {Type: &configv1.Filter_Oidc{Oidc: &oidcv1.OIDCConfig{}}}, - }, - }, - { - Name: "redis1", - Filters: []*configv1.Filter{ - { - Type: &configv1.Filter_Oidc{ - Oidc: &oidcv1.OIDCConfig{ - RedisSessionStoreConfig: &oidcv1.RedisConfig{ServerUri: "redis://" + redis1.Addr()}, - }, - }, - }, - }, - }, - { - Name: "redis2", - Filters: []*configv1.Filter{ - { - Type: &configv1.Filter_Oidc{ - Oidc: &oidcv1.OIDCConfig{ - RedisSessionStoreConfig: &oidcv1.RedisConfig{ServerUri: "redis://" + redis2.Addr()}, - }, - }, - }, - }, - }, - }, - } - - store := NewSessionStoreFactory(config).(*sessionStoreFactory) - g := run.Group{Logger: telemetry.NoopLogger()} - g.Register(store) - require.NoError(t, g.Run()) - - require.NotNil(t, store.memory) - require.Len(t, store.redis, 2) - - require.Nil(t, store.Get(nil)) - require.IsType(t, &memoryStore{}, store.Get(&oidcv1.OIDCConfig{})) - require.IsType(t, &memoryStore{}, store.Get(config.Chains[0].Filters[1].GetOidc())) - require.IsType(t, &memoryStore{}, store.Get(config.Chains[1].Filters[0].GetOidc())) - require.Equal(t, redis1.Addr(), store.Get(config.Chains[2].Filters[0].GetOidc()).(*redisStore).client.(*redis.Client).Options().Addr) - require.Equal(t, redis2.Addr(), store.Get(config.Chains[3].Filters[0].GetOidc()).(*redisStore).client.(*redis.Client).Options().Addr) -} - -func TestSessionStoreFactoryRedisFails(t *testing.T) { - mr := miniredis.RunT(t) - config := &configv1.Config{ - ListenAddress: "0.0.0.0", - ListenPort: 8080, - LogLevel: "debug", - Threads: 1, - Chains: []*configv1.FilterChain{ - { - Name: "redis", - Filters: []*configv1.Filter{ - { - Type: &configv1.Filter_Oidc{ - Oidc: &oidcv1.OIDCConfig{ - RedisSessionStoreConfig: &oidcv1.RedisConfig{ServerUri: "redis://" + mr.Addr()}, - }, - }, - }, - }, - }, - }, - } - - store := &sessionStoreFactory{Config: config} - g := run.Group{Logger: telemetry.NoopLogger()} - g.Register(store) - - mr.SetError("server error") - require.ErrorContains(t, g.Run(), "server error") -} - -func TestSessionGenerator(t *testing.T) { - t.Run("random", func(t *testing.T) { - sg := NewRandomGenerator() - require.NotEqual(t, sg.GenerateSessionID(), sg.GenerateSessionID()) - require.NotEqual(t, sg.GenerateState(), sg.GenerateState()) - require.NotEqual(t, sg.GenerateNonce(), sg.GenerateNonce()) - }) - t.Run("static", func(t *testing.T) { - sg := NewStaticGenerator("sessionid", "nonce", "state") - require.Equal(t, sg.GenerateSessionID(), sg.GenerateSessionID()) - require.Equal(t, sg.GenerateState(), sg.GenerateState()) - require.Equal(t, sg.GenerateNonce(), sg.GenerateNonce()) - require.Equal(t, "sessionid", sg.GenerateSessionID()) - require.Equal(t, "state", sg.GenerateState()) - require.Equal(t, "nonce", sg.GenerateNonce()) - }) -} diff --git a/internal/oidc/state.go b/internal/oidc/state.go deleted file mode 100644 index 96236e7..0000000 --- a/internal/oidc/state.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -// AuthorizationState contains information about the state of the authorization process. -type AuthorizationState struct { - State string - Nonce string - RequestedURL string -} diff --git a/internal/oidc/time.go b/internal/oidc/time.go deleted file mode 100644 index 1de4102..0000000 --- a/internal/oidc/time.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import "time" - -// Clock represents a source of current time. -type Clock struct { - // Override for time.Now. - NowFn func() time.Time -} - -// Now returns the current local time. -func (s *Clock) Now() time.Time { - if s.NowFn != nil { - return s.NowFn() - } - return time.Now() -} diff --git a/internal/oidc/time_test.go b/internal/oidc/time_test.go deleted file mode 100644 index 2ec18e8..0000000 --- a/internal/oidc/time_test.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestClockReal(t *testing.T) { - c := Clock{} - require.Greater(t, c.Now().Unix(), int64(0)) -} - -func TestClockCustom(t *testing.T) { - instant := time.Date(2020, time.January, 2, 3, 4, 5, 6, time.UTC) - c := Clock{ - NowFn: func() time.Time { - return instant - }, - } - require.Equal(t, instant, c.Now()) -} diff --git a/internal/oidc/token.go b/internal/oidc/token.go deleted file mode 100644 index 834e97a..0000000 --- a/internal/oidc/token.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "time" - - "github.com/lestrrat-go/jwx/v2/jwt" -) - -// TokenResponse contains information about the tokens returned by the Identity Provider. -type TokenResponse struct { - IDToken string - AccessToken string - AccessTokenExpiresAt time.Time - RefreshToken string -} - -// ParseIDToken parses the ID token string and returns the token and an error if any. -func (t *TokenResponse) ParseIDToken() (jwt.Token, error) { return ParseToken(t.IDToken) } - -// ParseToken parses the token string and returns the token and an error if any. -func ParseToken(token string) (jwt.Token, error) { - return jwt.Parse([]byte(token), jwt.WithValidate(false), jwt.WithVerify(false)) -} diff --git a/internal/oidc/token_test.go b/internal/oidc/token_test.go deleted file mode 100644 index 5871424..0000000 --- a/internal/oidc/token_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package oidc - -import ( - "testing" - "time" - - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/stretchr/testify/require" -) - -func TestParseIDToken(t *testing.T) { - t.Run("valid", func(t *testing.T) { - tr := &TokenResponse{ - IDToken: newToken(), - } - - it, err := tr.ParseIDToken() - require.NoError(t, err) - require.Equal(t, "authservice", it.Issuer()) - }) - - t.Run("invalid", func(t *testing.T) { - tr := &TokenResponse{} - _, err := tr.ParseIDToken() - require.Error(t, err) - }) -} - -func newToken() string { - token, _ := jwt.NewBuilder(). - Issuer("authservice"). - Subject("user"). - Expiration(time.Now().Add(time.Hour)). - Build() - signed, _ := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte("key"))) - return string(signed) -} diff --git a/internal/server/authz.go b/internal/server/authz.go deleted file mode 100644 index 5c139cd..0000000 --- a/internal/server/authz.go +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "context" - "fmt" - "regexp" - "strings" - - envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "github.com/tetratelabs/telemetry" - "google.golang.org/genproto/googleapis/rpc/status" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - "github.com/tetrateio/authservice-go/internal" - "github.com/tetrateio/authservice-go/internal/authz" - "github.com/tetrateio/authservice-go/internal/oidc" -) - -// EnvoyXRequestID is the header name for the request id -const EnvoyXRequestID = "x-request-id" - -var ( - // allow a request - allow = &envoy.CheckResponse{ - Status: &status.Status{ - Code: int32(codes.OK), - Message: "", - }, - } - - // deny a request with the given code and message - deny = func(code codes.Code, message string) *envoy.CheckResponse { - return &envoy.CheckResponse{ - Status: &status.Status{ - Code: int32(code), - Message: message, - }, - } - } -) - -// ExtAuthZFilter is an implementation of the Envoy AuthZ filter. -type ExtAuthZFilter struct { - log telemetry.Logger - cfg *configv1.Config - tlsPool internal.TLSConfigPool - jwks oidc.JWKSProvider - sessions oidc.SessionStoreFactory -} - -// NewExtAuthZFilter creates a new ExtAuthZFilter. -func NewExtAuthZFilter(cfg *configv1.Config, tlsPool internal.TLSConfigPool, jwks oidc.JWKSProvider, sessions oidc.SessionStoreFactory) *ExtAuthZFilter { - return &ExtAuthZFilter{ - log: internal.Logger(internal.Authz), - cfg: cfg, - tlsPool: tlsPool, - jwks: jwks, - sessions: sessions, - } -} - -// Register the ExtAuthZFilter with the given gRPC server. -func (e *ExtAuthZFilter) Register(server *grpc.Server) { - envoy.RegisterAuthorizationServer(server, e) -} - -// Check is the implementation of the Envoy AuthorizationServer interface. -func (e *ExtAuthZFilter) Check(ctx context.Context, req *envoy.CheckRequest) (response *envoy.CheckResponse, err error) { - log := e.log.Context(ctx) - - // If there are no trigger rules, allow the request with no check executions. - // TriggerRules are used to determine which request should be checked by the filter and which don't. - if !mustTriggerCheck(log, e.cfg.TriggerRules, req) { - log.Debug(fmt.Sprintf("no matching trigger rule, so allowing request to proceed without any authservice functionality %s://%s%s", - req.GetAttributes().GetRequest().GetHttp().GetScheme(), - req.GetAttributes().GetRequest().GetHttp().GetHost(), - req.GetAttributes().GetRequest().GetHttp().GetPath())) - return allow, nil - } - - for _, c := range e.cfg.Chains { - match := matches(c.Match, req) - - log = log.With("chain", c.Name) - log.Debug("evaluate filter chain", "match", match) - - if !match { - continue - } - - if len(c.Filters) == 0 { - log.Debug("no filters in chain, allowing request") - return allow, nil - } - - resp := &envoy.CheckResponse{} - - // Inside a filter chain, all filters must match - for i, f := range c.Filters { - log.Debug("applying filter", "type", fmt.Sprintf("%T", f.Type), "index", i) - - // Note that the Default_Oidc or the Oidc_Override types can't reach this point. The configurations have - // already been merged when loaded from the configuration file and populated accordingly in the Oidc settings. - var h authz.Handler - switch ft := f.Type.(type) { - case *configv1.Filter_Mock: - h = authz.NewMockHandler(ft.Mock) - case *configv1.Filter_Oidc: - if h, err = authz.NewOIDCHandler(ft.Oidc, e.tlsPool, e.jwks, e.sessions, oidc.Clock{}, oidc.NewRandomGenerator()); err != nil { - return nil, err - } - } - - if err = h.Process(ctx, req, resp); err != nil { - // If there is an error just return it without a verdict, and let the Envoy ext_authz - // failure policy decide if the request can continue or not. - return nil, err - } - - allow := codes.Code(resp.Status.Code) == codes.OK - log.Debug("filter result", "index", i, "allow", allow, "error", err) - if !allow { - return resp, nil - } - } - - // At this point all filters allowed the request, so return the response with any additional headers the filters may have added. - return resp, nil - } - - if e.cfg.AllowUnmatchedRequests { - return allow, nil - } - - return deny(codes.PermissionDenied, "no chains matched"), nil -} - -// matches returns true if the given request matches the given match configuration -func matches(m *configv1.Match, req *envoy.CheckRequest) bool { - if m == nil { - return true - } - headerValue := req.GetAttributes().GetRequest().GetHttp().GetHeaders()[strings.ToLower(m.Header)] - if m.GetEquality() != "" { - return headerValue == m.GetEquality() - } - return strings.HasPrefix(headerValue, m.GetPrefix()) -} - -// mustTriggerCheck returns true if the request must be checked by the authservice filters. -// If any of the TriggerRules match the request path, then the request must be checked. -func mustTriggerCheck(log telemetry.Logger, rules []*configv1.TriggerRule, req *envoy.CheckRequest) bool { - // If there are no trigger rules, authservice checks should be triggered for all requests. - // If the request path is empty, (unlikely, but the piece used to match the rules) then trigger the checks. - if len(rules) == 0 || len(req.GetAttributes().GetRequest().GetHttp().GetPath()) == 0 { - return true - } - - for i, rule := range rules { - l := log.With("rule-index", i) - if matchTriggerRule(l, rule, req.GetAttributes().GetRequest().GetHttp().GetPath()) { - return true - } - } - return false -} - -func matchTriggerRule(log telemetry.Logger, rule *configv1.TriggerRule, path string) bool { - if rule == nil { - return false - } - - // if any of the excluded paths match, this rule doesn't match - for i, match := range rule.GetExcludedPaths() { - l := log.With("excluded-match-index", i) - if stringMatch(l, match, path) { - return false - } - } - - // if no excluded paths match, and there are no included paths, this rule matches - if len(rule.GetIncludedPaths()) == 0 { - return true - } - - for i, match := range rule.GetIncludedPaths() { - // if any of the included paths match, this rule matches - l := log.With("included-match-index", i) - if stringMatch(l, match, path) { - return true - } - } - - // if none of the included paths match, this rule doesn't match - return false -} - -func stringMatch(log telemetry.Logger, match *configv1.StringMatch, path string) bool { - switch m := match.GetMatchType().(type) { - case *configv1.StringMatch_Exact: - return m.Exact == path - case *configv1.StringMatch_Prefix: - return strings.HasPrefix(path, m.Prefix) - case *configv1.StringMatch_Suffix: - return strings.HasSuffix(path, m.Suffix) - case *configv1.StringMatch_Regex: - b, err := regexp.MatchString(m.Regex, path) - if err != nil { - log.Error("error matching regex", err, "regex", m.Regex, "match", false) - } - return b - default: - return false - } -} diff --git a/internal/server/authz_test.go b/internal/server/authz_test.go deleted file mode 100644 index 9f87879..0000000 --- a/internal/server/authz_test.go +++ /dev/null @@ -1,367 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "context" - "testing" - - envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "github.com/stretchr/testify/require" - "github.com/tetratelabs/telemetry" - "google.golang.org/grpc/codes" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - mockv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/mock" -) - -func TestUnmatchedRequests(t *testing.T) { - tests := []struct { - name string - allow bool - want codes.Code - }{ - {"allow", true, codes.OK}, - {"deny", false, codes.PermissionDenied}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - e := NewExtAuthZFilter(&configv1.Config{AllowUnmatchedRequests: tt.allow}, nil, nil, nil) - got, err := e.Check(context.Background(), &envoy.CheckRequest{}) - require.NoError(t, err) - require.Equal(t, int32(tt.want), got.Status.Code) - }) - } -} - -func TestFiltersMatch(t *testing.T) { - tests := []struct { - name string - filters []*configv1.Filter - want codes.Code - }{ - {"no-filters", nil, codes.OK}, - {"all-filters-match", []*configv1.Filter{mock(true), mock(true)}, codes.OK}, - {"one-filter-deny", []*configv1.Filter{mock(true), mock(false)}, codes.PermissionDenied}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := &configv1.Config{Chains: []*configv1.FilterChain{{Filters: tt.filters}}} - e := NewExtAuthZFilter(cfg, nil, nil, nil) - - got, err := e.Check(context.Background(), &envoy.CheckRequest{}) - require.NoError(t, err) - require.Equal(t, int32(tt.want), got.Status.Code) - }) - } -} - -func TestUseFirstMatchingChain(t *testing.T) { - cfg := &configv1.Config{ - Chains: []*configv1.FilterChain{ - { - // Chain to be ignored - Match: eq("no-match"), - Filters: []*configv1.Filter{mock(false)}, - }, - { - // Chain to be used - Match: eq("match"), - Filters: []*configv1.Filter{mock(true)}, - }, - { - // Always matches but should not be used as the previous - // chain already matched - Filters: []*configv1.Filter{mock(false)}, - }, - }, - } - - e := NewExtAuthZFilter(cfg, nil, nil, nil) - - got, err := e.Check(context.Background(), header("match")) - require.NoError(t, err) - require.Equal(t, int32(codes.OK), got.Status.Code) -} - -func TestMatch(t *testing.T) { - tests := []struct { - name string - match *configv1.Match - req *envoy.CheckRequest - want bool - }{ - {"no-headers", eq("test"), &envoy.CheckRequest{}, false}, - {"no-match-condition", nil, &envoy.CheckRequest{}, true}, - {"equality-match", eq("test"), header("test"), true}, - {"equality-no-match", eq("test"), header("no-match"), false}, - {"prefix-match", prefix("test"), header("test-123"), true}, - {"prefix-no-match", prefix("test"), header("no-match"), false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, matches(tt.match, tt.req)) - }) - } -} - -func TestGrpcNoChainsMatched(t *testing.T) { - e := NewExtAuthZFilter(&configv1.Config{}, nil, nil, nil) - s := NewTestServer(e.Register) - go func() { require.NoError(t, s.Start()) }() - t.Cleanup(s.Stop) - - conn, err := s.GRPCConn() - require.NoError(t, err) - client := envoy.NewAuthorizationClient(conn) - - ok, err := client.Check(context.Background(), header("test")) - require.NoError(t, err) - require.Equal(t, int32(codes.PermissionDenied), ok.Status.Code) -} - -func TestStringMatch(t *testing.T) { - tests := []struct { - name string - match *configv1.StringMatch - path string - want bool - }{ - {"no-match", nil, "", false}, - {"equality-match", stringExact("test"), "test", true}, - {"equality-no-match", stringExact("test"), "no-match", false}, - {"prefix-match", stringPrefix("test"), "test-123", true}, - {"prefix-no-match", stringPrefix("test"), "no-match", false}, - {"suffix-match", stringSuffix("test"), "123-test", true}, - {"suffix-no-match", stringSuffix("test"), "no-match", false}, - {"regex-match", stringRegex(".*st"), "test", true}, - {"regex-no-match", stringRegex(".*st"), "no-match", false}, - {"regex-invalid", stringRegex("["), "no-match", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, stringMatch(telemetry.NoopLogger(), tt.match, tt.path)) - }) - } -} - -func TestMatchTriggerRule(t *testing.T) { - tests := []struct { - name string - rule *configv1.TriggerRule - path string - want bool - }{ - {"no-rule", nil, "/path", false}, - {"no-path", &configv1.TriggerRule{}, "", true}, - {"empty-rule", &configv1.TriggerRule{}, "/path", true}, - {"excluded-path-match", &configv1.TriggerRule{ExcludedPaths: []*configv1.StringMatch{stringExact("/path")}}, "/path", false}, - {"excluded-path-no-match", &configv1.TriggerRule{ExcludedPaths: []*configv1.StringMatch{stringExact("/path")}}, "/no-match", true}, - {"included-path-match", &configv1.TriggerRule{IncludedPaths: []*configv1.StringMatch{stringExact("/path")}}, "/path", true}, - {"included-path-no-match", &configv1.TriggerRule{IncludedPaths: []*configv1.StringMatch{stringExact("/path")}}, "/no-match", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, matchTriggerRule(telemetry.NoopLogger(), tt.rule, tt.path)) - }) - } - -} - -func TestMustTriggerCheck(t *testing.T) { - test := []struct { - name string - rules []*configv1.TriggerRule - path string - want bool - }{ - {"no-rules", nil, "/path", true}, - {"no-path", nil, "", true}, - {"empty-rules", []*configv1.TriggerRule{}, "/path", true}, - {"inclusions-match", []*configv1.TriggerRule{{IncludedPaths: []*configv1.StringMatch{stringExact("/path")}}}, "/path", true}, - {"inclusions-no-match", []*configv1.TriggerRule{{IncludedPaths: []*configv1.StringMatch{stringExact("/path")}}}, "/no-match", false}, - {"exclusions-match", []*configv1.TriggerRule{{ExcludedPaths: []*configv1.StringMatch{stringExact("/path")}}}, "/path", false}, - {"exclusions-no-match", []*configv1.TriggerRule{{ExcludedPaths: []*configv1.StringMatch{stringExact("/path")}}}, "/no-match", true}, - {"multiple-rules-no-match", []*configv1.TriggerRule{ - {ExcludedPaths: []*configv1.StringMatch{stringExact("/ex-path")}}, - {IncludedPaths: []*configv1.StringMatch{stringExact("/path")}}, - }, "/ex-path", false}, - {"multiple-rules-match", []*configv1.TriggerRule{ - {ExcludedPaths: []*configv1.StringMatch{stringExact("/no-path")}}, - {IncludedPaths: []*configv1.StringMatch{stringExact("/path")}}, - }, "/path", true}, - {"inverse-order-multiple-rules-no-match", []*configv1.TriggerRule{ - {IncludedPaths: []*configv1.StringMatch{stringExact("/path")}}, - {ExcludedPaths: []*configv1.StringMatch{stringExact("/ex-path")}}, - }, "/ex-path", false}, - {"inverse-order-multiple-rules-match", []*configv1.TriggerRule{ - {IncludedPaths: []*configv1.StringMatch{stringExact("/path")}}, - {ExcludedPaths: []*configv1.StringMatch{stringExact("/no-path")}}, - }, "/path", true}, - } - - for _, tt := range test { - t.Run(tt.name, func(t *testing.T) { - req := &envoy.CheckRequest{ - Attributes: &envoy.AttributeContext{ - Request: &envoy.AttributeContext_Request{ - Http: &envoy.AttributeContext_HttpRequest{ - Path: tt.path, - }, - }, - }, - } - require.Equal(t, tt.want, mustTriggerCheck(telemetry.NoopLogger(), tt.rules, req)) - }) - } -} - -func TestCheckTriggerRules(t *testing.T) { - tests := []struct { - name string - config *configv1.Config - path string - want codes.Code - }{ - { - "no-rules-triggers-check", - &configv1.Config{ - Chains: []*configv1.FilterChain{{Filters: []*configv1.Filter{mock(false)}}}, - }, - "/path", codes.PermissionDenied, - }, - { - "rules-inclusions-matching-triggers-check", - &configv1.Config{ - Chains: []*configv1.FilterChain{{Filters: []*configv1.Filter{mock(false)}}}, - TriggerRules: []*configv1.TriggerRule{ - { - IncludedPaths: []*configv1.StringMatch{stringExact("/path")}, - }, - }, - }, - "/path", codes.PermissionDenied}, - { - "rules-inclusions-no-match-does-not-trigger-check", - &configv1.Config{ - Chains: []*configv1.FilterChain{{Filters: []*configv1.Filter{mock(false)}}}, - TriggerRules: []*configv1.TriggerRule{ - { - IncludedPaths: []*configv1.StringMatch{stringExact("/path")}, - }, - }, - }, - "/no-path", codes.OK, // it does not reach mock(allow=false), so it returns OK - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - e := NewExtAuthZFilter(tt.config, nil, nil, nil) - req := &envoy.CheckRequest{ - Attributes: &envoy.AttributeContext{ - Request: &envoy.AttributeContext_Request{ - Http: &envoy.AttributeContext_HttpRequest{ - Path: tt.path, - }, - }, - }, - } - got, err := e.Check(context.Background(), req) - require.NoError(t, err) - require.Equal(t, int32(tt.want), got.Status.Code) - }) - } -} - -func mock(allow bool) *configv1.Filter { - return &configv1.Filter{ - Type: &configv1.Filter_Mock{ - Mock: &mockv1.MockConfig{ - Allow: allow, - }, - }, - } -} - -func eq(value string) *configv1.Match { - return &configv1.Match{ - Header: "X-Test-Headers", - Criteria: &configv1.Match_Equality{ - Equality: value, - }, - } -} - -func prefix(value string) *configv1.Match { - return &configv1.Match{ - Header: "X-Test-Headers", - Criteria: &configv1.Match_Prefix{ - Prefix: value, - }, - } -} - -func header(value string) *envoy.CheckRequest { - return &envoy.CheckRequest{ - Attributes: &envoy.AttributeContext{ - Request: &envoy.AttributeContext_Request{ - Http: &envoy.AttributeContext_HttpRequest{ - Headers: map[string]string{ - "x-request-id": "test-request-id", - "x-test-headers": value, - }, - }, - }, - }, - } -} - -func stringExact(s string) *configv1.StringMatch { - return &configv1.StringMatch{ - MatchType: &configv1.StringMatch_Exact{ - Exact: s, - }, - } -} - -func stringPrefix(s string) *configv1.StringMatch { - return &configv1.StringMatch{ - MatchType: &configv1.StringMatch_Prefix{ - Prefix: s, - }, - } -} - -func stringSuffix(s string) *configv1.StringMatch { - return &configv1.StringMatch{ - MatchType: &configv1.StringMatch_Suffix{ - Suffix: s, - }, - } -} - -func stringRegex(s string) *configv1.StringMatch { - return &configv1.StringMatch{ - MatchType: &configv1.StringMatch_Regex{ - Regex: s, - }, - } -} diff --git a/internal/server/health.go b/internal/server/health.go deleted file mode 100644 index 1cd436d..0000000 --- a/internal/server/health.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "fmt" - "net" - "net/http" - - "github.com/tetratelabs/run" - "github.com/tetratelabs/telemetry" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - "github.com/tetrateio/authservice-go/internal" -) - -const ( - HealthzPath = "/healthz" - HealthzPort = 10004 -) - -var ( - _ http.Handler = (*healthServer)(nil) - _ run.Service = (*healthServer)(nil) -) - -type healthServer struct { - log telemetry.Logger - config *configv1.Config - server *http.Server - - // Listen allows overriding the default listener. It is meant to - // be used in tests. - l net.Listener -} - -// NewHealthServer creates a new health server. -func NewHealthServer(config *configv1.Config) run.Unit { - hs := &healthServer{ - log: internal.Logger(internal.Health), - config: config, - } - httpServer := &http.Server{Handler: hs} - hs.server = httpServer - return hs -} - -// Name implements run.Unit. -func (hs *healthServer) Name() string { - return "Health Server" -} - -// Serve implements run.Service. -func (hs *healthServer) Serve() error { - // use test listener if set - if hs.l == nil { - var err error - hs.l, err = net.Listen("tcp", hs.getAddressAndPort()) - if err != nil { - return err - } - } - - hs.log.Info("starting health server", "addr", hs.l.Addr(), "path", hs.getPath()) - return hs.server.Serve(hs.l) -} - -// GracefulStop implements run.Service. -func (hs *healthServer) GracefulStop() { - hs.log.Info("stopping health server") - _ = hs.server.Close() -} - -func (hs *healthServer) getAddressAndPort() string { - addr := hs.config.GetHealthListenAddress() - if addr == "" { - addr = hs.config.GetListenAddress() - } - - port := hs.config.GetHealthListenPort() - if port == 0 { - port = HealthzPort - } - - return fmt.Sprintf("%s:%d", addr, port) -} - -func (hs *healthServer) getPath() string { - path := hs.config.GetHealthListenPath() - if path != "" { - return path - } - return HealthzPath -} - -// ServeHTTP implements http.Handler. -func (hs *healthServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - log := hs.log.With("method", r.Method, "path", r.URL.Path) - listenPath := hs.getPath() - - if r.Method != http.MethodGet || r.URL.Path != listenPath { - log.Debug("invalid request") - http.Error(w, fmt.Sprintf("only GET %s is allowed", listenPath), http.StatusBadRequest) - return - } - - w.WriteHeader(http.StatusOK) -} diff --git a/internal/server/health_test.go b/internal/server/health_test.go deleted file mode 100644 index 036684a..0000000 --- a/internal/server/health_test.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "context" - "net" - "net/http" - "testing" - - "github.com/stretchr/testify/require" - "github.com/tetratelabs/run" - "github.com/tetratelabs/run/pkg/test" - "github.com/tetratelabs/telemetry" - "google.golang.org/grpc/test/bufconn" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" -) - -func TestHealthServer(t *testing.T) { - - var ( - g = run.Group{Logger: telemetry.NoopLogger()} - irq = test.NewIRQService(func() {}) - l = bufconn.Listen(1024) - hs = NewHealthServer(nil) - ) - - hs.(*healthServer).l = l - g.Register(hs, irq) - - go func() { - require.NoError(t, g.Run()) - }() - t.Cleanup(func() { - require.NoError(t, irq.Close()) - }) - - client := http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return l.Dial() - }, - }, - } - - tests := []struct { - method string - url string - want int - }{ - {http.MethodGet, "http://bufconn" + HealthzPath, http.StatusOK}, - {http.MethodGet, "http://bufconn" + "/other", http.StatusBadRequest}, - {http.MethodPost, "http://bufconn" + HealthzPath, http.StatusBadRequest}, - {http.MethodPost, "http://bufconn" + "/other", http.StatusBadRequest}, - } - - for _, tt := range tests { - t.Run(tt.method+" "+tt.url, func(t *testing.T) { - req, err := http.NewRequest(tt.method, tt.url, nil) - require.NoError(t, err) - resp, err := client.Do(req) - require.NoError(t, err) - require.Equal(t, tt.want, resp.StatusCode) - }) - } -} - -func TestHealthConfig(t *testing.T) { - - tests := []struct { - name string - config *configv1.Config - wantAddress, wantPath string - }{ - {"default", nil, ":10004", "/healthz"}, - {"address", &configv1.Config{HealthListenAddress: "test"}, "test:10004", "/healthz"}, - {"port", &configv1.Config{HealthListenPort: 8000}, ":8000", "/healthz"}, - {"address and port", &configv1.Config{HealthListenAddress: "test", HealthListenPort: 8000}, "test:8000", "/healthz"}, - {"path", &configv1.Config{HealthListenPath: "/test"}, ":10004", "/test"}, - {"all", &configv1.Config{HealthListenAddress: "test", HealthListenPort: 8000, HealthListenPath: "/test"}, "test:8000", "/test"}, - {"address defaults to listen address", &configv1.Config{ListenAddress: "test"}, "test:10004", "/healthz"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - hs := NewHealthServer(tt.config) - require.Equal(t, tt.wantAddress, hs.(*healthServer).getAddressAndPort()) - require.Equal(t, tt.wantPath, hs.(*healthServer).getPath()) - }) - } -} diff --git a/internal/server/logging.go b/internal/server/logging.go deleted file mode 100644 index 34ef2e2..0000000 --- a/internal/server/logging.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "context" - "encoding/json" - - "github.com/tetratelabs/telemetry" - "google.golang.org/grpc" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - - "github.com/tetrateio/authservice-go/internal" -) - -// LogMiddleware is a gRPC middleware that logs all the requests and responses. -type LogMiddleware struct { - log telemetry.Logger -} - -// NewLogMiddleware creates a new LogMiddleware that logs all gRPC requests and responses. -func NewLogMiddleware() LogMiddleware { - return LogMiddleware{ - log: internal.Logger(internal.Requests), - } -} - -// UnaryServerInterceptor is a grpc.UnaryServerInterceptor that logs all the server requests and responses. -func (l LogMiddleware) UnaryServerInterceptor( - ctx context.Context, - req interface{}, - info *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, -) (interface{}, error) { - log := l.log.Context(ctx) - - log.Debug("request", "method", info.FullMethod, "data", toJSON(req)) - resp, err := handler(ctx, req) - log.Debug("response", "method", info.FullMethod, "data", toJSON(resp), "error", err) - - return resp, err -} - -func toJSON(obj interface{}) string { - var data []byte - message, ok := obj.(proto.Message) - if !ok { - data, _ = json.Marshal(obj) - } else { - data, _ = protojson.Marshal(message) - } - return string(data) -} diff --git a/internal/server/requestid.go b/internal/server/requestid.go deleted file mode 100644 index 8ddfb39..0000000 --- a/internal/server/requestid.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "context" - - envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "github.com/tetratelabs/telemetry" - "google.golang.org/grpc" -) - -// PropagateRequestID is a gRPC middleware that propagates the request id from an Envoy CheckRequest -// to the logging context. -func PropagateRequestID( - ctx context.Context, - req interface{}, - _ *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, -) (interface{}, error) { - check, ok := req.(*envoy.CheckRequest) - if !ok { - return handler(ctx, req) - } - - headers := check.GetAttributes().GetRequest().GetHttp().GetHeaders() - if headers == nil || headers[EnvoyXRequestID] == "" { - return handler(ctx, req) - } - - ctx = telemetry.KeyValuesToContext(ctx, EnvoyXRequestID, headers[EnvoyXRequestID]) - return handler(ctx, req) -} diff --git a/internal/server/requestid_test.go b/internal/server/requestid_test.go deleted file mode 100644 index 116f38f..0000000 --- a/internal/server/requestid_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "context" - "testing" - - envoy "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - "github.com/stretchr/testify/require" - "github.com/tetratelabs/telemetry" -) - -func TestPropagateRequestId(t *testing.T) { - tests := []struct { - name string - req interface{} - want []interface{} - }{ - {"not-envoy-request", struct{}{}, nil}, - {"no-x-request-id", &envoy.CheckRequest{}, nil}, - {"with-x-request-id", header("test"), []interface{}{EnvoyXRequestID, "test-request-id"}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - _, _ = PropagateRequestID(ctx, tt.req, nil, func(ctx context.Context, req interface{}) (interface{}, error) { - kvs := telemetry.KeyValuesFromContext(ctx) - require.Equal(t, tt.want, kvs) - return nil, nil - }) - }) - } -} diff --git a/internal/server/server.go b/internal/server/server.go deleted file mode 100644 index ae0fc10..0000000 --- a/internal/server/server.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "fmt" - "net" - - "github.com/tetratelabs/run" - "github.com/tetratelabs/telemetry" - "google.golang.org/grpc" - - configv1 "github.com/tetrateio/authservice-go/config/gen/go/v1" - "github.com/tetrateio/authservice-go/internal" -) - -// RegisterGrpc is an interface for registering gRPC registerHandlers. -type RegisterGrpc interface { - // Register a gRPC handler in the given server. - Register(s *grpc.Server) -} - -var ( - _ run.PreRunner = (*Server)(nil) - _ run.Service = (*Server)(nil) -) - -// Server that runs as a unit in a run.Group. -type Server struct { - log telemetry.Logger - cfg *configv1.Config - - server *grpc.Server - registerHandlers []func(s *grpc.Server) - - // Listen allows overriding the default listener. It is meant to - // be used in tests. - Listen func() (net.Listener, error) -} - -// New creates a new dual gRPC server. -func New(cfg *configv1.Config, registerHandlers ...func(s *grpc.Server)) *Server { - return &Server{ - log: internal.Logger(internal.Server), - cfg: cfg, - registerHandlers: registerHandlers, - } -} - -// Name returns the name of the unit in the run.Group. -func (s *Server) Name() string { return "gRPC Server" } - -// PreRun registers the server registerHandlers -func (s *Server) PreRun() error { - if s.Listen == nil { - s.Listen = func() (net.Listener, error) { - return net.Listen("tcp", fmt.Sprintf("%s:%d", s.cfg.ListenAddress, s.cfg.ListenPort)) - } - } - - logMiddleware := NewLogMiddleware() - - // Initialize the gRPC server - s.server = grpc.NewServer( // TODO(nacx): Expose the right flags for secure connections - grpc.ChainUnaryInterceptor( - PropagateRequestID, - logMiddleware.UnaryServerInterceptor, - ), - ) - - for _, h := range s.registerHandlers { - h(s.server) - } - - return nil -} - -// Serve starts the gRPC server. -func (s *Server) Serve() error { - l, err := s.Listen() - if err != nil { - return err - } - s.log.Info("starting gRPC server", "addr", l.Addr()) - return s.server.Serve(l) -} - -// GracefulStop stops the server. -func (s *Server) GracefulStop() { - s.log.Info("stopping gRPC server") - if s.server != nil { - s.server.GracefulStop() - } -} diff --git a/internal/server/server_test.go b/internal/server/server_test.go deleted file mode 100644 index f50ed14..0000000 --- a/internal/server/server_test.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package server - -import ( - "context" - "errors" - "net" - "testing" - - "github.com/stretchr/testify/require" - "github.com/tetratelabs/run" - "github.com/tetratelabs/run/pkg/test" - "github.com/tetratelabs/telemetry" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/interop" - testgrpc "google.golang.org/grpc/interop/grpc_testing" - "google.golang.org/grpc/test/bufconn" -) - -func TestGrpcServer(t *testing.T) { - s := NewTestServer(func(s *grpc.Server) { - testgrpc.RegisterTestServiceServer(s, interop.NewTestServer()) - }) - go func() { require.NoError(t, s.Start()) }() - t.Cleanup(s.Stop) - - conn, err := s.GRPCConn() - require.NoError(t, err) - - client := testgrpc.NewTestServiceClient(conn) - interop.DoEmptyUnaryCall(context.Background(), client) // this method will panic if fails -} - -func TestListenFails(t *testing.T) { - err := errors.New("listen failed") - s := New(nil) - s.Listen = func() (net.Listener, error) { return nil, err } - require.ErrorIs(t, s.Serve(), err) -} - -// TestServer that uses an in-memory listener for connections. -type TestServer struct { - g run.Group - l *bufconn.Listener - dialOpts []grpc.DialOption - shutdown func() -} - -// NewTestServer creates a new test server. -func NewTestServer(handlers ...func(s *grpc.Server)) *TestServer { - var ( - g = run.Group{Logger: telemetry.NoopLogger()} - irq = test.NewIRQService(func() {}) - l = bufconn.Listen(1024) - dialOpts = []grpc.DialOption{ - grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return l.Dial() }), - grpc.WithTransportCredentials(insecure.NewCredentials()), - } - - s = New(nil, handlers...) - ) - - s.Listen = func() (net.Listener, error) { return l, nil } - g.Register(s, irq) - - return &TestServer{ - g: g, - l: l, - dialOpts: dialOpts, - shutdown: func() { _ = irq.Close() }, - } -} - -// GRPCConn returns a gRPC connection that connects to the test server. -func (s *TestServer) GRPCConn() (*grpc.ClientConn, error) { - return grpc.Dial("bufnet", s.dialOpts...) -} - -// Start starts the server. This blocks until the server is stopped. -func (s *TestServer) Start() error { - return s.g.Run() -} - -// Stop the test server. -func (s *TestServer) Stop() { - s.shutdown() -} diff --git a/internal/testdata/duplicate-oidc.json b/internal/testdata/duplicate-oidc.json deleted file mode 100644 index 2e134c5..0000000 --- a/internal/testdata/duplicate-oidc.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "default_oidc_config": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - } - }, - "chains": [ - { - "name": "oidc", - "filters": [ - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - } - } - } - ] - } - ] -} diff --git a/internal/testdata/invalid-callback-logout.json b/internal/testdata/invalid-callback-logout.json deleted file mode 100644 index b103c94..0000000 --- a/internal/testdata/invalid-callback-logout.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "mock", - "filters": [ - { - "oidc": { - "callback_uri": "http://fake/same", - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "client_id": "fake", - "client_secret": "fake", - "jwks": "fake", - "id_token": { - "header": "authorization", - "preamble": "Bearer" - }, - "logout": { - "path": "/same" - } - } - } - ] - } - ] -} diff --git a/internal/testdata/invalid-callback.json b/internal/testdata/invalid-callback.json deleted file mode 100644 index fd3bc14..0000000 --- a/internal/testdata/invalid-callback.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "mock", - "filters": [ - { - "oidc": { - "callback_uri": "http://fake", - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "client_id": "fake", - "client_secret": "fake", - "jwks": "fake", - "id_token": { - "header": "authorization", - "preamble": "Bearer" - } - } - } - ] - } - ] -} diff --git a/internal/testdata/invalid-config.json b/internal/testdata/invalid-config.json deleted file mode 100644 index c8c4105..0000000 --- a/internal/testdata/invalid-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "foo": "bar" -} diff --git a/internal/testdata/invalid-health-port.json b/internal/testdata/invalid-health-port.json deleted file mode 100644 index 169f65e..0000000 --- a/internal/testdata/invalid-health-port.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "listen_port": 8000, - "health_listen_port": 8000 -} diff --git a/internal/testdata/invalid-logout.json b/internal/testdata/invalid-logout.json deleted file mode 100644 index 97faa5f..0000000 --- a/internal/testdata/invalid-logout.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "mock", - "filters": [ - { - "oidc": { - "callback_uri": "http://fake/callback", - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "client_id": "fake", - "client_secret": "fake", - "jwks": "fake", - "id_token": { - "header": "authorization", - "preamble": "Bearer" - }, - "logout": { - "path": "/" - } - } - } - ] - } - ] -} diff --git a/internal/testdata/invalid-oidc-client-secret-ref.json b/internal/testdata/invalid-oidc-client-secret-ref.json deleted file mode 100644 index f2979dc..0000000 --- a/internal/testdata/invalid-oidc-client-secret-ref.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret_ref": { - "name": "" - }, - "id_token": { - "preamble": "Bearer", - "header": "authorization" - } - } - } - ] - } - ] -} diff --git a/internal/testdata/invalid-oidc-client-secret.json b/internal/testdata/invalid-oidc-client-secret.json deleted file mode 100644 index 09dbd8d..0000000 --- a/internal/testdata/invalid-oidc-client-secret.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - } - } - } - ] - } - ] -} diff --git a/internal/testdata/invalid-oidc-override.json b/internal/testdata/invalid-oidc-override.json deleted file mode 100644 index 336fe50..0000000 --- a/internal/testdata/invalid-oidc-override.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "oidc_override": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - } - } - } - ] - } - ] -} diff --git a/internal/testdata/invalid-oidc-uris.json b/internal/testdata/invalid-oidc-uris.json deleted file mode 100644 index d688bcc..0000000 --- a/internal/testdata/invalid-oidc-uris.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "oidc": { - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - } - } - } - ] - } - ] -} diff --git a/internal/testdata/invalid-redis.json b/internal/testdata/invalid-redis.json deleted file mode 100644 index 1c5d9fc..0000000 --- a/internal/testdata/invalid-redis.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "http://fake" - } - } - } - ] - } - ] -} diff --git a/internal/testdata/invalid-values.json b/internal/testdata/invalid-values.json deleted file mode 100644 index 85b7a26..0000000 --- a/internal/testdata/invalid-values.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "listen_address": "INVALID", - "listen_port": 999999999, - "log_level": "debug", - "chains": [ - { - "name": "mock", - "filters": [ - { - "mock": { - "allow": true - } - } - ] - } - ] -} diff --git a/internal/testdata/mock.json b/internal/testdata/mock.json deleted file mode 100644 index 45f6b2e..0000000 --- a/internal/testdata/mock.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "mock", - "filters": [ - { - "mock": { - "allow": true - } - } - ] - } - ] -} diff --git a/internal/testdata/multiple-oidc.json b/internal/testdata/multiple-oidc.json deleted file mode 100644 index dafe29b..0000000 --- a/internal/testdata/multiple-oidc.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - } - } - }, - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - } - } - } - ] - } - ] -} diff --git a/internal/testdata/oidc-dynamic.json b/internal/testdata/oidc-dynamic.json deleted file mode 100644 index 09ce90f..0000000 --- a/internal/testdata/oidc-dynamic.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "oidc": { - "configuration_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "jwks": "fake-jwks", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "skip_verify_peer_cert": true - } - } - ] - } - ] -} diff --git a/internal/testdata/oidc-override.json b/internal/testdata/oidc-override.json deleted file mode 100644 index f2fda81..0000000 --- a/internal/testdata/oidc-override.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "default_oidc_config": { - "authorization_uri": "http://default", - "token_uri": "http://default", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake" - }, - "chains": [ - { - "name": "oidc", - "filters": [ - { - "oidc_override": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "tcp://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - }, - "skip_verify_peer_cert": true, - "jwks_fetcher": { - "jwks_uri": "http://fake/jwks", - "skip_verify_peer_cert": "true" - }, - "trusted_certificate_authority": "fake-ca-pem" - } - } - ] - } - ] -} diff --git a/internal/testdata/oidc.json b/internal/testdata/oidc.json deleted file mode 100644 index 5f8b05f..0000000 --- a/internal/testdata/oidc.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "listen_address": "0.0.0.0", - "listen_port": 8080, - "log_level": "debug", - "chains": [ - { - "name": "oidc", - "filters": [ - { - "oidc": { - "authorization_uri": "http://fake", - "token_uri": "http://fake", - "callback_uri": "http://fake/callback", - "proxy_uri": "http://fake", - "client_id": "fake-client-id", - "client_secret": "fake-client-secret", - "id_token": { - "preamble": "Bearer", - "header": "authorization" - }, - "redis_session_store_config": { - "server_uri": "redis://localhost:6379/0" - }, - "logout": { - "path": "/logout", - "redirect_uri": "http://fake" - }, - "skip_verify_peer_cert": true, - "jwks_fetcher": { - "jwks_uri": "http://fake/jwks", - "skip_verify_peer_cert": "true" - }, - "trusted_certificate_authority": "fake-ca-pem" - } - } - ] - } - ] -} diff --git a/internal/testdata/valid-logout-override-default.json b/internal/testdata/valid-logout-override-default.json deleted file mode 100644 index f8d31fe..0000000 --- a/internal/testdata/valid-logout-override-default.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "allow_unmatched_requests": true, - "listen_address": "0.0.0.0", - "listen_port": "10003", - "log_level": "trace", - "default_oidc_config": { - "authorization_uri": "https://fake/auth", - "token_uri": "https://fake/token", - "jwks_fetcher": { - "jwks_uri": "https://fake/certs" - }, - "client_id": "global_id", - "client_secret": "global_secret", - "id_token": { - "preamble": "Bearer", - "header": "Authorization" - }, - "logout": { - "path": "/globallogout", - "redirect_uri": "https://fake/logout" - } - }, - "threads": 8, - "chains": [ - { - "name": "jaeger", - "match": { - "header": ":authority", - "prefix": "some" - }, - "filters": [ - { - "oidc_override": { - "authorization_uri": "https://fake/auth", - "token_uri": "https://fale/token", - "callback_uri": "https://some/login", - "client_id": "client-id", - "logout": { - "redirect_uri": "https://fake/logout" - } - } - } - ] - }, - { - "name": "local", - "match": { - "header": ":local", - "prefix": "localhost" - }, - "filters": [ - { - "oidc_override": { - "callback_uri": "https://localhost/login", - "client_id": "local_id", - "client_secret": "local_secret", - "cookie_name_prefix": "local", - "logout": { - "path": "/local", - "redirect_uri": "https://localhost/logout" - }, - "scopes": [] - } - } - ] - } - ] -} diff --git a/internal/tls.go b/internal/tls.go deleted file mode 100644 index dd39d8b..0000000 --- a/internal/tls.go +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "bytes" - "context" - "crypto/tls" - "crypto/x509" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "hash/fnv" - "sync" - - "github.com/tetratelabs/telemetry" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/structpb" -) - -type ( - // TLSConfig is an interface for the TLS configuration of the AuthService. - TLSConfig interface { - // GetTrustedCertificateAuthority returns the trusted certificate authority PEM. - GetTrustedCertificateAuthority() string - // GetTrustedCertificateAuthorityFile returns the path to the trusted certificate authority file. - GetTrustedCertificateAuthorityFile() string - // GetSkipVerifyPeerCert returns whether to skip verification of the peer certificate. - GetSkipVerifyPeerCert() *structpb.Value - // GetTrustedCertificateAuthorityRefreshInterval returns interval at which the trusted certificate - // authority should be refreshed. - GetTrustedCertificateAuthorityRefreshInterval() *durationpb.Duration - } - - // TLSConfigPool is an interface for a pool of TLS configurations. - TLSConfigPool interface { - // LoadTLSConfig loads a TLS configuration from the given TLSConfig. - LoadTLSConfig(config TLSConfig) (*tls.Config, error) - } - - // tlsConfigPool is a pool of TLS configurations. - // That reloads the trusted certificate authority when there are changes. - tlsConfigPool struct { - log telemetry.Logger - - mu sync.RWMutex - configs map[string]*tls.Config - caWatcher *FileWatcher - } -) - -// NewTLSConfigPool creates a new TLSConfigPool. -func NewTLSConfigPool(ctx context.Context) TLSConfigPool { - return &tlsConfigPool{ - log: Logger(Config), - configs: make(map[string]*tls.Config), - caWatcher: NewFileWatcher(ctx), - } -} - -// LoadTLSConfig loads a TLS configuration from the given TLSConfig. -func (p *tlsConfigPool) LoadTLSConfig(config TLSConfig) (*tls.Config, error) { - if config.GetTrustedCertificateAuthority() == "" && - config.GetTrustedCertificateAuthorityFile() == "" && - config.GetSkipVerifyPeerCert() == nil { - // no given TLS config, nothing to load - return nil, nil - } - - encConfig := encodeConfig(config) - id := encConfig.hash() - - p.mu.Lock() - if tlsConfig, ok := p.configs[id]; ok { - p.mu.Unlock() - return tlsConfig, nil - } - p.mu.Unlock() - - log := p.log.With("id", id) - log.Info("loading new TLS config", "config", encConfig.JSON()) - tlsConfig := &tls.Config{} - - // Load the trusted CA PEM from the config - var ca []byte - switch { - case config.GetTrustedCertificateAuthority() != "": - ca = []byte(config.GetTrustedCertificateAuthority()) - - case config.GetTrustedCertificateAuthorityFile() != "": - var err error - ca, err = p.caWatcher.WatchFile( - NewFileReader(config.GetTrustedCertificateAuthorityFile()), - config.GetTrustedCertificateAuthorityRefreshInterval().AsDuration(), - func(data []byte) { p.updateCA(id, data) }, - ) - if err != nil { - return nil, fmt.Errorf("error watching trusted CA file: %w", err) - } - - case config.GetSkipVerifyPeerCert() != nil: - tlsConfig.InsecureSkipVerify = BoolStrValue(config.GetSkipVerifyPeerCert()) - } - - // Add the loaded CA to the TLS config - if len(ca) != 0 { - if BoolStrValue(config.GetSkipVerifyPeerCert()) { - log.Info("`skip_verify_peer_cert` is set to true but there's also a trusted certificate authority, ignoring `skip_verify_peer_cert`") - } - - certPool, err := x509.SystemCertPool() - if err != nil { - return nil, fmt.Errorf("error creating system cert pool: %w", err) - } - - if ok := certPool.AppendCertsFromPEM(ca); !ok { - return nil, errors.New("could no load trusted certificate authority") - } - - tlsConfig.RootCAs = certPool - } - - // Save the TLS config to the pool - p.mu.Lock() - p.configs[id] = tlsConfig - p.mu.Unlock() - return tlsConfig, nil -} - -func (p *tlsConfigPool) updateCA(id string, caPem []byte) { - log := p.log.With("id", id) - - // Load the TLS config - p.mu.Lock() - tlsConfig, ok := p.configs[id] - if !ok { - log.Error("couldn't update TLS config", errors.New("config not found")) - p.mu.Unlock() - return - } - p.mu.Unlock() - - // Add the loaded CA to the TLS config - certPool, err := x509.SystemCertPool() - if err != nil { - log.Error("error creating system cert pool", err) - return - } - - if ok := certPool.AppendCertsFromPEM(caPem); !ok { - log.Error("could not load trusted certificate authority", errors.New("failed to append certificate in the cert pool")) - return - } - - // Update the TLS config - tlsConfig.RootCAs = certPool - log.Info("updated TLS config with new trusted certificate authority") - - p.mu.Lock() - p.configs[id] = tlsConfig - p.mu.Unlock() -} - -// tlsConfigEncoder is the internal representation of a TLSConfig. -// It handles some useful methods for the TLSConfig. -type tlsConfigEncoder struct { - SkipVerifyPeerCert bool `json:"skipVerifyPeerCert,omitempty"` - TrustedCA string `json:"trustedCertificateAuthority,omitempty"` - TrustedCAFile string `json:"trustedCertificateAuthorityFile,omitempty"` - TrustedCARefreshInterval string `json:"trustedCertificateAuthorityRefreshInterval,omitempty"` -} - -// encodeConfig converts a TLSConfig to an tlsConfigEncoder. -func encodeConfig(config TLSConfig) tlsConfigEncoder { - return tlsConfigEncoder{ - TrustedCA: config.GetTrustedCertificateAuthority(), - TrustedCAFile: config.GetTrustedCertificateAuthorityFile(), - TrustedCARefreshInterval: config.GetTrustedCertificateAuthorityRefreshInterval().AsDuration().String(), - SkipVerifyPeerCert: BoolStrValue(config.GetSkipVerifyPeerCert()), - } -} - -// hash returns the hash of the tls config. -func (c tlsConfigEncoder) hash() string { - buff := bytes.Buffer{} - _, _ = buff.WriteString(fmt.Sprintf("%t", c.SkipVerifyPeerCert)) - _, _ = buff.WriteString(c.TrustedCA) - _, _ = buff.WriteString(c.TrustedCAFile) - _, _ = buff.WriteString(c.TrustedCARefreshInterval) - hash := fnv.New64a() - _, _ = hash.Write(buff.Bytes()) - out := hash.Sum(make([]byte, 0, 15)) - return hex.EncodeToString(out) -} - -// JSON returns the JSON representation of the tls config. -func (c tlsConfigEncoder) JSON() string { - jsonBytes, _ := json.Marshal(c) - return string(jsonBytes) -} diff --git a/internal/tls_test.go b/internal/tls_test.go deleted file mode 100644 index c366ab5..0000000 --- a/internal/tls_test.go +++ /dev/null @@ -1,353 +0,0 @@ -// Copyright 2024 Tetrate -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package internal - -import ( - "context" - "crypto/x509" - "encoding/pem" - "os" - "testing" - "time" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/structpb" - - "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc" -) - -const ( - invalidCAPem = `` - firstCertDNSName = "first" - secondCertDNSName = "second" - - firstCAPem = `-----BEGIN CERTIFICATE----- -MIICMjCCAdygAwIBAgIUfjMuIL07OwG1Q13HGhaDJbdKgRYwDQYJKoZIhvcNAQEL -BQAwWjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEDAOBgNVBAoM -B1RldHJhdGUxFDASBgNVBAsMC0VuZ2luZWVyaW5nMQ4wDAYDVQQDDAVmaXJzdDAg -Fw0yNDAyMjUwNzU5MjhaGA8zMDA0MDQyODA3NTkyOFowWjELMAkGA1UEBhMCVVMx -EzARBgNVBAgMCkNhbGlmb3JuaWExEDAOBgNVBAoMB1RldHJhdGUxFDASBgNVBAsM -C0VuZ2luZWVyaW5nMQ4wDAYDVQQDDAVmaXJzdDBcMA0GCSqGSIb3DQEBAQUAA0sA -MEgCQQDFT3pCjZyxnQ5o46GlBd7e6yredUuGdYhaLPjkcDZw5LTdy/WdJ8MRsUdJ -uh0v5HSpDsd6yIiP8SF20WgfbYpfAgMBAAGjeDB2MB0GA1UdDgQWBBQUQOM/blzh -GpovGudMO43BZSKjTjAfBgNVHSMEGDAWgBQUQOM/blzhGpovGudMO43BZSKjTjAS -BgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIC5DAQBgNVHREECTAHggVm -aXJzdDANBgkqhkiG9w0BAQsFAANBAG0Gwgwaxe+OnpFOdDi0QFILN10EFl0BsNjz -JROKsQSnX5sGlYdVcb0TBAf8MojqNZvq78C1fCXkDus3g3AZyLM= ------END CERTIFICATE-----` - - firstCertPem = `-----BEGIN CERTIFICATE----- -MIICDjCCAbigAwIBAgIUQPCzOs6M9RxopgX0HL8uJKDoBpowDQYJKoZIhvcNAQEL -BQAwWjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEDAOBgNVBAoM -B1RldHJhdGUxFDASBgNVBAsMC0VuZ2luZWVyaW5nMQ4wDAYDVQQDDAVmaXJzdDAg -Fw0yNDAyMjUwNzU5MjhaGA8zMDA0MDQyODA3NTkyOFowWjELMAkGA1UEBhMCVVMx -EzARBgNVBAgMCkNhbGlmb3JuaWExEDAOBgNVBAoMB1RldHJhdGUxFDASBgNVBAsM -C0VuZ2luZWVyaW5nMQ4wDAYDVQQDDAVmaXJzdDBcMA0GCSqGSIb3DQEBAQUAA0sA -MEgCQQCm/7/RmxYPmRmtoWmD4U6Gv9x96ApW3Wf2yvl5o7J4StvgRSreTBjQO59N -ERwfhAcNV+SRZWIXtodmhryCcbNzAgMBAAGjVDBSMBAGA1UdEQQJMAeCBWZpcnN0 -MB0GA1UdDgQWBBRczCCJnGGmj/mK8ncpBM4cYX4hoDAfBgNVHSMEGDAWgBQUQOM/ -blzhGpovGudMO43BZSKjTjANBgkqhkiG9w0BAQsFAANBAEynBzYcUtn1LgUnbXnq -UCQC5/a5NavSwD+uujen++9luWxZP5BDLIuqWVEkVeavaRD8WTNi6pB/4Kok3/h7 -mrU= ------END CERTIFICATE-----` - - secondCAPem = `-----BEGIN CERTIFICATE----- -MIICNTCCAd+gAwIBAgIUSNiQbnskpJz9qXIy3ZvyeBr5DgswDQYJKoZIhvcNAQEL -BQAwWzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEDAOBgNVBAoM -B1RldHJhdGUxFDASBgNVBAsMC0VuZ2luZWVyaW5nMQ8wDQYDVQQDDAZzZWNvbmQw -IBcNMjQwMjI1MDc1ODA4WhgPMzAwNDA0MjgwNzU4MDhaMFsxCzAJBgNVBAYTAlVT -MRMwEQYDVQQIDApDYWxpZm9ybmlhMRAwDgYDVQQKDAdUZXRyYXRlMRQwEgYDVQQL -DAtFbmdpbmVlcmluZzEPMA0GA1UEAwwGc2Vjb25kMFwwDQYJKoZIhvcNAQEBBQAD -SwAwSAJBAMz0bgSTGkUT4BevyrQUBI11ISf4sORB4iIOBeZxF9T3+k7fOqCieok7 -KquH6X7gsmL/A15qU0XCsVZWZ9ro9/UCAwEAAaN5MHcwHQYDVR0OBBYEFMO6IIAi -fGtfQdEJpC+IrTQqcbfoMB8GA1UdIwQYMBaAFMO6IIAifGtfQdEJpC+IrTQqcbfo -MBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgLkMBEGA1UdEQQKMAiC -BnNlY29uZDANBgkqhkiG9w0BAQsFAANBADGCBvNWs7L+uMYHvfk5Uy+P6eoIJKok -LeXeAdsKK+0F9xCmnNfuinTJ1ioZ47e7fFS2XGfO8qSmmb0wVnK/9Ig= ------END CERTIFICATE-----` - - secondCertPem = `-----BEGIN CERTIFICATE----- -MIICETCCAbugAwIBAgIUXHZGY3lT62cY3Y/ccIoRAp7P5mkwDQYJKoZIhvcNAQEL -BQAwWzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEDAOBgNVBAoM -B1RldHJhdGUxFDASBgNVBAsMC0VuZ2luZWVyaW5nMQ8wDQYDVQQDDAZzZWNvbmQw -IBcNMjQwMjI1MDc1ODA4WhgPMzAwNDA0MjgwNzU4MDhaMFsxCzAJBgNVBAYTAlVT -MRMwEQYDVQQIDApDYWxpZm9ybmlhMRAwDgYDVQQKDAdUZXRyYXRlMRQwEgYDVQQL -DAtFbmdpbmVlcmluZzEPMA0GA1UEAwwGc2Vjb25kMFwwDQYJKoZIhvcNAQEBBQAD -SwAwSAJBAOgoDZ6wH/7lbqGphAOlJqRJcWeaN4jB8BEc/MejG1UL75uFnwXDmwDH -KNU1e3VygpWyFwrrBKde4DEMBKnBdPsCAwEAAaNVMFMwEQYDVR0RBAowCIIGc2Vj -b25kMB0GA1UdDgQWBBTxli7ulcoMhJPUGTNS0qclcCd41DAfBgNVHSMEGDAWgBTD -uiCAInxrX0HRCaQviK00KnG36DANBgkqhkiG9w0BAQsFAANBAIdc1uTnNDMdROp4 -fIGuGu2HAHkqnBhOHh71Xd/WD/9kjPGUQNzRZUYaWs9EGz95VvcrSIPPMU8tLhIt -dabJiLY= ------END CERTIFICATE-----` -) - -func TestLoadTLSConfig(t *testing.T) { - tmpDir := t.TempDir() - var ( - validFile = tmpDir + "/valid.pem" - invalidFile = tmpDir + "/invalid.pem" - ) - require.NoError(t, os.WriteFile(validFile, []byte(firstCAPem), 0644)) - require.NoError(t, os.WriteFile(invalidFile, []byte(invalidCAPem), 0644)) - - tests := []struct { - name string - config TLSConfig - wantTLS bool - wantSkip bool - wantPool bool - wantErr bool - }{ - { - name: "no CA config", - config: &oidc.OIDCConfig{}, - wantTLS: false, - }, - { - name: "skip verify config", - config: &oidc.OIDCConfig{SkipVerifyPeerCert: structpb.NewBoolValue(true)}, - wantTLS: true, - wantSkip: true, - }, - { - name: "valid trusted CA string config", - config: &oidc.OIDCConfig{TrustedCaConfig: &oidc.OIDCConfig_TrustedCertificateAuthority{TrustedCertificateAuthority: firstCAPem}}, - wantTLS: true, - wantPool: true, - }, - { - name: "invalid trusted CA string config", - config: &oidc.OIDCConfig{TrustedCaConfig: &oidc.OIDCConfig_TrustedCertificateAuthority{TrustedCertificateAuthority: invalidCAPem}}, - wantErr: true, - }, - { - name: "valid trusted CA file config", - config: &oidc.OIDCConfig{TrustedCaConfig: &oidc.OIDCConfig_TrustedCertificateAuthorityFile{TrustedCertificateAuthorityFile: validFile}}, - wantTLS: true, - wantPool: true, - }, - { - name: "invalid trusted CA file config", - config: &oidc.OIDCConfig{TrustedCaConfig: &oidc.OIDCConfig_TrustedCertificateAuthorityFile{TrustedCertificateAuthorityFile: invalidFile}}, - wantErr: true, - }, - { - name: "no existing file trusted CA file config", - config: &oidc.OIDCConfig{TrustedCaConfig: &oidc.OIDCConfig_TrustedCertificateAuthorityFile{TrustedCertificateAuthorityFile: "non-existing.pem"}}, - wantErr: true, - }, - { - name: "valid trusted CA file and skip verify config", - config: &oidc.OIDCConfig{ - TrustedCaConfig: &oidc.OIDCConfig_TrustedCertificateAuthorityFile{TrustedCertificateAuthorityFile: validFile}, - SkipVerifyPeerCert: structpb.NewBoolValue(true), - }, - wantTLS: true, - wantSkip: false, // skip verify is ignored because there's a trusted CA - wantPool: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - pool := NewTLSConfigPool(ctx) - t.Cleanup(cancel) - - got, err := pool.LoadTLSConfig(tc.config) - - // Check for errors - if tc.wantErr { - require.Error(t, err) - require.Nil(t, got) - return - } - require.NoError(t, err) - - // Check for expected TLS config - if !tc.wantTLS { - require.Nil(t, got) - return - } - require.NotNil(t, got) - require.Equal(t, tc.wantSkip, got.InsecureSkipVerify) - require.Equal(t, tc.wantPool, got.RootCAs != nil) - }) - } -} - -func TestTLSConfigPoolUpdates(t *testing.T) { - tmpDir := t.TempDir() - var caFile1 = tmpDir + "/ca1.pem" - require.NoError(t, os.WriteFile(caFile1, []byte(firstCAPem), 0644)) - - block, _ := pem.Decode([]byte(firstCertPem)) - cert1, err := x509.ParseCertificate(block.Bytes) - require.NoError(t, err) - - block, _ = pem.Decode([]byte(secondCertPem)) - cert2, err := x509.ParseCertificate(block.Bytes) - require.NoError(t, err) - - ctx, cancel := context.WithCancel(context.Background()) - pool := NewTLSConfigPool(ctx) - t.Cleanup(cancel) - - const ( - interval = 100 * time.Millisecond - intervalAndHalf = interval + interval/2 - ) - - config := &oidc.OIDCConfig{ - TrustedCaConfig: &oidc.OIDCConfig_TrustedCertificateAuthorityFile{TrustedCertificateAuthorityFile: caFile1}, - TrustedCertificateAuthorityRefreshInterval: durationpb.New(interval), - } - - // load the TLS config - gotTLS, err := pool.LoadTLSConfig(config) - require.NoError(t, err) - require.NotNil(t, gotTLS) - - // verify the got TLS config is valid - _, err = cert1.Verify(x509.VerifyOptions{Roots: gotTLS.RootCAs, DNSName: firstCertDNSName}) - require.NoError(t, err) - - // update the CA file content - require.NoError(t, os.WriteFile(caFile1, []byte(secondCAPem), 0644)) - time.Sleep(intervalAndHalf) - - // load the TLS config again - gotTLS, err = pool.LoadTLSConfig(config) - require.NoError(t, err) - - // verify the got TLS config is not valid anymore for the old CA, - // as we updated it with CA only valid for cert2. - _, err = cert1.Verify(x509.VerifyOptions{Roots: gotTLS.RootCAs, DNSName: firstCertDNSName}) - require.Error(t, err) - - // verify the got TLS config is valid for the new CA - _, err = cert2.Verify(x509.VerifyOptions{Roots: gotTLS.RootCAs, DNSName: secondCertDNSName}) - require.NoError(t, err) - - // update the CA file content to be invalid - require.NoError(t, os.WriteFile(caFile1, []byte(invalidCAPem), 0644)) - time.Sleep(intervalAndHalf) - - // load the TLS config again - gotTLS, err = pool.LoadTLSConfig(config) - require.NoError(t, err) - - // verify the config is not updated, so the old TLS config is still valid - _, err = cert2.Verify(x509.VerifyOptions{Roots: gotTLS.RootCAs, DNSName: secondCertDNSName}) - require.NoError(t, err) - - // remove the CA file - require.NoError(t, os.Remove(caFile1)) - time.Sleep(intervalAndHalf) - - // load the TLS config again - gotTLS, err = pool.LoadTLSConfig(config) - require.NoError(t, err) - - // verify the config is not modified, so the old TLS config is still valid - _, err = cert2.Verify(x509.VerifyOptions{Roots: gotTLS.RootCAs, DNSName: secondCertDNSName}) - require.NoError(t, err) - - // update the CA file content to be valid again and verify the new CA is loaded - require.NoError(t, os.WriteFile(caFile1, []byte(firstCAPem), 0644)) - time.Sleep(intervalAndHalf) - - // load the TLS config again - gotTLS, err = pool.LoadTLSConfig(config) - require.NoError(t, err) - - _, err = cert1.Verify(x509.VerifyOptions{Roots: gotTLS.RootCAs, DNSName: firstCertDNSName}) - require.NoError(t, err) -} - -func TestTLSConfigPoolWithMultipleConfigs(t *testing.T) { - tmpDir := t.TempDir() - var ( - caFile1 = tmpDir + "/ca1.pem" - caFile2 = tmpDir + "/ca2.pem" - ) - require.NoError(t, os.WriteFile(caFile1, []byte(firstCAPem), 0644)) - require.NoError(t, os.WriteFile(caFile2, []byte(secondCAPem), 0644)) - - block, _ := pem.Decode([]byte(firstCertPem)) - cert1, err := x509.ParseCertificate(block.Bytes) - require.NoError(t, err) - - block, _ = pem.Decode([]byte(secondCertPem)) - cert2, err := x509.ParseCertificate(block.Bytes) - require.NoError(t, err) - - ctx, cancel := context.WithCancel(context.Background()) - pool := NewTLSConfigPool(ctx) - t.Cleanup(cancel) - - const ( - config1Interval = 100 * time.Millisecond - config2Interval = 200 * time.Millisecond - ) - var intervalAndHalf = func(interval time.Duration) time.Duration { return interval + interval/2 } - - config1 := &oidc.OIDCConfig{ - TrustedCaConfig: &oidc.OIDCConfig_TrustedCertificateAuthorityFile{TrustedCertificateAuthorityFile: caFile1}, - TrustedCertificateAuthorityRefreshInterval: durationpb.New(config1Interval), - } - config2 := &oidc.OIDCConfig{ - TrustedCaConfig: &oidc.OIDCConfig_TrustedCertificateAuthorityFile{TrustedCertificateAuthorityFile: caFile2}, - TrustedCertificateAuthorityRefreshInterval: durationpb.New(config2Interval), - } - - // load the TLS config for config1 - gotTLS1, err := pool.LoadTLSConfig(config1) - require.NoError(t, err) - - // load the TLS config for config2 - gotTLS2, err := pool.LoadTLSConfig(config2) - require.NoError(t, err) - - // verify the got TLS config for config1 is valid - _, err = cert1.Verify(x509.VerifyOptions{Roots: gotTLS1.RootCAs, DNSName: firstCertDNSName}) - require.NoError(t, err) - - // verify the got TLS config for config2 is valid - _, err = cert2.Verify(x509.VerifyOptions{Roots: gotTLS2.RootCAs, DNSName: secondCertDNSName}) - require.NoError(t, err) - - // update the second file to contain the first CA - require.NoError(t, os.WriteFile(caFile2, []byte(firstCAPem), 0644)) - time.Sleep(intervalAndHalf(config2Interval)) - - // load the TLS config for config2 again - gotTLS2, err = pool.LoadTLSConfig(config2) - require.NoError(t, err) - - // verify the got TLS config for config2 is valid for the first CA and not for the second - _, err = cert1.Verify(x509.VerifyOptions{Roots: gotTLS2.RootCAs, DNSName: firstCertDNSName}) - require.NoError(t, err) - _, err = cert2.Verify(x509.VerifyOptions{Roots: gotTLS2.RootCAs, DNSName: secondCertDNSName}) - require.Error(t, err) - - // verify the got TLS config for config1 is still valid - gotTLS1, err = pool.LoadTLSConfig(config1) - require.NoError(t, err) - _, err = cert1.Verify(x509.VerifyOptions{Roots: gotTLS1.RootCAs, DNSName: firstCertDNSName}) - require.NoError(t, err) -} diff --git a/run-in-docker.sh b/run-in-docker.sh deleted file mode 100755 index dd781c2..0000000 --- a/run-in-docker.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2024 Tetrate -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This script runs the given command in a Docker container. - -if [[ ${#} -lt 2 ]]; then - echo "Usage: ${0} " - echo " e.g: ${0} linux/amd64 make help" - exit 1 -fi - -set -e - -ROOT=$(git rev-parse --show-toplevel) -GO_VERSION=$(sed -ne 's/^go //gp' "${ROOT}/go.mod") -BUILD_IMAGE="golang:${GO_VERSION}" - -docker run \ - --rm \ - --platform "${1}" \ - -v "${PWD}":/source \ - -v "$(go env GOMODCACHE)":/go/pkg/mod \ - -e GOPROXY="$(go env GOPROXY)" \ - -e GOPRIVATE="$(go env GOPRIVATE)" \ - -w /source \ - "${BUILD_IMAGE}" \ - /bin/bash -c "git config --global --add safe.directory /source ; ${*:2}"