diff --git a/.github/workflows/build-docker.yaml b/.github/workflows/build-docker.yaml index 59efb802..8dbb517b 100644 --- a/.github/workflows/build-docker.yaml +++ b/.github/workflows/build-docker.yaml @@ -41,6 +41,9 @@ jobs: - name: dataplane context: launchers/dataplane dockerfile: launchers/dataplane/src/main/docker/Dockerfile + - name: clearglass + context: clearglass + dockerfile: clearglass/Dockerfile steps: - name: Checkout code diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 6de778af..f780d364 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -48,8 +48,9 @@ jobs: uses: azure/setup-helm@v5 - name: "Build runtime images" - run: | + run: |- ./gradlew dockerize + docker buildx build -f clearglass/Dockerfile -t ghcr.io/metaform/jad/clearglass:latest clearglass - name: "Create k8s Kind Cluster" uses: helm/kind-action@v1.14.0 @@ -57,11 +58,12 @@ jobs: cluster_name: jad - name: "Load runtime images into KinD" - run: | + run: |- kind load docker-image -n jad ghcr.io/metaform/jad/controlplane:latest \ ghcr.io/metaform/jad/dataplane:latest \ ghcr.io/metaform/jad/identity-hub:latest \ - ghcr.io/metaform/jad/issuerservice:latest + ghcr.io/metaform/jad/issuerservice:latest \ + ghcr.io/metaform/jad/clearglass:latest - name: "Install Traefik Gateway controller" @@ -94,6 +96,7 @@ jobs: --selector=type=edcv-infra \ --timeout=300s + - name: "Deploy JAD applications" run: |- # this is crucial - without it, KinD would take the prebuilt images from GHCR diff --git a/.gitignore b/.gitignore index cd8c54b2..7858ff14 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ out/ .idea/ .terraform* -*terraform.tfstate* \ No newline at end of file +*terraform.tfstate* +**/target +/authproxy/target/ diff --git a/README.md b/README.md index 4ecf5383..b68b0788 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,23 @@ and now want to see it in action, please follow the following steps to build and For this, both the EDC-V and CFM docker images must be built locally!! +#### 2.3 Build and deploy clearglass + +Clearglass is a small Rust application that acts as a reverse proxy for the JAD services and is described in more +detail in a [later chapter](#jads-apis--a-single-pane-of-glass). It is being deployed as part of the base (or +infrastructure) +layer. + +For now, we have to build and load it manually using the following commands: + +```shell +docker buildx build -f clearglass/Dockerfile -t ghcr.io/metaform/jad/clearglass:latest clearglass +kind load docker-image -n jad ghcr.io/metaform/jad/clearglass:latest +``` + +_Note that in a later evolution of JAD clearglass will be moved into its own repository which will make this step +obsolete._ + ### 3. Deploy the services JAD uses plain Kubernetes manifests to deploy the services. All the manifests are located in the [k8s](./k8s) folder. @@ -399,6 +416,76 @@ For example, if a participant onboarding went only through half-way, we recommen In some cases, even deleting and re-creating the KinD cluster may be required. +## JAD's APIs – A single pane of glass + +All JAD services are exposed through a single Traefik gateway (`edcv-gateway`) on `jad.localhost`, acting as a single +pane of glass. Each service is reachable via a path prefix that is rewritten before forwarding to the backend. + +Authentication is enforced at the gateway level using Traefik `ForwardAuth` middlewares. Each middleware forwards the +`Authorization` header to the `clearglass` service, which validates the Bearer token against Keycloak via RFC 7662 +token introspection and checks for the required OAuth2 scopes. Services without a `middleware` entry listed are +unauthenticated at the gateway level. + +### Application routes (`jad.localhost`) + +| Service | Exposed path | Rewrites to | Backend port | Auth middleware | +|---------------------|---------------------|-------------------------|--------------|----------------------------------------| +| Control Plane | `/api/management` | `/api/mgmt` | `8081` | `jwt-auth-management-api` | +| Identity Hub | `/api/identity` | `/api/identity/v1alpha` | `7081` | `jwt-auth-identity-api` | +| Issuer Service | `/api/issuer/admin` | `/api/admin/v1alpha` | `10013` | `jwt-auth-issuer-admin-api` | +| Provision Manager | `/api/pm` | `/api/v1alpha` | `8080` | `jwt-auth-provision-manager-api` | +| Tenant Manager | `/api/tm` | `/api/v1alpha1` | `8080` | `jwt-auth-tenant-manager-api` | +| Dataplane (public) | `/api/dp/public` | `/` | `11002` | — | +| Dataplane (control) | `/api/dp/control` | `/` | `8083` | — | +| Dataplane (certs) | `/api/dp/certs` | `/` | `8186` | — | +| Siglet | `/api/siglet` | `/` | `8080` | — | +| Redline | `/redline` | `/` | `8081` | — | +| Keycloak | `/auth` | `/` | `8080` | — (is the auth server) | +| Web UI | `/ui` | `/` | `80` | — (obtains its own token via Keycloak) | + +### Auth middleware scopes + +Each `jwt-auth-*` middleware enforces a specific pair of OAuth2 scopes (`read` and `write`): + +| Middleware | Required scopes | +|----------------------------------|-------------------------------------------------------------| +| `jwt-auth-management-api` | `management-api:read`, `management-api:write` | +| `jwt-auth-identity-api` | `identity-api:read`, `identity-api:write` | +| `jwt-auth-issuer-admin-api` | `issuer-admin-api:read`, `issuer-admin-api:write` | +| `jwt-auth-provision-manager-api` | `provision-manager-api:read`, `provision-manager-api:write` | +| `jwt-auth-tenant-manager-api` | `tenant-manager-api:read`, `tenant-manager-api:write` | + +### Infrastructure routes (each on their own hostname) + +Infrastructure services are not protected by the auth middleware and are only intended for local development access. + +| Service | Hostname | Remark | +|------------|------------------------|----------------------------------------------------------------| +| Grafana | `grafana.localhost` | | +| Prometheus | `prometheus.localhost` | | +| Jaeger | `jaeger.localhost` | | +| Loki | `loki.localhost` | | +| Vault | `vault.localhost` | access from outside the cluster is only intended for e2e tests | + +### Clearglass + +`clearglass` is a small sidecar service (`ghcr.io/metaform/jad/clearglass`) that acts as the authentication and +authorization enforcement point for all protected APIs. Traefik's `ForwardAuth` mechanism intercepts every inbound +request and calls `clearglass`'s `/validate` endpoint before forwarding it to the backend. + +The proxy performs two checks: + +1. **Token validation** — it calls Keycloak's RFC 7662 token introspection endpoint + (`/realms/edcv/protocol/openid-connect/token/introspect`) using its own client credentials (`clearglass` / + `clearglass-secret`) to verify that the Bearer token in the `Authorization` header is active. +2. **Scope check** — the required OAuth2 scopes are passed as `?scope=` query parameters by each Traefik middleware. + The proxy checks that the token carries at least those scopes. If either check fails, the request is rejected with + `401 Unauthorized` before it ever reaches the backend service. + +This design keeps authentication logic out of the individual services and centralizes it in one place, making it easy +to add or modify access rules by updating the middleware definitions in +[`k8s/base/jwt-middleware.yaml`](k8s/base/jwt-middleware.yaml). + ## Deploying JAD on a bare-metal/cloud-hosted Kubernetes KinD is geared towards local development and testing. For example, it comes with a bunch of useful defaults, such as diff --git a/clearglass/Cargo.lock b/clearglass/Cargo.lock new file mode 100644 index 00000000..db2e4127 --- /dev/null +++ b/clearglass/Cargo.lock @@ -0,0 +1,2443 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clearglass" +version = "0.1.0" +dependencies = [ + "async-trait", + "http", + "pingora-core", + "reqwest", + "serde", + "tokio", + "tracing", + "tracing-subscriber", + "wiremock", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "daemonize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" +dependencies = [ + "libc", +] + +[[package]] +name = "daggy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70def8d72740e44d9f676d8dab2c933a236663d86dd24319b57a2bed4d694774" +dependencies = [ + "petgraph", +] + +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "libz-ng-sys", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libz-ng-sys" +version = "1.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be734b33b7bc6a42d92d23e25e69758f866cf564a88d0bf80866fcf5a52c2255" +dependencies = [ + "cmake", + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pingora-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08973c4853cef4c682f7a592907e81a32dcad69476c4846e5de079f16448b177" +dependencies = [ + "ahash", + "async-trait", + "brotli", + "bstr", + "bytes", + "chrono", + "clap", + "daemonize", + "daggy", + "derivative", + "flate2", + "futures", + "h2", + "http", + "httparse", + "httpdate", + "libc", + "log", + "nix", + "once_cell", + "openssl-probe", + "parking_lot", + "percent-encoding", + "pingora-error", + "pingora-http", + "pingora-pool", + "pingora-runtime", + "pingora-timeout", + "prometheus", + "rand 0.8.6", + "regex", + "serde", + "serde_yaml", + "sfv", + "socket2", + "strum", + "strum_macros", + "tokio", + "tokio-stream", + "tokio-test", + "unicase", + "windows-sys 0.59.0", + "zstd", +] + +[[package]] +name = "pingora-error" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9fa97a500e7e5c27a7b8609b9294c8922c9656322285268bfad9520f12feb38" + +[[package]] +name = "pingora-http" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb52d4651b687fab6abf669539cfd97b7cd94b301fde8f57c63354f9c9cc5e2" +dependencies = [ + "bytes", + "http", + "pingora-error", +] + +[[package]] +name = "pingora-pool" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67f034be36772f318370d058913db43dbd22c3763ad974c995ba2e4afb2bb52a" +dependencies = [ + "crossbeam-queue", + "log", + "lru", + "parking_lot", + "pingora-timeout", + "thread_local", + "tokio", +] + +[[package]] +name = "pingora-runtime" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e371315b1c44c2e5a8788fdc61577527b785e121e6ff49144755f40d86511430" +dependencies = [ + "once_cell", + "rand 0.8.6", + "thread_local", + "tokio", +] + +[[package]] +name = "pingora-timeout" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a853fee5ce510a7f5db2561f99c752724112ed13fc3820e70d462d278d704ea" +dependencies = [ + "once_cell", + "parking_lot", + "pin-project-lite", + "thread_local", + "tokio", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust_decimal" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" +dependencies = [ + "arrayvec", + "num-traits", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sfv" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa1f336066b758b7c9df34ed049c0e693a426afe2b27ff7d5b14f410ab1a132" +dependencies = [ + "base64", + "indexmap", + "rust_decimal", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/clearglass/Cargo.toml b/clearglass/Cargo.toml new file mode 100644 index 00000000..cf3cfe8f --- /dev/null +++ b/clearglass/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "clearglass" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "clearglass" +path = "src/main.rs" + +[dependencies] +pingora-core = "0.8" +async-trait = "0.1" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-webpki-roots"] } +serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["full"] } +http = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[dev-dependencies] +wiremock = "0.6" diff --git a/clearglass/Dockerfile b/clearglass/Dockerfile new file mode 100644 index 00000000..66509546 --- /dev/null +++ b/clearglass/Dockerfile @@ -0,0 +1,24 @@ +# --- Chef base --- +FROM rust:bookworm AS chef +RUN cargo install cargo-chef +RUN apt-get update && apt-get install -y cmake && rm -rf /var/lib/apt/lists/* +WORKDIR /build + +# --- Planner: analyze dependencies --- +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +# --- Builder: build deps (cached) then app --- +FROM chef AS builder +COPY --from=planner /build/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json +COPY . . +RUN cargo build --release --bin clearglass + +# --- Runtime: minimal image --- +FROM gcr.io/distroless/cc-debian12 +COPY --from=builder /build/target/release/clearglass / +ENV RUST_LOG=info +EXPOSE 8080 +CMD ["/clearglass"] diff --git a/clearglass/src/main.rs b/clearglass/src/main.rs new file mode 100644 index 00000000..e4a0ac73 --- /dev/null +++ b/clearglass/src/main.rs @@ -0,0 +1,366 @@ +// Copyright (c) 2026 Metaform Systems, Inc. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License, Version 2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: Apache-2.0 +// +// Contributors: +// Metaform Systems, Inc. - initial API and implementation + +// clearglass is a lightweight Traefik ForwardAuth target that validates +// Bearer tokens via Keycloak's token introspection endpoint (RFC 7662) +// and enforces per-route scope requirements. +// +// Traefik calls: GET /validate?scope=&scope= +// - 200 → token is active and has at least one of the listed scopes +// - 401 → missing/inactive token +// - 403 → token is valid but lacks the required scopes + +use async_trait::async_trait; +use http::{Response, StatusCode}; +use pingora_core::apps::http_app::{HttpServer, ServeHttp}; +use pingora_core::protocols::http::ServerSession; +use pingora_core::server::Server; +use pingora_core::services::listening::Service; +use reqwest::Client; +use serde::Deserialize; +use std::collections::HashSet; +use std::env; +use std::time::Duration; +use tracing::{debug, error, info, warn}; +use tracing_subscriber::EnvFilter; + +struct ClearGlassProxy { + introspect_url: String, + client_id: String, + client_secret: String, + http_client: Client, +} + +fn main() { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string()); + let addr = format!("0.0.0.0:{port}"); + info!("clearglass listening on {addr}"); + + let mut server = Server::new(None).expect("failed to create server"); + server.bootstrap(); + + let app = HttpServer::new_app(ClearGlassProxy::from_env()); + let mut service = Service::new("clearglass".to_string(), app); + service.add_tcp(&addr); + server.add_service(service); + + server.run_forever(); +} + +#[derive(Deserialize)] +struct IntrospectResponse { + active: bool, + #[serde(default)] + scope: String, +} + +impl ClearGlassProxy { + fn from_env() -> Self { + ClearGlassProxy { + introspect_url: must_env("TOKEN_INTROSPECTION_URL"), + client_id: must_env("INTROSPECT_CLIENT_ID"), + client_secret: must_env("INTROSPECT_CLIENT_SECRET"), + http_client: Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .expect("failed to build HTTP client"), + } + } + + async fn introspect(&self, token: &str) -> Result { + debug!(url = %self.introspect_url, "calling token introspection endpoint"); + let resp = self + .http_client + .post(&self.introspect_url) + .form(&[ + ("token", token), + ("client_id", self.client_id.as_str()), + ("client_secret", self.client_secret.as_str()), + ]) + .send() + .await? + .json::() + .await?; + debug!(active = resp.active, scopes = %resp.scope, "introspection response received"); + Ok(resp) + } + + /// Handles the validation of a request's bearer token and its associated scopes. + /// The required scopes are specified in the `required_token_scopes` parameter in the format `?scope=value1&scope=value2&...`. + /// The auth header must start with "Bearer" and contain a valid JWT, the token itself must contain all + /// required scopes. + /// In addition, the token must pass the token introspection check. + async fn handle_validate( + &self, + auth_header: Option<&str>, + required_token_scopes: &str, + ) -> Response> { + let token = match auth_header.and_then(|h| h.strip_prefix("Bearer ")) { + Some(t) if !t.is_empty() && t.len() <= 4096 => t, + Some(_) => { + warn!("request rejected: invalid token length"); + return text_response(StatusCode::UNAUTHORIZED, "invalid bearer token"); + } + None => { + warn!("request rejected: no bearer token"); + return text_response(StatusCode::UNAUTHORIZED, "missing bearer token"); + } + }; + + let result = match self.introspect(token).await { + Ok(r) => r, + Err(e) => { + error!(error = %e, "introspection request failed"); + return text_response(StatusCode::INTERNAL_SERVER_ERROR, "internal error"); + } + }; + + if !result.active { + warn!("request rejected: token inactive"); + return text_response(StatusCode::UNAUTHORIZED, "token inactive"); + } + + // ?scope= params are candidates; the token must carry at least one. + let required: Vec<&str> = required_token_scopes + .split('&') + .filter_map(|kv| { + let (k, v) = kv.split_once('=')?; + (k == "scope").then_some(v) + }) + .collect(); + + if !required.is_empty() { + let present: HashSet<&str> = result.scope.split_whitespace().collect(); + if !required.iter().any(|s| present.contains(s)) { + warn!(required = ?required, present = ?present, "request rejected: insufficient scope"); + return text_response(StatusCode::FORBIDDEN, "insufficient scope"); + } + debug!(required = ?required, "scope check passed"); + } + + debug!("request allowed"); + text_response(StatusCode::OK, "ok") + } +} + +#[async_trait] +impl ServeHttp for ClearGlassProxy { + async fn response(&self, http_session: &mut ServerSession) -> Response> { + let path = http_session.req_header().uri.path().to_owned(); + let query = http_session + .req_header() + .uri + .query() + .unwrap_or("") + .to_owned(); + let auth = http_session + .req_header() + .headers + .get(http::header::AUTHORIZATION) + .and_then(|v| v.to_str().ok()) + .map(str::to_owned); + + match path.as_str() { + "/healthz" => text_response(StatusCode::OK, "ok"), + "/validate" => self.handle_validate(auth.as_deref(), &query).await, + _ => { + warn!(path = %path, "request for unknown path"); + text_response(StatusCode::NOT_FOUND, "not found") + } + } + } +} + +fn text_response(status: StatusCode, body: &str) -> Response> { + Response::builder() + .status(status) + .header(http::header::CONTENT_TYPE, "text/plain") + .body(body.as_bytes().to_vec()) + .unwrap() +} + +fn must_env(key: &str) -> String { + env::var(key).unwrap_or_else(|_| { + eprintln!("required environment variable not set: {key}"); + std::process::exit(1); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use wiremock::matchers::{method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + fn make_proxy(url: &str) -> ClearGlassProxy { + ClearGlassProxy { + introspect_url: url.to_string(), + client_id: "test-client".to_string(), + client_secret: "test-secret".to_string(), + http_client: Client::builder() + .timeout(Duration::from_secs(2)) + .build() + .unwrap(), + } + } + + async fn setup_mock(active: bool, scope: &str) -> (MockServer, ClearGlassProxy) { + let server = MockServer::start().await; + let body = format!(r#"{{"active":{},"scope":"{}"}}"#, active, scope); + Mock::given(method("POST")) + .and(path("/introspect")) + .respond_with( + ResponseTemplate::new(200) + .set_body_string(body) + .insert_header("content-type", "application/json"), + ) + .mount(&server) + .await; + let proxy = make_proxy(&format!("{}/introspect", server.uri())); + (server, proxy) + } + + fn response_body(resp: &Response>) -> String { + String::from_utf8(resp.body().clone()).unwrap() + } + + // --- Token extraction --- + + #[tokio::test] + async fn no_auth_header_returns_401() { + let (_, proxy) = setup_mock(true, "").await; + let resp = proxy.handle_validate(None, "").await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + assert_eq!(response_body(&resp), "missing bearer token"); + } + + #[tokio::test] + async fn wrong_auth_scheme_returns_401() { + let (_, proxy) = setup_mock(true, "").await; + let resp = proxy.handle_validate(Some("Basic abc123"), "").await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + assert_eq!(response_body(&resp), "missing bearer token"); + } + + #[tokio::test] + async fn empty_token_returns_401() { + let (_, proxy) = setup_mock(true, "").await; + let resp = proxy.handle_validate(Some("Bearer "), "").await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + assert_eq!(response_body(&resp), "invalid bearer token"); + } + + #[tokio::test] + async fn oversized_token_returns_401() { + let (_, proxy) = setup_mock(true, "").await; + let long_token = format!("Bearer {}", "x".repeat(4097)); + let resp = proxy.handle_validate(Some(&long_token), "").await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + assert_eq!(response_body(&resp), "invalid bearer token"); + } + + #[tokio::test] + async fn max_length_token_is_accepted() { + let (_, proxy) = setup_mock(true, "").await; + let token = format!("Bearer {}", "x".repeat(4096)); + let resp = proxy.handle_validate(Some(&token), "").await; + assert_eq!(resp.status(), StatusCode::OK); + } + + // --- Introspection results --- + + #[tokio::test] + async fn inactive_token_returns_401() { + let (_, proxy) = setup_mock(false, "").await; + let resp = proxy.handle_validate(Some("Bearer valid-token"), "").await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + assert_eq!(response_body(&resp), "token inactive"); + } + + #[tokio::test] + async fn introspection_error_returns_500() { + // Point to a server that doesn't exist + let proxy = make_proxy("http://127.0.0.1:1/introspect"); + let resp = proxy.handle_validate(Some("Bearer some-token"), "").await; + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(response_body(&resp), "internal error"); + } + + // --- Scope checking --- + + #[tokio::test] + async fn active_token_no_scope_required_returns_200() { + let (_, proxy) = setup_mock(true, "read write").await; + let resp = proxy.handle_validate(Some("Bearer tok"), "").await; + assert_eq!(resp.status(), StatusCode::OK); + } + + #[tokio::test] + async fn active_token_matching_scope_returns_200() { + let (_, proxy) = setup_mock(true, "read write").await; + let resp = proxy + .handle_validate(Some("Bearer tok"), "scope=read") + .await; + assert_eq!(resp.status(), StatusCode::OK); + } + + #[tokio::test] + async fn active_token_one_of_multiple_scopes_matches_returns_200() { + let (_, proxy) = setup_mock(true, "write").await; + let resp = proxy + .handle_validate(Some("Bearer tok"), "scope=read&scope=write") + .await; + assert_eq!(resp.status(), StatusCode::OK); + } + + #[tokio::test] + async fn active_token_no_matching_scope_returns_403() { + let (_, proxy) = setup_mock(true, "read").await; + let resp = proxy + .handle_validate(Some("Bearer tok"), "scope=admin") + .await; + assert_eq!(resp.status(), StatusCode::FORBIDDEN); + assert_eq!(response_body(&resp), "insufficient scope"); + } + + #[tokio::test] + async fn empty_scope_value_is_ignored() { + let (_, proxy) = setup_mock(true, "").await; + // scope= with no value: split_once('=') yields ("scope", ""), which is empty + // but it's still added to required. The token has no scopes either. + // With empty string in required and empty scope set, .any() won't match → 403 + let resp = proxy.handle_validate(Some("Bearer tok"), "scope=").await; + assert_eq!(resp.status(), StatusCode::FORBIDDEN); + } + + #[tokio::test] + async fn non_scope_query_params_are_ignored() { + let (_, proxy) = setup_mock(true, "read").await; + let resp = proxy + .handle_validate(Some("Bearer tok"), "foo=bar&scope=read&baz=qux") + .await; + assert_eq!(resp.status(), StatusCode::OK); + } + + // --- text_response helper --- + + #[test] + fn text_response_sets_status_and_body() { + let resp = text_response(StatusCode::FORBIDDEN, "denied"); + assert_eq!(resp.status(), StatusCode::FORBIDDEN); + assert_eq!(response_body(&resp), "denied"); + assert_eq!(resp.headers().get("content-type").unwrap(), "text/plain"); + } +} diff --git a/k8s/apps/controlplane.yaml b/k8s/apps/controlplane.yaml index 5c5fd9fd..8653638d 100644 --- a/k8s/apps/controlplane.yaml +++ b/k8s/apps/controlplane.yaml @@ -11,6 +11,7 @@ # Metaform Systems, Inc. - initial API and implementation # +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -109,12 +110,24 @@ spec: kind: Gateway sectionName: http hostnames: - - cp.localhost + - jad.localhost rules: + # /cp → management API (port 8081, path /api/mgmt) - matches: - path: type: PathPrefix - value: / + value: /api/management + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /api/mgmt + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: jwt-auth-management-api backendRefs: - name: controlplane port: 8081 diff --git a/k8s/apps/dataplane.yaml b/k8s/apps/dataplane.yaml index 071ed93a..ea825d02 100644 --- a/k8s/apps/dataplane.yaml +++ b/k8s/apps/dataplane.yaml @@ -112,13 +112,13 @@ spec: - name: edcv-gateway namespace: edc-v hostnames: - - dp.localhost + - jad.localhost rules: - # /dp/public → public port 11002 + # /api/dp/public → public port 11002 (data transfer API) - matches: - path: type: PathPrefix - value: /public + value: /api/dp/public filters: - type: URLRewrite urlRewrite: @@ -130,11 +130,11 @@ spec: port: 11002 weight: 1 - # /app/internal → control port 8083 + # /api/dp/control → control port 8083 (internal control API) - matches: - path: type: PathPrefix - value: /app/internal + value: /api/dp/control filters: - type: URLRewrite urlRewrite: @@ -146,11 +146,11 @@ spec: port: 8083 weight: 1 - # /app/public → certs port 8186 + # /api/dp/certs → certs port 8186 (data/certificate API) - matches: - path: type: PathPrefix - value: /app/public + value: /api/dp/certs filters: - type: URLRewrite urlRewrite: diff --git a/k8s/apps/identityhub.yaml b/k8s/apps/identityhub.yaml index 6b44e716..3a87df1f 100644 --- a/k8s/apps/identityhub.yaml +++ b/k8s/apps/identityhub.yaml @@ -117,27 +117,24 @@ spec: kind: Gateway sectionName: http hostnames: - - ih.localhost + - jad.localhost rules: + # /ih/cs → identity API port 7081 (/ih/cs/** rewrites to /api/identity/**) - matches: - path: type: PathPrefix - value: /cs + value: /api/identity filters: - type: URLRewrite urlRewrite: path: type: ReplacePrefixMatch - replacePrefixMatch: / + replacePrefixMatch: /api/identity/v1alpha + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: jwt-auth-identity-api backendRefs: - name: identityhub - port: 7081 - - # DID Route - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: identityhub - port: 7083 \ No newline at end of file + port: 7081 \ No newline at end of file diff --git a/k8s/apps/issuerservice.yaml b/k8s/apps/issuerservice.yaml index 868cc4c4..9101a7d2 100644 --- a/k8s/apps/issuerservice.yaml +++ b/k8s/apps/issuerservice.yaml @@ -116,46 +116,25 @@ spec: kind: Gateway sectionName: http hostnames: - - issuer.localhost + - jad.localhost rules: - # Issuer Admin API + # /issuer/admin → issuer admin API port 10013 - matches: - path: type: PathPrefix - value: /admin + value: /api/issuer/admin filters: - type: URLRewrite urlRewrite: path: type: ReplacePrefixMatch - replacePrefixMatch: / + replacePrefixMatch: /api/admin/v1alpha + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: jwt-auth-issuer-admin-api backendRefs: - name: issuerservice port: 10013 - weight: 1 - - # Credential Service API - - matches: - - path: - type: PathPrefix - value: /cs - filters: - - type: URLRewrite - urlRewrite: - path: - type: ReplacePrefixMatch - replacePrefixMatch: / - backendRefs: - - name: issuerservice - port: 10015 - weight: 1 - - # Web DID Resolution endpoint - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: issuerservice - port: 10016 weight: 1 \ No newline at end of file diff --git a/k8s/apps/provision-manager.yaml b/k8s/apps/provision-manager.yaml index af3028da..ac2bf9a7 100644 --- a/k8s/apps/provision-manager.yaml +++ b/k8s/apps/provision-manager.yaml @@ -81,12 +81,23 @@ spec: kind: Gateway sectionName: http hostnames: - - pm.localhost + - jad.localhost rules: - matches: - path: type: PathPrefix - value: / + value: /api/pm + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /api/v1alpha + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: jwt-auth-provision-manager-api backendRefs: - name: provision-manager port: 8080 diff --git a/k8s/apps/redline-config.yaml b/k8s/apps/redline-config.yaml index 9e3636bc..f4a46e0e 100644 --- a/k8s/apps/redline-config.yaml +++ b/k8s/apps/redline-config.yaml @@ -34,4 +34,4 @@ data: IDENTITYHUB_URL: "http://identityhub.edc-v.svc.cluster.local:7081/identities" TENANT-MANAGER_URL: "http://tenant-manager.edc-v.svc.cluster.local:8080" SIGLET_URL: "http://siglet.edc-v.svc.cluster.local:8080" - CORS_ALLOWED_ORIGIN: "http://ui.localhost" \ No newline at end of file + CORS_ALLOWED_ORIGIN: "http://jad.localhost" \ No newline at end of file diff --git a/k8s/apps/redline.yaml b/k8s/apps/redline.yaml index 7b1b8d11..f586d3ff 100644 --- a/k8s/apps/redline.yaml +++ b/k8s/apps/redline.yaml @@ -95,12 +95,18 @@ spec: kind: Gateway sectionName: http hostnames: - - redline.localhost + - jad.localhost rules: - matches: - path: type: PathPrefix - value: / + value: /redline + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / backendRefs: - name: redline port: 8081 diff --git a/k8s/apps/siglet.yaml b/k8s/apps/siglet.yaml index b917a898..71b683ad 100644 --- a/k8s/apps/siglet.yaml +++ b/k8s/apps/siglet.yaml @@ -174,12 +174,23 @@ spec: kind: Gateway sectionName: http hostnames: - - siglet.localhost + - jad.localhost rules: - matches: - path: type: PathPrefix - value: / + value: /api/siglet + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: jwt-auth-siglet-api backendRefs: - name: siglet port: 8080 diff --git a/k8s/apps/tenant-manager.yaml b/k8s/apps/tenant-manager.yaml index de79cca3..b5d6dd94 100644 --- a/k8s/apps/tenant-manager.yaml +++ b/k8s/apps/tenant-manager.yaml @@ -81,12 +81,23 @@ spec: kind: Gateway sectionName: http hostnames: - - tm.localhost + - jad.localhost rules: - matches: - path: type: PathPrefix - value: / + value: /api/tm + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /api/v1alpha1 + - type: ExtensionRef + extensionRef: + group: traefik.io + kind: Middleware + name: jwt-auth-tenant-manager-api backendRefs: - name: tenant-manager port: 8080 diff --git a/k8s/apps/ui-config.yaml b/k8s/apps/ui-config.yaml index 2a48eb82..bf845666 100644 --- a/k8s/apps/ui-config.yaml +++ b/k8s/apps/ui-config.yaml @@ -21,7 +21,7 @@ data: config.json: | { "production": true, - "apiUrl": "http://redline.localhost/api/ui", + "apiUrl": "http://jad.localhost/redline/api/ui", "appName": "Aruba's DataSpace", "version": "1.0.0", "features": { diff --git a/k8s/apps/ui.yaml b/k8s/apps/ui.yaml index cc46f145..ca261958 100644 --- a/k8s/apps/ui.yaml +++ b/k8s/apps/ui.yaml @@ -86,12 +86,19 @@ spec: kind: Gateway sectionName: http hostnames: - - ui.localhost + - jad.localhost rules: + # No JWT auth — the browser-based UI obtains its own token via Keycloak - matches: - path: type: PathPrefix - value: / + value: /ui + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / backendRefs: - name: jad-web-ui port: 80 diff --git a/k8s/base/clearglass.yaml b/k8s/base/clearglass.yaml new file mode 100644 index 00000000..fcc3203b --- /dev/null +++ b/k8s/base/clearglass.yaml @@ -0,0 +1,78 @@ +# +# Copyright (c) 2026 Metaform Systems, Inc. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Metaform Systems, Inc. - initial API and implementation +# + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: clearglass + namespace: edc-v + labels: + app: clearglass + platform: edcv + type: edcv-infra +spec: + replicas: 1 + selector: + matchLabels: + app: clearglass + template: + metadata: + labels: + app: clearglass + platform: edcv + type: edcv-infra + spec: + containers: + - name: clearglass + image: ghcr.io/metaform/jad/clearglass:latest + imagePullPolicy: Never + ports: + - containerPort: 8080 + name: http + env: + - name: TOKEN_INTROSPECTION_URL + value: http://keycloak.edc-v.svc.cluster.local:8080/realms/edcv/protocol/openid-connect/token/introspect + - name: INTROSPECT_CLIENT_ID + value: clearglass + - name: INTROSPECT_CLIENT_SECRET + value: clearglass-secret + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + restartPolicy: Always + +--- +apiVersion: v1 +kind: Service +metadata: + name: clearglass + namespace: edc-v + labels: + tier: infrastructure +spec: + selector: + app: clearglass + ports: + - port: 8080 + targetPort: 8080 + name: http diff --git a/k8s/base/gateway.yaml b/k8s/base/gateway.yaml index 650f44f2..75ee4126 100644 --- a/k8s/base/gateway.yaml +++ b/k8s/base/gateway.yaml @@ -25,7 +25,7 @@ spec: port: 80 allowedRoutes: namespaces: - from: All #or Same or Selector -# kinds: -# - kind: HTTPRoute -# group: gateway.networking.k8s.io \ No newline at end of file + from: Same #or Same or Selector + kinds: + - kind: HTTPRoute + group: gateway.networking.k8s.io \ No newline at end of file diff --git a/k8s/base/jwt-middleware.yaml b/k8s/base/jwt-middleware.yaml new file mode 100644 index 00000000..6d7a92a4 --- /dev/null +++ b/k8s/base/jwt-middleware.yaml @@ -0,0 +1,104 @@ +# +# Copyright (c) 2025 Metaform Systems, Inc. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Metaform Systems, Inc. - initial API and implementation +# + +# Traefik ForwardAuth middlewares that validate Bearer tokens via the clearglass +# service, which calls Keycloak's RFC 7662 token introspection endpoint and +# checks the required scopes. +# +# Each middleware passes ?scope= query params to clearglass; the token must +# carry at least one of the listed scopes for the request to be allowed. + +--- +# requires that all requests targeting the management API be authenticated and have the appropriate scopes +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: jwt-auth-management-api + namespace: edc-v +spec: + forwardAuth: + address: http://clearglass.edc-v.svc.cluster.local:8080/validate?scope=management-api:read&scope=management-api:write + authRequestHeaders: + - Authorization + trustForwardHeader: true + +--- +# requires that all requests targeting the Identity API be authenticated and have the appropriate scopes +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: jwt-auth-identity-api + namespace: edc-v +spec: + forwardAuth: + address: http://clearglass.edc-v.svc.cluster.local:8080/validate?scope=identity-api:read&scope=identity-api:write + authRequestHeaders: + - Authorization + trustForwardHeader: true + +--- +# requires that all requests targeting the IssuerAdmin API be authenticated and have the appropriate scopes +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: jwt-auth-issuer-admin-api + namespace: edc-v +spec: + forwardAuth: + address: http://clearglass.edc-v.svc.cluster.local:8080/validate?scope=issuer-admin-api:read&scope=issuer-admin-api:write + authRequestHeaders: + - Authorization + trustForwardHeader: true + +--- +# requires all requests to the CFM Tenant Manager to be authenticated and have the appropriate scopes +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: jwt-auth-tenant-manager-api + namespace: edc-v +spec: + forwardAuth: + address: http://clearglass.edc-v.svc.cluster.local:8080/validate?scope=tenant-manager-api:read&scope=tenant-manager-api:write + authRequestHeaders: + - Authorization + trustForwardHeader: true + +--- +# requires all requests to the CFM Provision Manager to be authenticated and have the appropriate scopes +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: jwt-auth-provision-manager-api + namespace: edc-v +spec: + forwardAuth: + address: http://clearglass.edc-v.svc.cluster.local:8080/validate?scope=provision-manager-api:read&scope=provision-manager-api:write + authRequestHeaders: + - Authorization + trustForwardHeader: true + + +--- +# requires all requests to the CFM Provision Manager to be authenticated and have the appropriate scopes +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: jwt-auth-siglet-api + namespace: edc-v +spec: + forwardAuth: + address: http://clearglass.edc-v.svc.cluster.local:8080/validate?scope=siglet-api:read + authRequestHeaders: + - Authorization + trustForwardHeader: true diff --git a/k8s/base/keycloak.yaml b/k8s/base/keycloak.yaml index 620f28f7..ad181dae 100644 --- a/k8s/base/keycloak.yaml +++ b/k8s/base/keycloak.yaml @@ -180,6 +180,51 @@ data: "include.in.token.scope": "true", "display.on.consent.screen": "true" } + }, + { + "name": "tenant-manager-api:write", + "description": "write access to the CFM Tenant Manager API", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + }, + { + "name": "tenant-manager-api:read", + "description": "read access to the CFM Tenant Manager API", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + }, + { + "name": "provision-manager-api:write", + "description": "write access to the CFM Provision Manager API", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + }, + { + "name": "provision-manager-api:read", + "description": "read access to the CFM Provision Manager API", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + }, + { + "name": "siglet-api:read", + "description": "read access to the Siglet Token API", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } } ], "defaultOptionalClientScopes": [ @@ -189,7 +234,8 @@ data: "identity-api:read", "identity-api:write", "issuer-admin-api:read", - "issuer-admin-api:write" + "issuer-admin-api:write", + "siglet-api:read" ], "clients": [ { @@ -226,7 +272,12 @@ data: "identity-api:write", "identity-api:read", "management-api:write", - "management-api:read" + "management-api:read", + "provision-manager-api:write", + "provision-manager-api:read", + "tenant-manager-api:write", + "tenant-manager-api:read", + "siglet-api:read" ] }, { @@ -263,8 +314,24 @@ data: "identity-api:write", "identity-api:read", "management-api:write", - "management-api:read" + "management-api:read", + "provision-manager-api:write", + "provision-manager-api:read", + "tenant-manager-api:write", + "tenant-manager-api:read" ] + }, + { + "clientId": "clearglass", + "name": "Clearglass", + "description": "Used by the gateway clearglass to introspect tokens (RFC 7662)", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "serviceAccountsEnabled": false, + "secret": "clearglass-secret", + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false } ], "users": [], @@ -303,12 +370,19 @@ spec: kind: Gateway sectionName: http hostnames: - - keycloak.localhost + - jad.localhost rules: + # /auth → Keycloak — no JWT auth (this IS the auth server) - matches: - path: type: PathPrefix - value: / + value: /auth + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: / backendRefs: - name: keycloak port: 8080 \ No newline at end of file diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml index 564dee7d..0d4a84f0 100644 --- a/k8s/base/kustomization.yaml +++ b/k8s/base/kustomization.yaml @@ -15,6 +15,8 @@ resources: - edcv-namespace.yaml - gateway-class.yaml - gateway.yaml + - jwt-middleware.yaml + - clearglass.yaml - postgres.yaml - nats.yaml - keycloak.yaml diff --git a/k8s/base/nats.yaml b/k8s/base/nats.yaml index ea47c7d8..47b3df54 100644 --- a/k8s/base/nats.yaml +++ b/k8s/base/nats.yaml @@ -100,23 +100,3 @@ spec: - name: monitor port: 8222 targetPort: 8222 - ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: nats - namespace: edc-v -spec: - parentRefs: - - name: edcv-gateway - hostnames: - - nats.localhost - rules: - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: nats - port: 4222 \ No newline at end of file diff --git a/requests/EDC-V Onboarding/CFM - Provision Consumer/Create a new Tenant.bru b/requests/EDC-V Onboarding/CFM - Provision Consumer/Create a new Tenant.bru index e707ad2b..d3befc2e 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Consumer/Create a new Tenant.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Consumer/Create a new Tenant.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{tmBaseUrl}}/api/v1alpha1/tenants + url: {{tmBaseUrl}}/tenants body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/CFM - Provision Consumer/Deploy Participant Profile.bru b/requests/EDC-V Onboarding/CFM - Provision Consumer/Deploy Participant Profile.bru index 99aa8bc5..a1ed5a4d 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Consumer/Deploy Participant Profile.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Consumer/Deploy Participant Profile.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{tmBaseUrl}}/api/v1alpha1/tenants/{{tenant_id}}/participant-profiles + url: {{tmBaseUrl}}/tenants/{{tenant_id}}/participant-profiles body: json auth: inherit } @@ -17,13 +17,22 @@ body:json { "properties": {}, "cellId": "{{cell_id}}", "version": 0, - "vpaProperties": {}, + "vpaProperties": { + "cfm.issuer":{ + "key1": "value1", + "bar": "baz" + } + }, "participantRoles": { "{{dataspace_id}}": [ "manufacturer" ] + }, + "properties": { } } + + } vars:pre-request { diff --git a/requests/EDC-V Onboarding/CFM - Provision Consumer/Get Participant Profile.bru b/requests/EDC-V Onboarding/CFM - Provision Consumer/Get Participant Profile.bru index 5ef2b9b0..37288da8 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Consumer/Get Participant Profile.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Consumer/Get Participant Profile.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{tmBaseUrl}}/api/v1alpha1/tenants/{{tenant_id}}/participant-profiles/{{participant_profile_id}} + url: {{tmBaseUrl}}/tenants/{{tenant_id}}/participant-profiles/{{participant_profile_id}} body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/CFM - Provision Consumer/TM- Get Cells.bru b/requests/EDC-V Onboarding/CFM - Provision Consumer/TM- Get Cells.bru index 74d6614d..167bb2a2 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Consumer/TM- Get Cells.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Consumer/TM- Get Cells.bru @@ -5,9 +5,15 @@ meta { } get { - url: {{tmBaseUrl}}/api/v1alpha1/cells + url: {{tmBaseUrl}}/cells body: json auth: inherit +} + +body:json { + + + } script:post-response { diff --git a/requests/EDC-V Onboarding/CFM - Provision Consumer/TM- Get Dataspace Profiles.bru b/requests/EDC-V Onboarding/CFM - Provision Consumer/TM- Get Dataspace Profiles.bru index d5f5672a..f2c0de99 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Consumer/TM- Get Dataspace Profiles.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Consumer/TM- Get Dataspace Profiles.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{tmBaseUrl}}/api/v1alpha1/dataspace-profiles + url: {{tmBaseUrl}}/dataspace-profiles body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/CFM - Provision Consumer/folder.bru b/requests/EDC-V Onboarding/CFM - Provision Consumer/folder.bru index 9105dc36..160578d2 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Consumer/folder.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Consumer/folder.bru @@ -1,6 +1,6 @@ meta { name: CFM - Provision Consumer - seq: 2 + seq: 1 } auth { diff --git a/requests/EDC-V Onboarding/CFM - Provision Provider/Create a new Tenant.bru b/requests/EDC-V Onboarding/CFM - Provision Provider/Create a new Tenant.bru index e707ad2b..d3befc2e 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Provider/Create a new Tenant.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Provider/Create a new Tenant.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{tmBaseUrl}}/api/v1alpha1/tenants + url: {{tmBaseUrl}}/tenants body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/CFM - Provision Provider/Deploy Participant Profile.bru b/requests/EDC-V Onboarding/CFM - Provision Provider/Deploy Participant Profile.bru index 3d67b7da..cfaf8722 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Provider/Deploy Participant Profile.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Provider/Deploy Participant Profile.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{tmBaseUrl}}/api/v1alpha1/tenants/{{tenant_id}}/participant-profiles + url: {{tmBaseUrl}}/tenants/{{tenant_id}}/participant-profiles body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/CFM - Provision Provider/Get Participant Profile.bru b/requests/EDC-V Onboarding/CFM - Provision Provider/Get Participant Profile.bru index 2b088d07..4637912c 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Provider/Get Participant Profile.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Provider/Get Participant Profile.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{tmBaseUrl}}/api/v1alpha1/tenants/{{tenant_id}}/participant-profiles/{{participant_profile_id}} + url: {{tmBaseUrl}}/tenants/{{tenant_id}}/participant-profiles/{{participant_profile_id}} body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/CFM - Provision Provider/TM- Get Cells.bru b/requests/EDC-V Onboarding/CFM - Provision Provider/TM- Get Cells.bru index 74d6614d..a6163d9b 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Provider/TM- Get Cells.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Provider/TM- Get Cells.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{tmBaseUrl}}/api/v1alpha1/cells + url: {{tmBaseUrl}}/cells body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/CFM - Provision Provider/TM- Get Dataspace Profiles.bru b/requests/EDC-V Onboarding/CFM - Provision Provider/TM- Get Dataspace Profiles.bru index d5f5672a..f2c0de99 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Provider/TM- Get Dataspace Profiles.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Provider/TM- Get Dataspace Profiles.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{tmBaseUrl}}/api/v1alpha1/dataspace-profiles + url: {{tmBaseUrl}}/dataspace-profiles body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/CFM - Provision Provider/folder.bru b/requests/EDC-V Onboarding/CFM - Provision Provider/folder.bru index f7574178..1afb1179 100644 --- a/requests/EDC-V Onboarding/CFM - Provision Provider/folder.bru +++ b/requests/EDC-V Onboarding/CFM - Provision Provider/folder.bru @@ -1,6 +1,6 @@ meta { name: CFM - Provision Provider - seq: 3 + seq: 2 } auth { diff --git a/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Fetch Token.bru b/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Fetch Token.bru index 474fcade..a85e9c03 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Fetch Token.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Fetch Token.bru @@ -7,7 +7,7 @@ meta { get { url: {{sigletBaseUrl}}/tokens/{{consumer_id}}/{{TRANSFER_ID}} body: json - auth: none + auth: inherit } script:post-response { diff --git a/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Get Catalog.bru b/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Get Catalog.bru index 22cae316..945f0e14 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Get Catalog.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Get Catalog.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{cpBaseUrl}}/api/mgmt/v5beta/participants/{{consumer_id}}/catalog/request + url: {{cpBaseUrl}}/participants/{{consumer_id}}/catalog/request body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Poll Contract Negotiation.bru b/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Poll Contract Negotiation.bru index a1662574..2a6ef0a6 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Poll Contract Negotiation.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Poll Contract Negotiation.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{cpBaseUrl}}/api/mgmt/v5beta/participants/{{consumer_id}}/contractnegotiations/{{NEGOTIATION_ID}} + url: {{cpBaseUrl}}/participants/{{consumer_id}}/contractnegotiations/{{NEGOTIATION_ID}} body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Setup Contract Negotiation.bru b/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Setup Contract Negotiation.bru index 1b634e47..c6d219c0 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Setup Contract Negotiation.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Setup Contract Negotiation.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{cpBaseUrl}}/api/mgmt/v5beta/participants/{{consumer_id}}/contractnegotiations + url: {{cpBaseUrl}}/participants/{{consumer_id}}/contractnegotiations body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Setup Transfer.bru b/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Setup Transfer.bru index ef82751c..8ead490c 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Setup Transfer.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Data Transfer/Http Certs/Consumer/Setup Transfer.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{cpBaseUrl}}/api/mgmt/v5beta/participants/{{consumer_id}}/transferprocesses + url: {{cpBaseUrl}}/participants/{{consumer_id}}/transferprocesses body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Create CEL expression (Manufacturing).bru b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Create CEL expression (Manufacturing).bru index d6e2e6ca..4949b282 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Create CEL expression (Manufacturing).bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Create CEL expression (Manufacturing).bru @@ -5,7 +5,7 @@ meta { } post { - url: {{cpBaseUrl}}/api/mgmt/v5beta/celexpressions + url: {{cpBaseUrl}}/celexpressions body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Create CEL expression (Membership).bru b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Create CEL expression (Membership).bru index 829733aa..e5add982 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Create CEL expression (Membership).bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Create CEL expression (Membership).bru @@ -1,11 +1,11 @@ meta { name: Create CEL expression (Membership) type: http - seq: 2 + seq: 1 } post { - url: {{cpBaseUrl}}/api/mgmt/v5beta/celexpressions + url: {{cpBaseUrl}}/v5beta/celexpressions body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Get CEL expression.bru b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Get CEL expression.bru index 4dffa37a..a0f6f367 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Get CEL expression.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Get CEL expression.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{cpBaseUrl}}/api/mgmt/v5beta/celexpressions/{{CEL_EXPR_ID}} + url: {{cpBaseUrl}}/celexpressions/{{CEL_EXPR_ID}} body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Prepare Dataplane.bru b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Prepare Dataplane.bru index 5a5f1f5c..d0874b5f 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Prepare Dataplane.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Prepare Dataplane.bru @@ -1,11 +1,11 @@ meta { name: Prepare Dataplane type: http - seq: 5 + seq: 6 } put { - url: {{cpBaseUrl}}/api/mgmt/v5beta/participants/{{consumer_id}}/dataplanes + url: {{cpBaseUrl}}/participants/{{consumer_id}}/dataplanes body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Query all CEL Expressions.bru b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Query all CEL Expressions.bru new file mode 100644 index 00000000..8209725e --- /dev/null +++ b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Query all CEL Expressions.bru @@ -0,0 +1,25 @@ +meta { + name: Query all CEL Expressions + type: http + seq: 5 +} + +post { + url: {{cpBaseUrl}}/v5beta/celexpressions/request + body: json + auth: inherit +} + +body:json { + { + "@context": [ + "https://w3id.org/edc/connector/management/v2" + ], + "@type": "QuerySpec" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Update CEL expression.bru b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Update CEL expression.bru index 5106c2af..9b985bb9 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Update CEL expression.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Prepare Consumer Participant/Update CEL expression.bru @@ -5,7 +5,7 @@ meta { } put { - url: {{cpBaseUrl}}/api/mgmt/v5beta/celexpressions/{{CEL_EXPR_ID}} + url: {{cpBaseUrl}}/celexpressions/{{CEL_EXPR_ID}} body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Cert Asset.bru b/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Cert Asset.bru index 363a7a82..a2292713 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Cert Asset.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Cert Asset.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{cpBaseUrl}}/api/mgmt/v5beta/participants/{{provider_id}}/assets + url: {{cpBaseUrl}}/participants/{{provider_id}}/assets body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Contract Definition.bru b/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Contract Definition.bru index bdc6124f..848947dd 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Contract Definition.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Contract Definition.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{cpBaseUrl}}/api/mgmt/v5beta/participants/{{provider_id}}/contractdefinitions + url: {{cpBaseUrl}}/participants/{{provider_id}}/contractdefinitions body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Policy.bru b/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Policy.bru index 7ea5e194..761f0be4 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Policy.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Create Policy.bru @@ -5,7 +5,7 @@ meta { } post { - url: {{cpBaseUrl}}/api/mgmt/v5beta/participants/{{provider_id}}/policydefinitions + url: {{cpBaseUrl}}/participants/{{provider_id}}/policydefinitions body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Prepare Dataplane.bru b/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Prepare Dataplane.bru index ae01277e..c6317f6e 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Prepare Dataplane.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/Prepare Provider Participant/Prepare Dataplane.bru @@ -5,7 +5,7 @@ meta { } put { - url: {{cpBaseUrl}}/api/mgmt/v5beta/participants/{{provider_id}}/dataplanes + url: {{cpBaseUrl}}/participants/{{provider_id}}/dataplanes body: json auth: inherit } diff --git a/requests/EDC-V Onboarding/EDC-V Management/folder.bru b/requests/EDC-V Onboarding/EDC-V Management/folder.bru index 26faa729..34db41ff 100644 --- a/requests/EDC-V Onboarding/EDC-V Management/folder.bru +++ b/requests/EDC-V Onboarding/EDC-V Management/folder.bru @@ -1,6 +1,6 @@ meta { name: EDC-V Management - seq: 4 + seq: 3 } auth { @@ -16,6 +16,7 @@ auth:oauth2 { scope: management-api:read management-api:write credentials_placement: body credentials_id: credentials + token_source: access_token token_placement: header token_header_prefix: Bearer auto_fetch_token: true diff --git a/requests/EDC-V Onboarding/collection.bru b/requests/EDC-V Onboarding/collection.bru index 18262578..a47b903b 100644 --- a/requests/EDC-V Onboarding/collection.bru +++ b/requests/EDC-V Onboarding/collection.bru @@ -3,7 +3,23 @@ meta { } auth { - mode: none + mode: oauth2 +} + +auth:oauth2 { + grant_type: client_credentials + access_token_url: http://jad.localhost/auth/realms/edcv/protocol/openid-connect/token + refresh_token_url: + client_id: admin + client_secret: edc-v-admin-secret + scope: provision-manager-api:write + credentials_placement: body + credentials_id: administration-api + token_source: access_token + token_placement: header + token_header_prefix: Bearer + auto_fetch_token: true + auto_refresh_token: false } vars:pre-request { diff --git a/requests/EDC-V Onboarding/environments/KinD Local.bru b/requests/EDC-V Onboarding/environments/KinD Local.bru index 81d56f2b..67bab9c5 100644 --- a/requests/EDC-V Onboarding/environments/KinD Local.bru +++ b/requests/EDC-V Onboarding/environments/KinD Local.bru @@ -1,10 +1,10 @@ vars { - cpBaseUrl: http://cp.localhost - KC_HOST: http://keycloak.localhost + cpBaseUrl: http://jad.localhost/api/management + KC_HOST: http://jad.localhost/auth VAULT_HOST: http://vault.localhost - tmBaseUrl: http://tm.localhost - pmBaseUrl: http://pm.localhost + tmBaseUrl: http://jad.localhost/api/tm + pmBaseUrl: http://jad.localhost/api/pm VAULT_TOKEN: root - dpBaseUrl: http://dp.localhost - sigletBaseUrl: http://siglet.localhost + dpBaseUrl: http://jad.localhost/dp + sigletBaseUrl: http://jad.localhost/api/siglet } diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/ApiAuthTest.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/ApiAuthTest.java new file mode 100644 index 00000000..9943abde --- /dev/null +++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/ApiAuthTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.jad.tests; + +import org.eclipse.edc.junit.annotations.EndToEndTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; + +@EndToEndTest +public class ApiAuthTest { + @Test + void verifyAuthenticatedRequest_shouldSucceed() { + var token = KeycloakApi.createKeycloakToken("provisioner", "provisioner-secret", "tenant-manager-api:read", "tenant-manager-api:write"); + + given() + .header("Authorization", "Bearer " + token) + .baseUri(Constants.TM_BASE_URL) + .get("/cells") + .then() + .statusCode(200) + .body("size()", greaterThanOrEqualTo(1)); + } + + @Test + void verifyMissingAuthHeader_shouldReturn401() { + given() + // missing: auth header + .baseUri(Constants.TM_BASE_URL) + .get("/cells") + .then() + .statusCode(401); + } + + @Test + void verifyInvalidToken_shouldReturn401() { + given() + .header("Authorization", "Bearer invalid-token") + .baseUri(Constants.TM_BASE_URL) + .get("/cells") + .then() + .statusCode(401); + } +} diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/Constants.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/Constants.java index a085c81f..e75ccb9e 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/Constants.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/Constants.java @@ -19,13 +19,13 @@ public interface Constants { // make sure that all the following URLs are valid. This is done by port-forwarding the Gateway API Controller (Traefik) port (80) to localhost:8080 String APPLICATION_JSON = "application/json"; - String TM_BASE_URL = "http://tm.localhost:8080"; - String PM_BASE_URL = "http://pm.localhost:8080"; + String TM_BASE_URL = "http://jad.localhost:8080/api/tm"; + String PM_BASE_URL = "http://jad.localhost:8080/api/pm"; String VAULT_URL = "http://vault.localhost:8080"; - String CONTROLPLANE_BASE_URL = "http://cp.localhost:8080"; - String SIGLET_BASE_URL = "http://siglet.localhost:8080"; - String DATAPLANE_BASE_URL = "http://dp.localhost:8080"; - String IDENTITYHUB_BASE_URL = "http://ih.localhost:8080"; - String KEYCLOAK_URL = "http://keycloak.localhost:8080"; + String CONTROLPLANE_BASE_URL = "http://jad.localhost:8080/api/management"; + String SIGLET_BASE_URL = "http://jad.localhost:8080/api/siglet"; + String DATAPLANE_BASE_URL = "http://jad.localhost:8080/"; + String IDENTITYHUB_BASE_URL = "http://jad.localhost:8080/api/identity"; + String KEYCLOAK_URL = "http://jad.localhost:8080/auth"; String CONTROLPLANE_PROTOCOL_URL = "http://controlplane.edc-v.svc.cluster.local:8082/api/dsp/%s/2025-1"; } diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferEndToEndTest.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferEndToEndTest.java index 971fb6dc..3829d765 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferEndToEndTest.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/DataTransferEndToEndTest.java @@ -19,6 +19,7 @@ import io.restassured.RestAssured; import io.restassured.config.ObjectMapperConfig; import io.restassured.config.RestAssuredConfig; +import io.restassured.specification.RequestSpecification; import org.eclipse.edc.connector.controlplane.test.system.utils.client.ManagementApiClientV5; import org.eclipse.edc.connector.controlplane.test.system.utils.client.api.model.AssetDto; import org.eclipse.edc.connector.controlplane.test.system.utils.client.api.model.AtomicConstraintDto; @@ -36,7 +37,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import java.io.IOException; import java.net.URI; import java.time.Instant; import java.util.HashMap; @@ -63,29 +63,16 @@ */ @EndToEndTest public class DataTransferEndToEndTest { - - private static final String VAULT_TOKEN = "root"; private static final ConsoleMonitor MONITOR = new ConsoleMonitor(ConsoleMonitor.Level.DEBUG, true); private static final DynamicTokenProvider DYNAMIC_TOKEN_PROVIDER = new DynamicTokenProvider(); - private static final ManagementApiClientV5 MANAGEMENT_API_CLIENT = new ManagementApiClientV5(DYNAMIC_TOKEN_PROVIDER, new LazySupplier<>(() -> URI.create(CONTROLPLANE_BASE_URL + "/api/mgmt"))); + private static final ManagementApiClientV5 MANAGEMENT_API_CLIENT = new ManagementApiClientV5(DYNAMIC_TOKEN_PROVIDER, new LazySupplier<>(() -> URI.create(CONTROLPLANE_BASE_URL))); private static ClientCredentials providerCredentials; private static ClientCredentials consumerCredentials; private static String providerContextId; private static ClientCredentials manufacturerCredentials; - static String loadResourceFile(String resourceName) { - try (var is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName)) { - if (is == null) { - throw new RuntimeException("Resource not found: " + resourceName); - } - return new String(is.readAllBytes()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - @BeforeAll static void prepare() { // globally disable failing on unknown properties for RestAssured @@ -155,15 +142,28 @@ private static void createManufacturerCelExpression() { * * @return the Cell ID */ - private static String getCellId() { - return given() + public static String getCellId() { + return apiRequest() .contentType(APPLICATION_JSON) - .get(TM_BASE_URL + "/api/v1alpha1/cells") + .get(TM_BASE_URL + "/cells") .then() .statusCode(200) .extract().jsonPath().getString("[0].id"); } + public static RequestSpecification participantRequest() { + return given() + .header("Authorization", "Bearer " + DYNAMIC_TOKEN_PROVIDER.createToken(providerCredentials.clientId(), "participant")); + } + + /** + * Creates an authenticated request for any of the Administration APIs (hitting the "single pane of glass") + */ + public static RequestSpecification apiRequest() { + return given() + .header("Authorization", "Bearer " + DYNAMIC_TOKEN_PROVIDER.createToken(null, "admin")); + } + @Test void testCertDataTransfer() { @@ -188,7 +188,7 @@ void testCertDataTransfer() { MONITOR.info("Fetching siglet token for transferId: " + transferId); - var transferResponse = given() + var transferResponse = apiRequest() .baseUri(SIGLET_BASE_URL) .get("/tokens/%s/%s".formatted(consumerCredentials.clientId(), transferId)) .then() @@ -202,7 +202,7 @@ void testCertDataTransfer() { .header("Authorization", "Bearer " + accessToken) .body("{}") .contentType("application/json") - .post("/app/public/api/data/certs/request") + .post("/api/dp/certs/api/data/certs/request") .then() .statusCode(200) .extract().body().as(List.class); @@ -242,7 +242,7 @@ void testTransferLimitedAccess() { MONITOR.info("Fetching siglet token for transferId: " + transferId); - var transferResponse = given() + var transferResponse = apiRequest() .baseUri(SIGLET_BASE_URL) .get("/tokens/%s/%s".formatted(manufacturerCredentials.clientId(), transferId)) .then() @@ -256,7 +256,7 @@ void testTransferLimitedAccess() { .header("Authorization", "Bearer " + accessToken) .body("{}") .contentType("application/json") - .post("/app/public/api/data/certs/request") + .post("/api/dp/certs/api/data/certs/request") .then() .statusCode(200) .extract().body().as(List.class); @@ -284,7 +284,7 @@ private void registerDataPlane(String participantContextId) { private String createAsset(String participantContextId, String description) { var properties = new HashMap(); properties.put("description", description); - var asset = new AssetDto(properties, null); + var asset = new AssetDto(properties, Map.of()); return MANAGEMENT_API_CLIENT.assets().createAsset(participantContextId, asset); } @@ -299,9 +299,9 @@ private String createPolicyDef(String participantContextId, String leftOperand) return MANAGEMENT_API_CLIENT.policies().createPolicyDefinition(participantContextId, new PolicyDefinitionDto(policy)); } - private String createContractDef(String participantContextId, String accessPolicyId, String contractPolicyId, String assetId) { + private void createContractDef(String participantContextId, String accessPolicyId, String contractPolicyId, String assetId) { var selector = new CriterionDto("https://w3id.org/edc/v0.0.1/ns/id", "=", assetId); var contractDef = new ContractDefinitionDto(accessPolicyId, contractPolicyId, List.of(selector)); - return MANAGEMENT_API_CLIENT.contractDefinitions().createContractDefinition(participantContextId, contractDef); + MANAGEMENT_API_CLIENT.contractDefinitions().createContractDefinition(participantContextId, contractDef); } } diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/KeycloakApi.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/KeycloakApi.java index 92003543..b8ca4b9d 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/KeycloakApi.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/KeycloakApi.java @@ -18,55 +18,13 @@ import static io.restassured.RestAssured.given; import static org.eclipse.edc.jad.tests.Constants.KEYCLOAK_URL; -import static org.eclipse.edc.jad.tests.DataTransferEndToEndTest.loadResourceFile; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.equalTo; public class KeycloakApi { - private static final String KEYCLOAK_ADMIN_USER = "admin"; - private static final String KEYCLOAK_ADMIN_PASSWORD = "admin"; - - static void createKeycloakUser(String name, String clientId, String secret, String role, String token) { - var template = loadResourceFile("create_keycloak_user.json"); - template = template - .replace("{{issuer_name}}", name) - .replace("{{issuer_clientId}}", clientId) - .replace("{{issuer_clientSecret}}", secret) - .replace("{{role}}", role); - - given() - .baseUri(KEYCLOAK_URL) - .contentType("application/json") - .auth().oauth2(token) - .body(template) - .post("/admin/realms/edcv/clients") - .then() - .log().ifError() - .statusCode(anyOf(equalTo(201), equalTo(409))); - - } static String createKeycloakToken(String clientId, String clientSecret, String... scopes) { return getAccessToken(clientId, clientSecret, String.join(" ", scopes)).accessToken(); } - static String createKeycloakAdminToken() { - var at = given() - .baseUri(KEYCLOAK_URL) - .contentType("application/x-www-form-urlencoded") - .formParam("username", KEYCLOAK_ADMIN_USER) - .formParam("password", KEYCLOAK_ADMIN_PASSWORD) - .formParam("client_id", "admin-cli") - .formParam("grant_type", "password") - .post("/realms/master/protocol/openid-connect/token") - .then() - .statusCode(200) - .extract() - .body() - .as(AccessToken.class); - return at.accessToken(); - } - static AccessToken getAccessToken(String clientId, String clientSecret, String scope) { return given() .baseUri(KEYCLOAK_URL) diff --git a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/ParticipantOnboarding.java b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/ParticipantOnboarding.java index f5f5fa3c..785cec54 100644 --- a/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/ParticipantOnboarding.java +++ b/tests/end2end/src/test/java/org/eclipse/edc/jad/tests/ParticipantOnboarding.java @@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.eclipse.edc.jad.tests.Constants.IDENTITYHUB_BASE_URL; +import static org.eclipse.edc.jad.tests.DataTransferEndToEndTest.apiRequest; import static org.eclipse.edc.jad.tests.KeycloakApi.createKeycloakToken; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; @@ -86,10 +87,10 @@ public ClientCredentials execute(String cellId, String... roles) { } private String getDataspaceProfileId() { - return given() + return apiRequest() .baseUri(Constants.TM_BASE_URL) .contentType(Constants.APPLICATION_JSON) - .get("/api/v1alpha1/dataspace-profiles") + .get("/dataspace-profiles") .then() .log().ifValidationFails() .statusCode(200) @@ -115,10 +116,10 @@ private String getVaultSecret(String participantContextId) { * @return the Orchestration object */ private ParticipantProfile getParticipantProfile(String tenant, String profileId) { - return given() + return apiRequest() .baseUri(Constants.TM_BASE_URL) .contentType(Constants.APPLICATION_JSON) - .get("/api/v1alpha1/tenants/%s/participant-profiles/%s".formatted(tenant, profileId)) + .get("/tenants/%s/participant-profiles/%s".formatted(tenant, profileId)) .then() .log().ifValidationFails() .statusCode(200) @@ -143,7 +144,7 @@ private String queryOrchestrationByProfileId(String participantProfileId) { "predicate": "correlationId = '%s'" } """.formatted(participantProfileId)) - .post("/api/v1alpha1/orchestrations/query") + .post("/api/orchestrations/query") .then() .log().ifValidationFails() .statusCode(200) @@ -175,11 +176,11 @@ private String deployParticipantProfile(String tenantId, String cellId, String p body.put("participantRoles", Map.of(dataspaceId, rolesString)); } - return given() + return apiRequest() .baseUri(Constants.TM_BASE_URL) .contentType(Constants.APPLICATION_JSON) .body(body) - .post("/api/v1alpha1/tenants/%s/participant-profiles".formatted(tenantId)) + .post("/tenants/%s/participant-profiles".formatted(tenantId)) .then() .log().ifValidationFails() .statusCode(202) @@ -194,7 +195,7 @@ private String deployParticipantProfile(String tenantId, String cellId, String p * @return the tenant ID. */ private String createTenant(String tenantName) { - return given() + return apiRequest() .baseUri(Constants.TM_BASE_URL) .contentType(Constants.APPLICATION_JSON) .body(""" @@ -205,7 +206,7 @@ private String createTenant(String tenantName) { } } """.formatted(tenantName)) - .post("/api/v1alpha1/tenants") + .post("/tenants") .then() .log().ifValidationFails() .statusCode(201) @@ -216,11 +217,11 @@ private String createTenant(String tenantName) { private void waitForCredentialIssuance(String participantContextId, String userToken, String holderPid) { await().atMost(20, SECONDS) .pollInterval(1, SECONDS).until(() -> { - var body = given() + var body = apiRequest() .baseUri(IDENTITYHUB_BASE_URL) .contentType("application/json") .auth().oauth2(userToken) - .get("/cs/api/identity/v1alpha/participants/%s/credentials/request/%s".formatted(participantContextId, holderPid)) + .get("/participants/%s/credentials/request/%s".formatted(participantContextId, holderPid)) .then() .log().ifValidationFails() .statusCode(anyOf(equalTo(200), equalTo(204))) diff --git a/tests/end2end/src/test/resources/create_keycloak_user.json b/tests/end2end/src/test/resources/create_keycloak_user.json deleted file mode 100644 index 21150e41..00000000 --- a/tests/end2end/src/test/resources/create_keycloak_user.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "clientId": "{{issuer_name}}", - "name": "{{issuer_clientId}} Client", - "description": "Client for Vault Access", - "enabled": true, - "secret": "{{issuer_clientSecret}}", - "protocol": "openid-connect", - "publicClient": false, - "serviceAccountsEnabled": true, - "standardFlowEnabled": false, - "directAccessGrantsEnabled": false, - "fullScopeAllowed": true, - "protocolMappers": [ - { - "name": "participantContextId", - "protocol": "openid-connect", - "protocolMapper": "oidc-hardcoded-claim-mapper", - "consentRequired": false, - "config": { - "claim.name": "participant_context_id", - "claim.value": "{{issuer_clientId}}", - "jsonType.label": "String", - "access.token.claim": "true", - "id.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "name": "role", - "protocol": "openid-connect", - "protocolMapper": "oidc-hardcoded-claim-mapper", - "consentRequired": false, - "config": { - "claim.name": "role", - "claim.value": "{{role}}", - "jsonType.label": "String", - "access.token.claim": "true", - "id.token.claim": "true", - "userinfo.token.claim": "true" - } - } - ] -} \ No newline at end of file