diff --git a/.agents/skills/debug-openshell-cluster/SKILL.md b/.agents/skills/debug-openshell-cluster/SKILL.md index cc77d771b2..cb685102c6 100644 --- a/.agents/skills/debug-openshell-cluster/SKILL.md +++ b/.agents/skills/debug-openshell-cluster/SKILL.md @@ -286,14 +286,20 @@ even when local Helm values disable TLS. If `server.providerTokenGrants.spiffe.enabled=true`, the gateway should still render `[openshell.gateway.gateway_jwt]` and mount the `sandbox-jwt` Secret. -SPIRE is used only by sandbox pods for dynamic provider token grants. Verify -that SPIRE is installed, the CSI driver is available, and the Kubernetes driver -config includes `provider_spiffe_workload_api_socket_path`: +SPIRE is used by both the gateway and sandbox supervisors for dynamic provider +token grants. The gateway pod must mount the `spiffe-workload-api` CSI volume +and set `OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET`; sandbox pods must +receive the matching Workload API socket from the Kubernetes driver config. +The gateway verifies supervisor JWT-SVIDs from JWT bundles fetched through this +Workload API socket, not from the SPIRE OIDC discovery endpoint. +Verify that SPIRE is installed, the CSI driver is available, and the Kubernetes +driver config includes `provider_spiffe_workload_api_socket_path`: ```bash helm -n openshell get values openshell | grep -E 'providerTokenGrants|workloadApiSocketPath' kubectl get pods -A | grep -E 'spire|spiffe' kubectl -n openshell get configmap openshell-config -o yaml | grep provider_spiffe_workload_api_socket_path +kubectl -n openshell get pod -l app.kubernetes.io/name=helm-chart -o jsonpath="{.items[*].spec.containers[*].env[?(@.name==\"OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET\")].value}{\"\n\"}" ``` Sandbox pods using provider token grants should have an diff --git a/Cargo.lock b/Cargo.lock index c0afff104b..d458f61286 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,6 +690,12 @@ dependencies = [ "backtrace", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base16ct" version = "1.0.0" @@ -1278,6 +1284,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-bigint" version = "0.7.5" @@ -1322,7 +1340,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.7.5", "rand_core 0.10.1", ] @@ -1345,6 +1363,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "5.0.0" @@ -1355,7 +1389,7 @@ dependencies = [ "cpufeatures 0.3.0", "curve25519-dalek-derive", "digest 0.11.2", - "fiat-crypto", + "fiat-crypto 0.3.0", "rand_core 0.10.1", "rustc_version", "subtle", @@ -1605,6 +1639,20 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", +] + [[package]] name = "ecdsa" version = "0.17.0" @@ -1613,13 +1661,23 @@ checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ "der 0.8.0", "digest 0.11.2", - "elliptic-curve", - "rfc6979", + "elliptic-curve 0.14.1", + "rfc6979 0.6.0", "signature 3.0.0", "spki 0.8.0", "zeroize", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + [[package]] name = "ed25519" version = "3.0.0" @@ -1630,14 +1688,28 @@ dependencies = [ "signature 3.0.0", ] +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "ed25519-dalek" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ - "curve25519-dalek", - "ed25519", + "curve25519-dalek 5.0.0", + "ed25519 3.0.0", "rand_core 0.10.1", "serde", "sha2 0.11.0", @@ -1655,24 +1727,45 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest 0.10.7", + "ff 0.13.1", + "generic-array 0.14.7", + "group 0.13.0", + "hkdf 0.12.4", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", + "subtle", + "zeroize", +] + [[package]] name = "elliptic-curve" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ - "base16ct", - "crypto-bigint", + "base16ct 1.0.0", + "crypto-bigint 0.7.5", "crypto-common 0.2.2", "digest 0.11.2", - "ff", - "group", + "ff 0.14.0", + "group 0.14.0", "hkdf 0.13.0", "hybrid-array", "pem-rfc7468 1.0.0", "pkcs8 0.11.0", "rand_core 0.10.1", - "sec1", + "sec1 0.8.1", "subtle", "zeroize", ] @@ -1759,6 +1852,16 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "ff" version = "0.14.0" @@ -1769,6 +1872,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fiat-crypto" version = "0.3.0" @@ -1968,6 +2077,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -2069,13 +2179,24 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.1", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "group" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ - "ff", + "ff 0.14.0", "rand_core 0.10.1", "subtle", ] @@ -2411,6 +2532,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.4", "tower-service", + "webpki-roots", ] [[package]] @@ -2868,11 +2990,18 @@ checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "aws-lc-rs", "base64 0.22.1", + "ed25519-dalek 2.2.0", "getrandom 0.2.17", + "hmac 0.12.1", "js-sys", + "p256 0.13.2", + "p384 0.13.1", "pem", + "rand 0.8.6", + "rsa 0.9.10", "serde", "serde_json", + "sha2 0.10.9", "signature 2.2.0", "simple_asn1", ] @@ -4133,6 +4262,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "socket2 0.6.3", + "spiffe", "sqlx", "tempfile", "thiserror 2.0.18", @@ -4406,30 +4536,54 @@ version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", + "sha2 0.10.9", +] + [[package]] name = "p256" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", "primefield", - "primeorder", + "primeorder 0.14.0", "sha2 0.11.0", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", + "sha2 0.10.9", +] + [[package]] name = "p384" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f" dependencies = [ - "ecdsa", - "elliptic-curve", - "fiat-crypto", + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "fiat-crypto 0.3.0", "primefield", - "primeorder", + "primeorder 0.14.0", "sha2 0.11.0", ] @@ -4439,11 +4593,11 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449" dependencies = [ - "base16ct", - "ecdsa", - "elliptic-curve", + "base16ct 1.0.0", + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", "primefield", - "primeorder", + "primeorder 0.14.0", "sha2 0.11.0", ] @@ -4818,21 +4972,30 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.7.5", "crypto-common 0.2.2", - "ff", + "ff 0.14.0", "rand_core 0.10.1", "subtle", "zeroize", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve 0.13.8", +] + [[package]] name = "primeorder" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.14.1", "once_cell", "primefield", "serdect", @@ -5349,6 +5512,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", ] [[package]] @@ -5392,13 +5556,23 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "rfc6979" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.7.5", "hmac 0.13.0", ] @@ -5455,7 +5629,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" dependencies = [ "const-oid 0.10.2", - "crypto-bigint", + "crypto-bigint 0.7.5", "crypto-primes", "digest 0.11.2", "pkcs1 0.8.0-rc.4", @@ -5481,16 +5655,16 @@ dependencies = [ "bytes", "cbc", "cipher", - "crypto-bigint", + "crypto-bigint 0.7.5", "ctr", - "curve25519-dalek", + "curve25519-dalek 5.0.0", "data-encoding", "delegate", "der 0.8.0", "digest 0.11.2", - "ecdsa", - "ed25519-dalek", - "elliptic-curve", + "ecdsa 0.17.0", + "ed25519-dalek 3.0.0", + "elliptic-curve 0.14.1", "enum_dispatch", "flate2", "futures", @@ -5507,8 +5681,8 @@ dependencies = [ "ml-kem", "module-lattice", "num-bigint", - "p256", - "p384", + "p256 0.14.0", + "p384 0.14.0", "p521", "pageant", "pbkdf2", @@ -5523,7 +5697,7 @@ dependencies = [ "russh-util", "salsa20", "scrypt", - "sec1", + "sec1 0.8.1", "sha1 0.11.0", "sha2 0.11.0", "sha3 0.12.0", @@ -5825,13 +5999,27 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.10", + "generic-array 0.14.7", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + [[package]] name = "sec1" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ - "base16ct", + "base16ct 1.0.0", "ctutils", "der 0.8.0", "hybrid-array", @@ -6028,7 +6216,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9af4a3e75ebd5599b30d4de5768e00b5095d518a79fefc3ecbaf77e665d1ec06" dependencies = [ - "base16ct", + "base16ct 1.0.0", "serde", ] @@ -6246,6 +6434,7 @@ dependencies = [ "fastrand", "futures", "hyper-util", + "jsonwebtoken", "log", "prost", "prost-types", @@ -6512,7 +6701,7 @@ checksum = "7b54d0ed0498daf3f78d82e00e28c8eec9d75a067c4cfbcc7a0f7d0f4077749e" dependencies = [ "base64ct", "bytes", - "crypto-bigint", + "crypto-bigint 0.7.5", "ctutils", "digest 0.11.2", "pem-rfc7468 1.0.0", @@ -6528,15 +6717,15 @@ dependencies = [ "argon2", "bcrypt-pbkdf", "ctutils", - "ed25519-dalek", + "ed25519-dalek 3.0.0", "hex", "hmac 0.13.0", - "p256", - "p384", + "p256 0.14.0", + "p384 0.14.0", "p521", "rand_core 0.10.1", "rsa 0.10.0-rc.18", - "sec1", + "sec1 0.8.1", "sha1 0.11.0", "sha2 0.11.0", "signature 3.0.0", @@ -8310,8 +8499,8 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" dependencies = [ - "ff", - "group", + "ff 0.14.0", + "group 0.14.0", "hybrid-array", ] diff --git a/Cargo.toml b/Cargo.toml index 150df10d69..c7eb364acf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,7 +105,7 @@ rand = "0.9" jsonwebtoken = { version = "10", features = ["aws_lc_rs"] } getrandom = "0.3" ring = "0.17" -spiffe = { version = "0.15", default-features = false, features = ["workload-api-jwt", "tracing"] } +spiffe = { version = "0.15", default-features = false, features = ["workload-api-jwt", "jwt-verify-rust-crypto", "tracing"] } # Filesystem embedding include_dir = "0.7" diff --git a/architecture/sandbox.md b/architecture/sandbox.md index bd1feae1f2..2425ed1511 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -221,13 +221,29 @@ placeholders for rotating provider credentials; provider environment keys beginning with `v_` are reserved for that placeholder namespace. Provider profiles can also declare dynamic token grants. For matching HTTP -endpoints, the supervisor obtains a SPIFFE JWT-SVID from the local Workload API, -exchanges it for an OAuth2 access token, caches the token, and injects it as an -`Authorization: Bearer` header before forwarding the request. Token grant -endpoints are HTTPS-only except for loopback and Kubernetes service DNS hosts, -and returned access tokens must be bearer-compatible before they are cached or -injected. Token response lifetimes are capped and cached with an expiry margin -unless a profile supplies an explicit cache TTL override. +endpoints, the supervisor obtains or exchanges OAuth2 access tokens, caches +them, and injects them before forwarding the request. `client_credentials` +grants use the supervisor SPIFFE JWT-SVID directly as the client assertion. +`token_exchange` grants ask the gateway to broker an intermediate token using a +stored provider subject credential and the gateway's own SPIFFE JWT-SVID; the +supervisor then exchanges that intermediate token for the final upstream token +using its own JWT-SVID. The gateway validates that its own JWT-SVID has the +requested audience, a SPIFFE subject, and a non-expired `exp` claim when +present. It also validates that the stored subject credential is declared by the +provider profile, and that the supervisor JWT-SVID is a well-formed +three-segment JWT with a SPIFFE subject in the same trust domain as the gateway +SVID. The gateway verifies the supervisor JWT-SVID signature with JWT bundles +fetched from its SPIFFE Workload API. Token grant endpoints are HTTPS-only +except for loopback and Kubernetes service DNS hosts, and returned access tokens +must be bearer-compatible before they are cached or injected. Token response +lifetimes are capped and cached with an expiry margin unless a profile supplies +an explicit cache TTL override. Cache entries are scoped by the sandbox provider +environment revision so provider credential updates miss the old token cache +without changing endpoint matching semantics. Gateway-brokered intermediate +tokens are cached separately by provider resource version, supervisor SPIFFE +subject, and gateway SPIFFE subject, and their cache lifetime is capped by the +intermediate token response, stored subject-token expiry, and supervisor SVID +expiry. For AWS endpoints that require request-level signing, the proxy supports SigV4 re-signing. When `credential_signing: sigv4` is set on an L7 endpoint, the proxy diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index 6733d2d293..ca495c4192 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -811,7 +811,7 @@ impl From for openshell_cli::ssh::Editor { #[derive(Subcommand, Debug)] enum ProviderCommands { /// Create a provider config. - #[command(group = clap::ArgGroup::new("cred_source").required(true).args(["from_existing", "credentials", "from_gcloud_adc", "runtime_credentials"]), help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + #[command(group = clap::ArgGroup::new("cred_source").required(true).multiple(true).args(["from_existing", "credentials", "from_gcloud_adc", "runtime_credentials", "from_oidc_token"]), help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] Create { /// Provider name. #[arg(long)] @@ -822,7 +822,7 @@ enum ProviderCommands { provider_type: String, /// Load provider credentials/config from existing local state. - #[arg(long, conflicts_with_all = ["credentials", "from_gcloud_adc", "runtime_credentials"])] + #[arg(long, conflicts_with_all = ["credentials", "from_gcloud_adc", "runtime_credentials", "from_oidc_token"])] from_existing: bool, /// Provider credential pair (`KEY=VALUE`) or env lookup key (`KEY`). @@ -836,11 +836,15 @@ enum ProviderCommands { /// Configure credentials from gcloud Application Default Credentials /// (`~/.config/gcloud/application_default_credentials.json`). /// Valid for providers whose profile declares an ADC-compatible credential. - #[arg(long, group = "cred_source", conflicts_with_all = ["from_existing", "credentials", "runtime_credentials"])] + #[arg(long, group = "cred_source", conflicts_with_all = ["from_existing", "credentials", "runtime_credentials", "from_oidc_token"])] from_gcloud_adc: bool, + /// Store the active gateway OIDC access token as the named provider credential. + #[arg(long, group = "cred_source", conflicts_with_all = ["from_existing", "from_gcloud_adc", "runtime_credentials"])] + from_oidc_token: bool, + /// Create a provider whose required credentials are resolved at runtime by the gateway/sandbox. - #[arg(long, conflicts_with_all = ["from_existing", "credentials", "from_gcloud_adc"])] + #[arg(long, conflicts_with_all = ["from_existing", "credentials", "from_gcloud_adc", "from_oidc_token"])] runtime_credentials: bool, /// Provider config key/value pair. @@ -913,9 +917,13 @@ enum ProviderCommands { name: String, /// Re-discover credentials from existing local state (e.g. env vars, config files). - #[arg(long, conflicts_with = "credentials")] + #[arg(long, conflicts_with_all = ["credentials", "from_oidc_token"])] from_existing: bool, + /// Store the active gateway OIDC access token as the named provider credential. + #[arg(long, conflicts_with = "from_existing")] + from_oidc_token: bool, + /// Provider credential pair (`KEY=VALUE`) or env lookup key (`KEY`). #[arg( long = "credential", @@ -3328,24 +3336,34 @@ async fn run_async() -> Result<()> { from_existing, credentials, from_gcloud_adc, + from_oidc_token, runtime_credentials, config, global_profile, } => { let profile_ws = if global_profile { "" } else { &cli.workspace }; - run::provider_create_with_options( - endpoint, - &name, - provider_type.as_str(), - from_existing, - &credentials, - from_gcloud_adc, - runtime_credentials, - &config, - &cli.workspace, - profile_ws, - &tls, - ) + let credential_source = if from_existing { + run::ProviderCreateCredentialSource::Existing + } else if from_gcloud_adc { + run::ProviderCreateCredentialSource::GcloudAdc + } else if from_oidc_token { + run::ProviderCreateCredentialSource::OidcToken + } else if runtime_credentials { + run::ProviderCreateCredentialSource::Runtime + } else { + run::ProviderCreateCredentialSource::ExplicitCredentials + }; + run::provider_create_with_options(run::ProviderCreateOptions { + server: endpoint, + name: &name, + provider_type: provider_type.as_str(), + credentials: &credentials, + credential_source, + config: &config, + workspace: &cli.workspace, + profile_workspace: profile_ws, + tls: &tls, + }) .await?; } ProviderCommands::Refresh(command) => match command { @@ -3498,20 +3516,22 @@ async fn run_async() -> Result<()> { ProviderCommands::Update { name, from_existing, + from_oidc_token, credentials, config, credential_expires_at, } => { - run::provider_update( - endpoint, - &name, + run::provider_update(run::ProviderUpdateOptions { + server: endpoint, + name: &name, from_existing, - &credentials, - &config, - &credential_expires_at, - &cli.workspace, - &tls, - ) + from_oidc_token, + credentials: &credentials, + config: &config, + credential_expires_at: &credential_expires_at, + workspace: &cli.workspace, + tls: &tls, + }) .await?; } ProviderCommands::Delete { names } => { diff --git a/crates/openshell-cli/src/oidc_auth.rs b/crates/openshell-cli/src/oidc_auth.rs index 2aacdb0c9c..291108f311 100644 --- a/crates/openshell-cli/src/oidc_auth.rs +++ b/crates/openshell-cli/src/oidc_auth.rs @@ -277,10 +277,11 @@ fn bundle_from_refresh_output( } } -/// Ensure we have a valid OIDC token for the given gateway, refreshing if needed. -/// -/// Returns the access token string. -pub async fn ensure_valid_oidc_token(gateway_name: &str, insecure: bool) -> Result { +/// Ensure we have a valid OIDC token bundle for the given gateway, refreshing if needed. +pub async fn ensure_valid_oidc_token_bundle( + gateway_name: &str, + insecure: bool, +) -> Result { let bundle = openshell_bootstrap::oidc_token::load_oidc_token(gateway_name).ok_or_else(|| { miette::miette!( @@ -290,7 +291,7 @@ pub async fn ensure_valid_oidc_token(gateway_name: &str, insecure: bool) -> Resu })?; if !openshell_bootstrap::oidc_token::is_token_expired(&bundle) { - return Ok(bundle.access_token); + return Ok(bundle); } debug!( @@ -302,7 +303,16 @@ pub async fn ensure_valid_oidc_token(gateway_name: &str, insecure: bool) -> Resu .and_then(|metadata| metadata.oidc_scopes); let refreshed = oidc_refresh_token(&bundle, scopes.as_deref(), insecure).await?; openshell_bootstrap::oidc_token::store_oidc_token(gateway_name, &refreshed)?; - Ok(refreshed.access_token) + Ok(refreshed) +} + +/// Ensure we have a valid OIDC token for the given gateway, refreshing if needed. +/// +/// Returns the access token string. +pub async fn ensure_valid_oidc_token(gateway_name: &str, insecure: bool) -> Result { + Ok(ensure_valid_oidc_token_bundle(gateway_name, insecure) + .await? + .access_token) } // ── Helpers ────────────────────────────────────────────────────────── diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 01d28163d0..9841a68ce4 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -47,13 +47,13 @@ use openshell_core::proto::{ LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest, ListSandboxPoliciesRequest, ListSandboxProvidersRequest, ListSandboxesRequest, ListServicesRequest, PolicySource, PolicyStatus, Provider, ProviderCredentialRefreshStatus, - ProviderCredentialRefreshStrategy, ProviderProfile, ProviderProfileDiagnostic, - ProviderProfileImportItem, RejectDraftChunkRequest, ResourceRequirements, - RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, SandboxPhase, SandboxPolicy, - SandboxSpec, SandboxTemplate, ServiceEndpointResponse, SetInferenceRouteRequest, SettingScope, - TcpForwardFrame, TcpForwardInit, TcpRelayTarget, UpdateConfigRequest, - UpdateProviderProfilesRequest, UpdateProviderRequest, WatchSandboxRequest, exec_sandbox_event, - setting_value, tcp_forward_init, + ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantType, ProviderProfile, + ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest, + ResourceRequirements, RevokeSshSessionRequest, RotateProviderCredentialRequest, Sandbox, + SandboxPhase, SandboxPolicy, SandboxSpec, SandboxTemplate, ServiceEndpointResponse, + SetInferenceRouteRequest, SettingScope, TcpForwardFrame, TcpForwardInit, TcpRelayTarget, + UpdateConfigRequest, UpdateProviderProfilesRequest, UpdateProviderRequest, WatchSandboxRequest, + exec_sandbox_event, setting_value, tcp_forward_init, }; use openshell_core::settings; use openshell_core::{ObjectId, ObjectName, ObjectWorkspace}; @@ -3251,6 +3251,126 @@ fn missing_credentials_error(provider_type: &str) -> miette::Report { ) } +async fn provider_credential_from_oidc_token( + credentials: &[String], + profile: Option<&ProviderProfile>, + tls: &TlsOptions, +) -> Result<(HashMap, HashMap)> { + let credential_key = oidc_subject_credential_key(credentials, profile)?; + + let gateway_name = tls.gateway_name().ok_or_else(|| { + miette::miette!("--from-oidc-token requires an active named OIDC gateway") + })?; + let bundle = + crate::oidc_auth::ensure_valid_oidc_token_bundle(gateway_name, tls.gateway_insecure) + .await + .map_err(|err| { + miette::miette!( + "failed to load or refresh OIDC token for gateway '{gateway_name}' while preparing provider credential: {err}" + ) + })?; + + let mut credential_map = HashMap::new(); + credential_map.insert(credential_key.clone(), bundle.access_token); + + let mut credential_expires_at_ms = HashMap::new(); + if let Some(expires_at) = bundle.expires_at { + let expires_at_ms = i64::try_from(expires_at) + .unwrap_or(i64::MAX / 1000) + .saturating_mul(1000); + credential_expires_at_ms.insert(credential_key, expires_at_ms); + } + + Ok((credential_map, credential_expires_at_ms)) +} + +fn oidc_subject_credential_key( + credentials: &[String], + profile: Option<&ProviderProfile>, +) -> Result { + if credentials.len() > 1 { + return Err(miette::miette!( + "--from-oidc-token accepts at most one --credential KEY destination" + )); + } + + if let Some(credential) = credentials.first() { + let credential = credential.trim(); + if credential.is_empty() || credential.contains('=') { + return Err(miette::miette!( + "--from-oidc-token requires --credential KEY without an inline value" + )); + } + if let Some(profile) = profile { + ensure_profile_declares_subject_credential(profile, credential)?; + } + return Ok(credential.to_string()); + } + + let Some(profile) = profile else { + return Err(miette::miette!( + "--from-oidc-token requires --credential KEY when the provider profile is unavailable" + )); + }; + + infer_oidc_subject_credential_from_profile(profile) +} + +fn ensure_profile_declares_subject_credential( + profile: &ProviderProfile, + credential: &str, +) -> Result<()> { + let matches = token_exchange_subject_credentials(profile); + if matches.iter().any(|candidate| candidate == credential) { + return Ok(()); + } + Err(miette::miette!( + "credential '{credential}' is not declared as a token-exchange subject credential in provider profile '{}'; expected one of: {}", + profile.id, + matches.join(", ") + )) +} + +fn infer_oidc_subject_credential_from_profile(profile: &ProviderProfile) -> Result { + let matches = token_exchange_subject_credentials(profile); + match matches.as_slice() { + [credential] => Ok(credential.clone()), + [] => Err(miette::miette!( + "provider profile '{}' does not declare a token-exchange subject credential; pass --credential KEY", + profile.id + )), + _ => Err(miette::miette!( + "provider profile '{}' declares multiple token-exchange subject credentials ({}); pass --credential KEY", + profile.id, + matches.join(", ") + )), + } +} + +fn token_exchange_subject_credentials(profile: &ProviderProfile) -> Vec { + let mut matches = Vec::new(); + for credential in &profile.credentials { + let Some(token_grant) = credential.token_grant.as_ref() else { + continue; + }; + if ProviderCredentialTokenGrantType::try_from(token_grant.grant_type).ok() + != Some(ProviderCredentialTokenGrantType::TokenExchange) + { + continue; + } + let Some(subject_token) = token_grant.subject_token.as_ref() else { + continue; + }; + if subject_token.source != "provider_credential" || subject_token.credential.is_empty() { + continue; + } + if !matches.contains(&subject_token.credential) { + matches.push(subject_token.credential.clone()); + } + } + matches +} + #[allow(clippy::too_many_arguments)] pub async fn provider_create( server: &str, @@ -3263,44 +3383,77 @@ pub async fn provider_create( workspace: &str, tls: &TlsOptions, ) -> Result<()> { - provider_create_with_options( + let credential_source = match (from_existing, from_gcloud_adc) { + (true, true) => { + return Err(miette::miette!( + "--from-gcloud-adc cannot be combined with --from-existing, --from-oidc-token, or --credential; it also cannot be combined with --runtime-credentials" + )); + } + (true, false) => ProviderCreateCredentialSource::Existing, + (false, true) => ProviderCreateCredentialSource::GcloudAdc, + (false, false) => ProviderCreateCredentialSource::ExplicitCredentials, + }; + provider_create_with_options(ProviderCreateOptions { server, name, provider_type, - from_existing, credentials, - from_gcloud_adc, - false, + credential_source, config, workspace, - workspace, + profile_workspace: workspace, tls, - ) + }) .await } -#[allow(clippy::too_many_arguments)] -pub async fn provider_create_with_options( - server: &str, - name: &str, - provider_type: &str, - from_existing: bool, - credentials: &[String], - from_gcloud_adc: bool, - runtime_credentials: bool, - config: &[String], - workspace: &str, - profile_workspace: &str, - tls: &TlsOptions, -) -> Result<()> { - if from_gcloud_adc && (from_existing || !credentials.is_empty() || runtime_credentials) { +pub struct ProviderCreateOptions<'a> { + pub server: &'a str, + pub name: &'a str, + pub provider_type: &'a str, + pub credentials: &'a [String], + pub credential_source: ProviderCreateCredentialSource, + pub config: &'a [String], + pub workspace: &'a str, + pub profile_workspace: &'a str, + pub tls: &'a TlsOptions, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProviderCreateCredentialSource { + ExplicitCredentials, + Existing, + GcloudAdc, + OidcToken, + Runtime, +} + +pub async fn provider_create_with_options(options: ProviderCreateOptions<'_>) -> Result<()> { + let ProviderCreateOptions { + server, + name, + provider_type, + credentials, + credential_source, + config, + workspace, + profile_workspace, + tls, + } = options; + + let from_existing = credential_source == ProviderCreateCredentialSource::Existing; + let from_gcloud_adc = credential_source == ProviderCreateCredentialSource::GcloudAdc; + let from_oidc_token = credential_source == ProviderCreateCredentialSource::OidcToken; + let runtime_credentials = credential_source == ProviderCreateCredentialSource::Runtime; + + if from_gcloud_adc && !credentials.is_empty() { return Err(miette::miette!( - "--from-gcloud-adc cannot be combined with --from-existing, --credential, or --runtime-credentials" + "--from-gcloud-adc cannot be combined with --from-existing, --from-oidc-token, or --credential; it also cannot be combined with --runtime-credentials" )); } - if from_existing && (!credentials.is_empty() || runtime_credentials) { + if from_existing && !credentials.is_empty() { return Err(miette::miette!( - "--from-existing cannot be combined with --credential or --runtime-credentials" + "--from-existing cannot be combined with --credential" )); } if runtime_credentials && !credentials.is_empty() { @@ -3370,7 +3523,17 @@ pub async fn provider_create_with_options( None }; - let mut credential_map = parse_credential_pairs(credentials)?; + let oidc_profile = if from_oidc_token { + Some(fetch_provider_profile(&mut client, &provider_type, profile_workspace).await?) + } else { + None + }; + + let (mut credential_map, oidc_credential_expires_at_ms) = if from_oidc_token { + provider_credential_from_oidc_token(credentials, oidc_profile.as_ref(), tls).await? + } else { + (parse_credential_pairs(credentials)?, HashMap::new()) + }; let mut config_map = parse_key_value_pairs(config, "--config")?; if from_existing { @@ -3444,7 +3607,7 @@ pub async fn provider_create_with_options( r#type: provider_type.clone(), credentials: credential_map, config: config_map, - credential_expires_at_ms: HashMap::new(), + credential_expires_at_ms: oidc_credential_expires_at_ms, profile_workspace: profile_workspace.to_string(), credential_handles: HashMap::new(), }), @@ -4472,28 +4635,73 @@ fn print_provider_type_row( ); } -#[allow(clippy::too_many_arguments)] -pub async fn provider_update( - server: &str, - name: &str, - from_existing: bool, - credentials: &[String], - config: &[String], - credential_expires_at: &[String], - workspace: &str, - tls: &TlsOptions, -) -> Result<()> { +pub struct ProviderUpdateOptions<'a> { + pub server: &'a str, + pub name: &'a str, + pub from_existing: bool, + pub from_oidc_token: bool, + pub credentials: &'a [String], + pub config: &'a [String], + pub credential_expires_at: &'a [String], + pub workspace: &'a str, + pub tls: &'a TlsOptions, +} + +pub async fn provider_update(options: ProviderUpdateOptions<'_>) -> Result<()> { + let ProviderUpdateOptions { + server, + name, + from_existing, + from_oidc_token, + credentials, + config, + credential_expires_at, + workspace, + tls, + } = options; + if from_existing && !credentials.is_empty() { return Err(miette::miette!( "--from-existing cannot be combined with --credential" )); } + if from_existing && from_oidc_token { + return Err(miette::miette!( + "--from-existing cannot be combined with --from-oidc-token" + )); + } let mut client = grpc_client(server, tls).await?; - let mut credential_map = parse_credential_pairs(credentials)?; + let oidc_profile = if from_oidc_token { + let existing = client + .get_provider(GetProviderRequest { + name: name.to_string(), + workspace: workspace.to_string(), + }) + .await + .into_diagnostic()? + .into_inner() + .provider + .ok_or_else(|| miette::miette!("provider '{name}' not found"))?; + let profile_workspace = if existing.profile_workspace.is_empty() { + workspace + } else { + &existing.profile_workspace + }; + Some(fetch_provider_profile(&mut client, &existing.r#type, profile_workspace).await?) + } else { + None + }; + + let (mut credential_map, oidc_credential_expires_at_ms) = if from_oidc_token { + provider_credential_from_oidc_token(credentials, oidc_profile.as_ref(), tls).await? + } else { + (parse_credential_pairs(credentials)?, HashMap::new()) + }; let mut config_map = parse_key_value_pairs(config, "--config")?; - let credential_expires_at_ms = parse_credential_expiry_pairs(credential_expires_at)?; + let mut credential_expires_at_ms = parse_credential_expiry_pairs(credential_expires_at)?; + credential_expires_at_ms.extend(oidc_credential_expires_at_ms); if from_existing { // Fetch the existing provider to discover its type for credential lookup. diff --git a/crates/openshell-cli/tests/ensure_providers_integration.rs b/crates/openshell-cli/tests/ensure_providers_integration.rs index 5bd64c2f36..e02060368d 100644 --- a/crates/openshell-cli/tests/ensure_providers_integration.rs +++ b/crates/openshell-cli/tests/ensure_providers_integration.rs @@ -17,7 +17,8 @@ use openshell_core::proto::{ AttachSandboxProviderRequest, AttachSandboxProviderResponse, CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - DetachSandboxProviderRequest, DetachSandboxProviderResponse, ExecSandboxEvent, + DetachSandboxProviderRequest, DetachSandboxProviderResponse, + ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, @@ -220,6 +221,13 @@ impl OpenShell for TestOpenShell { Ok(Response::new(RevokeSshSessionResponse::default())) } + async fn exchange_provider_subject_token( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_provider( &self, request: tonic::Request, diff --git a/crates/openshell-cli/tests/mtls_integration.rs b/crates/openshell-cli/tests/mtls_integration.rs index 38c68ed83a..66341cd248 100644 --- a/crates/openshell-cli/tests/mtls_integration.rs +++ b/crates/openshell-cli/tests/mtls_integration.rs @@ -13,10 +13,11 @@ use openshell_cli::{ }; use openshell_core::proto::{ CreateProviderRequest, CreateSshSessionRequest, CreateSshSessionResponse, - DeleteProviderRequest, DeleteProviderResponse, ExecSandboxEvent, ExecSandboxInput, - ExecSandboxRequest, GetProviderRequest, HealthRequest, HealthResponse, ListProvidersRequest, - ListProvidersResponse, ProviderResponse, RevokeSshSessionRequest, RevokeSshSessionResponse, - ServiceStatus, UpdateProviderRequest, + DeleteProviderRequest, DeleteProviderResponse, ExchangeProviderSubjectTokenRequest, + ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, + GetProviderRequest, HealthRequest, HealthResponse, ListProvidersRequest, ListProvidersResponse, + ProviderResponse, RevokeSshSessionRequest, RevokeSshSessionResponse, ServiceStatus, + UpdateProviderRequest, open_shell_server::{OpenShell, OpenShellServer}, }; use tempfile::tempdir; @@ -192,6 +193,13 @@ impl OpenShell for TestOpenShell { Ok(Response::new(RevokeSshSessionResponse::default())) } + async fn exchange_provider_subject_token( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_provider( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/provider_commands_integration.rs b/crates/openshell-cli/tests/provider_commands_integration.rs index 24645ea259..d5a05ed30e 100644 --- a/crates/openshell-cli/tests/provider_commands_integration.rs +++ b/crates/openshell-cli/tests/provider_commands_integration.rs @@ -14,7 +14,8 @@ use openshell_core::proto::{ CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, DeleteProviderRefreshRequest, DeleteProviderRefreshResponse, DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - DetachSandboxProviderRequest, DetachSandboxProviderResponse, ExecSandboxEvent, + DetachSandboxProviderRequest, DetachSandboxProviderResponse, + ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse, GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, @@ -351,6 +352,13 @@ impl OpenShell for TestOpenShell { Ok(Response::new(RevokeSshSessionResponse::default())) } + async fn exchange_provider_subject_token( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_provider( &self, request: tonic::Request, @@ -1165,16 +1173,17 @@ async fn provider_cli_run_functions_support_full_crud_flow() { .await .expect("provider list"); - run::provider_update( - &ts.endpoint, - "my-claude", - false, - &["API_KEY=rotated".to_string()], - &["profile=prod".to_string()], - &[], - "default", - &ts.tls, - ) + run::provider_update(run::ProviderUpdateOptions { + server: &ts.endpoint, + name: "my-claude", + from_existing: false, + from_oidc_token: false, + credentials: &["API_KEY=rotated".to_string()], + config: &["profile=prod".to_string()], + credential_expires_at: &[], + workspace: "default", + tls: &ts.tls, + }) .await .expect("provider update"); @@ -1523,19 +1532,17 @@ async fn provider_create_allows_empty_credentials_for_gateway_refresh_profiles() }, ); - run::provider_create_with_options( - &ts.endpoint, - "custom-refresh-provider", - "custom-refresh", - false, - &[], - false, - true, - &[], - "default", - "default", - &ts.tls, - ) + run::provider_create_with_options(run::ProviderCreateOptions { + server: &ts.endpoint, + name: "custom-refresh-provider", + provider_type: "custom-refresh", + credentials: &[], + credential_source: run::ProviderCreateCredentialSource::Runtime, + config: &[], + workspace: "default", + profile_workspace: "default", + tls: &ts.tls, + }) .await .expect("provider create"); @@ -2079,16 +2086,17 @@ async fn provider_update_from_existing_uses_profile_discovery_when_v2_enabled() ); let _env = EnvVarGuard::set(&[("CUSTOM_UPDATE_DISCOVERY_API_KEY", "updated-profile-secret")]); - run::provider_update( - &ts.endpoint, - "custom-update", - true, - &[], - &[], - &[], - "default", - &ts.tls, - ) + run::provider_update(run::ProviderUpdateOptions { + server: &ts.endpoint, + name: "custom-update", + from_existing: true, + from_oidc_token: false, + credentials: &[], + config: &[], + credential_expires_at: &[], + workspace: "default", + tls: &ts.tls, + }) .await .expect("profile-backed provider update --from-existing"); @@ -2343,19 +2351,17 @@ async fn provider_create_supports_generic_type_and_env_lookup_credentials() { async fn provider_create_sends_inline_credentials() { let ts = run_server().await; - run::provider_create_with_options( - &ts.endpoint, - "openai-inline", - "openai", - false, - &["OPENAI_API_KEY=sk-test".to_string()], - false, - false, - &[], - "default", - "default", - &ts.tls, - ) + run::provider_create_with_options(run::ProviderCreateOptions { + server: &ts.endpoint, + name: "openai-inline", + provider_type: "openai", + credentials: &["OPENAI_API_KEY=sk-test".to_string()], + credential_source: run::ProviderCreateCredentialSource::ExplicitCredentials, + config: &[], + workspace: "default", + profile_workspace: "default", + tls: &ts.tls, + }) .await .expect("provider create with inline credential"); @@ -2421,7 +2427,7 @@ async fn provider_create_rejects_combined_from_gcloud_adc_and_from_existing() { assert!( err.to_string() - .contains("--from-gcloud-adc cannot be combined with --from-existing, --credential"), + .contains("--from-gcloud-adc cannot be combined with --from-existing, --from-oidc-token, or --credential"), "unexpected error: {err}" ); assert!(ts.state.providers.lock().await.is_empty()); @@ -2447,7 +2453,7 @@ async fn provider_create_rejects_combined_from_gcloud_adc_and_credentials() { assert!( err.to_string() - .contains("--from-gcloud-adc cannot be combined with --from-existing, --credential"), + .contains("--from-gcloud-adc cannot be combined with --from-existing, --from-oidc-token, or --credential"), "unexpected error: {err}" ); assert!(ts.state.providers.lock().await.is_empty()); diff --git a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs index 43e72f64bb..885d50148d 100644 --- a/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs +++ b/crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs @@ -16,7 +16,8 @@ use openshell_core::proto::{ AttachSandboxProviderRequest, AttachSandboxProviderResponse, CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - DetachSandboxProviderRequest, DetachSandboxProviderResponse, ExecSandboxEvent, + DetachSandboxProviderRequest, DetachSandboxProviderResponse, + ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, @@ -262,6 +263,13 @@ impl OpenShell for TestOpenShell { Ok(Response::new(RevokeSshSessionResponse::default())) } + async fn exchange_provider_subject_token( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_provider( &self, _request: tonic::Request, diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index 019b2b12e4..b289ce9795 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -14,7 +14,8 @@ use openshell_core::proto::{ AttachSandboxProviderRequest, AttachSandboxProviderResponse, CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - DetachSandboxProviderRequest, DetachSandboxProviderResponse, ExecSandboxEvent, + DetachSandboxProviderRequest, DetachSandboxProviderResponse, + ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxPolicyStatusRequest, GetSandboxPolicyStatusResponse, @@ -255,6 +256,13 @@ impl OpenShell for TestOpenShell { )) } + async fn exchange_provider_subject_token( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_provider( &self, _request: tonic::Request, diff --git a/crates/openshell-core/Cargo.toml b/crates/openshell-core/Cargo.toml index 586ef63b56..c2a821b548 100644 --- a/crates/openshell-core/Cargo.toml +++ b/crates/openshell-core/Cargo.toml @@ -37,7 +37,9 @@ default = ["telemetry"] ## Compile in anonymous telemetry emission support. On by default; disable with ## `--no-default-features` (plus any other features you need) for a build that ## contains no telemetry endpoint, no HTTP client, and no emission code at all. -telemetry = ["dep:reqwest", "dep:chrono"] +telemetry = ["dep:reqwest", "dep:chrono", "reqwest?/blocking", "reqwest?/rustls-tls-webpki-roots"] +## OAuth2 token request helpers used by supervisor and gateway. +oauth = ["dep:reqwest"] [build-dependencies] tonic-prost-build = { workspace = true } diff --git a/crates/openshell-core/src/driver_utils.rs b/crates/openshell-core/src/driver_utils.rs index 79f2eed3aa..181f594d8e 100644 --- a/crates/openshell-core/src/driver_utils.rs +++ b/crates/openshell-core/src/driver_utils.rs @@ -345,6 +345,9 @@ pub fn read_upstream_proxy_credential_file(path: &str) -> Result Ok(buf) } +/// Container-side directory where the provider SPIFFE Workload API socket is mounted. +pub const PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR: &str = "/spiffe-workload-api"; + /// Return the XDG state path for a driver's sandbox JWT token file. /// /// The resulting path is `$XDG_STATE_HOME/openshell/[/]//sandbox.jwt`. diff --git a/crates/openshell-core/src/grpc_client.rs b/crates/openshell-core/src/grpc_client.rs index 7921b0716b..73af8039d6 100644 --- a/crates/openshell-core/src/grpc_client.rs +++ b/crates/openshell-core/src/grpc_client.rs @@ -23,12 +23,12 @@ use std::sync::{Arc, OnceLock, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use crate::proto::{ - DenialSummary, GetDraftPolicyRequest, GetInferenceBundleRequest, GetInferenceBundleResponse, - GetSandboxConfigRequest, GetSandboxProviderEnvironmentRequest, IssueSandboxTokenRequest, - NetworkActivitySummary, PolicyChunk, PolicySource, PolicyStatus, RefreshSandboxTokenRequest, - ReportPolicyStatusRequest, SandboxPolicy as ProtoSandboxPolicy, SubmitPolicyAnalysisRequest, - SubmitPolicyAnalysisResponse, UpdateConfigRequest, inference_client::InferenceClient, - open_shell_client::OpenShellClient, + DenialSummary, ExchangeProviderSubjectTokenRequest, GetDraftPolicyRequest, + GetInferenceBundleRequest, GetInferenceBundleResponse, GetSandboxConfigRequest, + GetSandboxProviderEnvironmentRequest, IssueSandboxTokenRequest, NetworkActivitySummary, + PolicyChunk, PolicySource, PolicyStatus, RefreshSandboxTokenRequest, ReportPolicyStatusRequest, + SandboxPolicy as ProtoSandboxPolicy, SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, + UpdateConfigRequest, inference_client::InferenceClient, open_shell_client::OpenShellClient, }; use crate::sandbox_env; use miette::{IntoDiagnostic, Result, WrapErr}; @@ -757,6 +757,55 @@ pub async fn fetch_provider_environment( }) } +pub async fn exchange_provider_subject_token( + endpoint: &str, + sandbox_id: &str, + provider: &str, + credential_key: &str, + supervisor_jwt_svid: &str, +) -> Result { + debug!( + endpoint = %endpoint, + sandbox_id = %sandbox_id, + provider = %provider, + credential_key = %credential_key, + "Exchanging provider subject token through gateway" + ); + + let mut client = connect(endpoint).await?; + let response = client + .exchange_provider_subject_token(ExchangeProviderSubjectTokenRequest { + sandbox_id: sandbox_id.to_string(), + provider: provider.to_string(), + credential_key: credential_key.to_string(), + supervisor_jwt_svid: supervisor_jwt_svid.to_string(), + }) + .await + .map_err(provider_subject_token_exchange_status)?; + let inner = response.into_inner(); + Ok(ProviderSubjectTokenExchangeResult { + access_token: inner.access_token, + expires_in: inner.expires_in, + token_type: inner.token_type, + }) +} + +fn provider_subject_token_exchange_status(status: Status) -> miette::Report { + let message = status.message(); + if message.is_empty() { + miette::miette!( + "gateway ExchangeProviderSubjectToken failed with status {}", + status.code() + ) + } else { + miette::miette!( + "gateway ExchangeProviderSubjectToken failed with status {}: {}", + status.code(), + message + ) + } +} + /// A reusable gRPC client for the `OpenShell` service. /// /// Wraps a tonic channel connected once and reused for policy polling @@ -847,6 +896,12 @@ pub struct ProviderEnvironmentResult { pub non_secret_environment_keys: Vec, } +pub struct ProviderSubjectTokenExchangeResult { + pub access_token: String, + pub expires_in: i64, + pub token_type: String, +} + impl CachedOpenShellClient { pub async fn connect(endpoint: &str) -> Result { debug!(endpoint = %endpoint, "Connecting openshell gRPC client for policy polling"); diff --git a/crates/openshell-core/src/lib.rs b/crates/openshell-core/src/lib.rs index d373d656ed..2bd44cb67f 100644 --- a/crates/openshell-core/src/lib.rs +++ b/crates/openshell-core/src/lib.rs @@ -29,6 +29,8 @@ pub mod jwt; pub mod metadata; pub mod middleware; pub mod net; +#[cfg(feature = "oauth")] +pub mod oauth; pub mod paths; pub mod policy; pub mod progress; @@ -39,6 +41,7 @@ pub mod provider_credentials; pub mod sandbox_env; pub mod secrets; pub mod settings; +pub mod spiffe; pub mod telemetry; pub mod time; pub mod transport_errors; diff --git a/crates/openshell-core/src/oauth.rs b/crates/openshell-core/src/oauth.rs new file mode 100644 index 0000000000..c68ecfa6b4 --- /dev/null +++ b/crates/openshell-core/src/oauth.rs @@ -0,0 +1,766 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Shared `OAuth2` token request helpers. +//! +//! Provides [`post_oauth_token_grant`] (client credentials) and +//! [`post_oauth_token_exchange`] (RFC 8693 token exchange) — typed functions +//! that assemble form parameters, POST to a validated `OAuth2` token endpoint, +//! parse the JSON response, and validate the returned access token. + +use std::net::IpAddr; + +use miette::{IntoDiagnostic, Result, WrapErr}; +use serde::Deserialize; + +pub const DEFAULT_CLIENT_ASSERTION_TYPE: &str = + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; +pub const ACCESS_TOKEN_TYPE: &str = "urn:ietf:params:oauth:token-type:access_token"; +const TOKEN_EXCHANGE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:token-exchange"; + +const MAX_OAUTH_ERROR_FIELD_LEN: usize = 256; + +/// `OAuth2` token response. +#[derive(Debug, Clone)] +pub struct OAuthTokenResponse { + pub access_token: String, + pub expires_in: i64, + pub token_type: String, +} + +#[derive(Debug, Deserialize)] +struct RawTokenResponse { + access_token: String, + #[serde(default)] + expires_in: i64, + #[serde(default)] + token_type: String, +} + +#[derive(Debug, Deserialize)] +struct OAuthErrorResponse { + error: Option, + error_description: Option, +} + +async fn post_oauth_token_request( + client: &reqwest::Client, + token_endpoint: &str, + mut form_params: Vec<(&str, &str)>, + audience: &str, + scopes: &[String], +) -> Result { + let token_endpoint_url = parse_token_endpoint_url(token_endpoint)?; + + let audience_param; + if !audience.is_empty() { + audience_param = audience.to_string(); + form_params.push(("audience", &audience_param)); + } + + let scope_param; + if !scopes.is_empty() { + scope_param = scopes.join(" "); + form_params.push(("scope", &scope_param)); + } + + let response = client + .post(token_endpoint_url) + .form(&form_params) + .send() + .await + .into_diagnostic() + .wrap_err_with(|| format!("failed to POST to token endpoint {token_endpoint}"))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response + .text() + .await + .unwrap_or_else(|_| "".to_string()); + return Err(miette::miette!("{}", failure_message(status, &body))); + } + + let raw = response + .json::() + .await + .into_diagnostic() + .wrap_err("failed to parse token response as JSON")?; + validate_access_token(&raw.access_token)?; + Ok(OAuthTokenResponse { + access_token: raw.access_token, + expires_in: raw.expires_in, + token_type: raw.token_type, + }) +} + +/// Client credentials grant form fields. +pub struct TokenGrantParams<'a> { + pub client_assertion: &'a str, + pub client_assertion_type: &'a str, + pub audience: &'a str, + pub scopes: &'a [String], +} + +/// POST a client credentials grant request to an `OAuth2` token endpoint. +/// +/// Applies the default for `client_assertion_type` and conditionally includes +/// `audience` and `scope`. +pub async fn post_oauth_token_grant( + client: &reqwest::Client, + token_endpoint: &str, + params: &TokenGrantParams<'_>, +) -> Result { + let client_assertion_type = effective_client_assertion_type(params.client_assertion_type); + let form_params = vec![ + ("grant_type", "client_credentials"), + ("client_assertion_type", client_assertion_type), + ("client_assertion", params.client_assertion), + ]; + post_oauth_token_request( + client, + token_endpoint, + form_params, + params.audience, + params.scopes, + ) + .await +} + +/// RFC 8693 token-exchange form fields. +pub struct TokenExchangeParams<'a> { + pub client_assertion: &'a str, + pub client_assertion_type: &'a str, + pub subject_token: &'a str, + pub subject_token_type: &'a str, + pub audience: &'a str, + pub scopes: &'a [String], + pub requested_token_type: &'a str, +} + +/// POST an RFC 8693 token-exchange request to an `OAuth2` token endpoint. +/// +/// Assembles the standard token-exchange form fields, applies defaults for +/// `client_assertion_type`, `subject_token_type`, and `requested_token_type`, +/// and conditionally includes `audience` and `scope`. +pub async fn post_oauth_token_exchange( + client: &reqwest::Client, + token_endpoint: &str, + params: &TokenExchangeParams<'_>, +) -> Result { + let client_assertion_type = effective_client_assertion_type(params.client_assertion_type); + let subject_token_type = effective_token_type(params.subject_token_type); + let requested_token_type = effective_token_type(params.requested_token_type); + let form_params = vec![ + ("grant_type", TOKEN_EXCHANGE_GRANT_TYPE), + ("client_assertion_type", client_assertion_type), + ("client_assertion", params.client_assertion), + ("subject_token", params.subject_token), + ("subject_token_type", subject_token_type), + ("requested_token_type", requested_token_type), + ]; + post_oauth_token_request( + client, + token_endpoint, + form_params, + params.audience, + params.scopes, + ) + .await +} + +pub fn effective_client_assertion_type(client_assertion_type: &str) -> &str { + if client_assertion_type.trim().is_empty() { + DEFAULT_CLIENT_ASSERTION_TYPE + } else { + client_assertion_type + } +} + +pub fn effective_token_type(token_type: &str) -> &str { + if token_type.trim().is_empty() { + ACCESS_TOKEN_TYPE + } else { + token_type + } +} + +fn parse_token_endpoint_url(token_endpoint: &str) -> Result { + let url = reqwest::Url::parse(token_endpoint) + .into_diagnostic() + .wrap_err("token_endpoint must be an absolute URL")?; + if token_endpoint_transport_allowed(&url) { + return Ok(url); + } + Err(miette::miette!( + "token_endpoint must use https, except http for loopback or in-cluster service hosts" + )) +} + +fn token_endpoint_transport_allowed(url: &reqwest::Url) -> bool { + match url.scheme() { + "https" => true, + "http" => url + .host_str() + .is_some_and(|host| is_loopback_host(host) || is_kubernetes_service_host(host)), + _ => false, + } +} + +fn is_loopback_host(host: &str) -> bool { + let host = host.trim_matches(['[', ']']); + if host.eq_ignore_ascii_case("localhost") { + return true; + } + match host.parse::() { + Ok(IpAddr::V4(v4)) => v4.is_loopback(), + Ok(IpAddr::V6(v6)) => { + v6.is_loopback() || v6.to_ipv4_mapped().is_some_and(|v4| v4.is_loopback()) + } + Err(_) => false, + } +} + +fn is_kubernetes_service_host(host: &str) -> bool { + let host = host.trim_end_matches('.').to_ascii_lowercase(); + let labels = host.split('.').collect::>(); + let is_service_name = labels.len() == 3 && labels[2] == "svc"; + let is_cluster_local_service = + labels.len() == 5 && labels[2] == "svc" && labels[3] == "cluster" && labels[4] == "local"; + (is_service_name || is_cluster_local_service) && labels.iter().all(|label| !label.is_empty()) +} + +pub fn validate_access_token(token: &str) -> Result<()> { + if token.is_empty() || !is_token68(token) { + return Err(miette::miette!( + "token grant returned a malformed access token" + )); + } + Ok(()) +} + +fn is_token68(token: &str) -> bool { + let mut padding_started = false; + let mut saw_value = false; + for byte in token.bytes() { + if byte == b'=' { + padding_started = true; + continue; + } + if padding_started || !is_token68_value_byte(byte) { + return false; + } + saw_value = true; + } + saw_value +} + +fn is_token68_value_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'+' | b'/') +} + +fn failure_message(status: reqwest::StatusCode, body: &str) -> String { + let Ok(error_response) = serde_json::from_str::(body) else { + return format!("token grant failed with status {status}"); + }; + let error = error_response + .error + .as_deref() + .map(sanitize_oauth_error_field) + .filter(|value| !value.is_empty()); + let description = error_response + .error_description + .as_deref() + .map(sanitize_oauth_error_field) + .filter(|value| !value.is_empty()); + match (error, description) { + (Some(error), Some(description)) => { + format!( + "token grant failed with status {status}: error={error}; error_description={description}" + ) + } + (Some(error), None) => { + format!("token grant failed with status {status}: error={error}") + } + (None, Some(description)) => { + format!("token grant failed with status {status}: error_description={description}") + } + (None, None) => format!("token grant failed with status {status}"), + } +} + +fn sanitize_oauth_error_field(value: &str) -> String { + value + .chars() + .map(|ch| if ch.is_control() { ' ' } else { ch }) + .take(MAX_OAUTH_ERROR_FIELD_LEN) + .collect::() + .trim() + .to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpListener; + + #[derive(Debug)] + struct CapturedRequest { + form: HashMap, + } + + fn test_client() -> reqwest::Client { + reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .no_proxy() + .redirect(reqwest::redirect::Policy::none()) + .build() + .expect("test client") + } + + async fn token_endpoint_once( + status: &str, + body: &str, + ) -> (String, tokio::task::JoinHandle) { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind token endpoint"); + let addr = listener.local_addr().expect("token endpoint local addr"); + let status = status.to_string(); + let body = body.to_string(); + let handle = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.expect("accept"); + let mut buf = Vec::new(); + let mut chunk = [0u8; 512]; + let mut expected_len = None; + loop { + let n = stream.read(&mut chunk).await.expect("read"); + assert!(n > 0); + buf.extend_from_slice(&chunk[..n]); + if expected_len.is_none() + && let Some(header_end) = header_end(&buf) + { + let headers = String::from_utf8_lossy(&buf[..header_end]); + let cl = headers + .lines() + .find_map(|line| { + let (name, value) = line.split_once(':')?; + name.eq_ignore_ascii_case("content-length") + .then(|| value.trim().parse::().ok()) + .flatten() + }) + .unwrap_or(0); + expected_len = Some(header_end + cl); + } + if expected_len.is_some_and(|len| buf.len() >= len) { + break; + } + } + let header_end = header_end(&buf).unwrap(); + let form_body = String::from_utf8_lossy(&buf[header_end..]).to_string(); + let form = parse_form_body(&form_body); + let response = format!( + "HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len(), + ); + stream.write_all(response.as_bytes()).await.expect("write"); + CapturedRequest { form } + }); + (format!("http://{addr}/token"), handle) + } + + async fn token_endpoint_redirect_once(location: &str) -> (String, tokio::task::JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind token endpoint"); + let addr = listener.local_addr().expect("token endpoint local addr"); + let location = location.to_string(); + let handle = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.expect("accept"); + let mut buf = vec![0u8; 512]; + let _ = stream.read(&mut buf).await; + let response = format!( + "HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + ); + stream.write_all(response.as_bytes()).await.expect("write"); + }); + (format!("http://{addr}/token"), handle) + } + + fn header_end(buf: &[u8]) -> Option { + buf.windows(4) + .position(|w| w == b"\r\n\r\n") + .map(|idx| idx + 4) + } + + fn parse_form_body(body: &str) -> HashMap { + body.split('&') + .filter(|part| !part.is_empty()) + .filter_map(|part| { + let (name, value) = part.split_once('=')?; + Some((decode_form_component(name), decode_form_component(value))) + }) + .collect() + } + + fn decode_form_component(value: &str) -> String { + let bytes = value.as_bytes(); + let mut decoded = Vec::with_capacity(bytes.len()); + let mut idx = 0; + while idx < bytes.len() { + match bytes[idx] { + b'+' => { + decoded.push(b' '); + idx += 1; + } + b'%' if idx + 2 < bytes.len() => { + let hex = &value[idx + 1..idx + 3]; + if let Ok(byte) = u8::from_str_radix(hex, 16) { + decoded.push(byte); + idx += 3; + } else { + decoded.push(bytes[idx]); + idx += 1; + } + } + byte => { + decoded.push(byte); + idx += 1; + } + } + } + String::from_utf8(decoded).expect("form body should be UTF-8") + } + + fn empty_grant_params() -> TokenGrantParams<'static> { + TokenGrantParams { + client_assertion: "jwt-svid-token", + client_assertion_type: "", + audience: "", + scopes: &[], + } + } + + #[tokio::test] + async fn posts_form_params_and_parses_success_response() { + let (endpoint, request) = token_endpoint_once( + "200 OK", + r#"{"access_token":"access-123","token_type":"Bearer","expires_in":42}"#, + ) + .await; + let client = test_client(); + + let response = post_oauth_token_grant(&client, &endpoint, &empty_grant_params()) + .await + .expect("should succeed"); + let request = request.await.expect("endpoint task"); + + assert_eq!(response.access_token, "access-123"); + assert_eq!(response.expires_in, 42); + assert_eq!(response.token_type, "Bearer"); + assert_eq!( + request.form.get("grant_type").map(String::as_str), + Some("client_credentials") + ); + assert_eq!( + request.form.get("client_assertion").map(String::as_str), + Some("jwt-svid-token") + ); + } + + #[tokio::test] + async fn rejects_malformed_access_token() { + let (endpoint, request) = token_endpoint_once( + "200 OK", + r#"{"access_token":"access-123\r\nX-Injected: yes"}"#, + ) + .await; + let client = test_client(); + + let err = post_oauth_token_grant(&client, &endpoint, &empty_grant_params()) + .await + .expect_err("malformed token should fail"); + let _ = request.await; + + assert_eq!( + err.to_string(), + "token grant returned a malformed access token" + ); + } + + #[tokio::test] + async fn does_not_follow_redirects() { + let (endpoint, handle) = token_endpoint_redirect_once("http://127.0.0.1:1/stolen").await; + let client = test_client(); + + let err = post_oauth_token_grant(&client, &endpoint, &empty_grant_params()) + .await + .expect_err("redirect should fail"); + let _ = handle.await; + + assert_eq!(err.to_string(), "token grant failed with status 302 Found"); + } + + #[tokio::test] + async fn reports_sanitized_oauth_error() { + let (endpoint, request) = token_endpoint_once( + "401 Unauthorized", + r#"{"error":"invalid_client","error_description":"bad assertion"}"#, + ) + .await; + let client = test_client(); + + let err = post_oauth_token_grant(&client, &endpoint, &empty_grant_params()) + .await + .expect_err("should fail on OAuth error"); + let _ = request.await; + + assert_eq!( + err.to_string(), + "token grant failed with status 401 Unauthorized: error=invalid_client; error_description=bad assertion" + ); + } + + #[tokio::test] + async fn does_not_echo_unstructured_error_body() { + let (endpoint, request) = token_endpoint_once( + "500 Internal Server Error", + "internal stack trace with implementation details", + ) + .await; + let client = test_client(); + + let err = post_oauth_token_grant(&client, &endpoint, &empty_grant_params()) + .await + .expect_err("should fail on server error"); + let _ = request.await; + let message = err.to_string(); + + assert_eq!( + message, + "token grant failed with status 500 Internal Server Error" + ); + assert!(!message.contains("stack trace")); + } + + #[tokio::test] + async fn reports_malformed_success_json() { + let (endpoint, request) = token_endpoint_once("200 OK", r#"{"access_token":42"#).await; + let client = test_client(); + + let err = post_oauth_token_grant(&client, &endpoint, &empty_grant_params()) + .await + .expect_err("should fail on malformed JSON"); + let _ = request.await; + + assert!( + err.to_string() + .contains("failed to parse token response as JSON") + ); + } + + #[tokio::test] + async fn token_exchange_posts_rfc8693_form_fields() { + let (endpoint, request) = token_endpoint_once( + "200 OK", + r#"{"access_token":"exchanged-token","token_type":"Bearer","expires_in":60}"#, + ) + .await; + let client = test_client(); + let scopes = vec!["read".to_string(), "write".to_string()]; + + let response = post_oauth_token_exchange( + &client, + &endpoint, + &TokenExchangeParams { + client_assertion: "jwt-svid-token", + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe", + subject_token: "subject-access-token", + subject_token_type: ACCESS_TOKEN_TYPE, + audience: "api://resource", + scopes: &scopes, + requested_token_type: "urn:ietf:params:oauth:token-type:id_token", + }, + ) + .await + .expect("token exchange should succeed"); + let request = request.await.expect("endpoint task"); + + assert_eq!(response.access_token, "exchanged-token"); + assert_eq!(response.expires_in, 60); + assert_eq!( + request.form.get("grant_type").map(String::as_str), + Some(TOKEN_EXCHANGE_GRANT_TYPE) + ); + assert_eq!( + request + .form + .get("client_assertion_type") + .map(String::as_str), + Some("urn:ietf:params:oauth:client-assertion-type:jwt-spiffe") + ); + assert_eq!( + request.form.get("client_assertion").map(String::as_str), + Some("jwt-svid-token") + ); + assert_eq!( + request.form.get("subject_token").map(String::as_str), + Some("subject-access-token") + ); + assert_eq!( + request.form.get("subject_token_type").map(String::as_str), + Some(ACCESS_TOKEN_TYPE) + ); + assert_eq!( + request.form.get("audience").map(String::as_str), + Some("api://resource") + ); + assert_eq!( + request.form.get("scope").map(String::as_str), + Some("read write") + ); + assert_eq!( + request.form.get("requested_token_type").map(String::as_str), + Some("urn:ietf:params:oauth:token-type:id_token") + ); + } + + #[tokio::test] + async fn token_exchange_applies_defaults_for_empty_type_fields() { + let (endpoint, request) = + token_endpoint_once("200 OK", r#"{"access_token":"exchanged-token"}"#).await; + let client = test_client(); + + post_oauth_token_exchange( + &client, + &endpoint, + &TokenExchangeParams { + client_assertion: "jwt-svid-token", + client_assertion_type: "", + subject_token: "subject-token", + subject_token_type: "", + audience: "", + scopes: &[], + requested_token_type: "", + }, + ) + .await + .expect("token exchange should succeed"); + let request = request.await.expect("endpoint task"); + + assert_eq!( + request + .form + .get("client_assertion_type") + .map(String::as_str), + Some(DEFAULT_CLIENT_ASSERTION_TYPE) + ); + assert_eq!( + request.form.get("subject_token_type").map(String::as_str), + Some(ACCESS_TOKEN_TYPE) + ); + assert_eq!( + request.form.get("requested_token_type").map(String::as_str), + Some(ACCESS_TOKEN_TYPE) + ); + assert!(!request.form.contains_key("audience")); + assert!(!request.form.contains_key("scope")); + } + + #[test] + fn token_endpoint_url_allows_https_loopback_and_in_cluster_http() { + for endpoint in [ + "https://auth.example.com/token", + "http://127.0.0.1:8080/token", + "http://[::1]:8080/token", + "http://token-issuer.default.svc.cluster.local/token", + "http://token-issuer.default.svc/token", + ] { + parse_token_endpoint_url(endpoint).expect("should be allowed"); + } + } + + #[test] + fn token_endpoint_url_rejects_plain_http_non_cluster_hosts() { + for endpoint in [ + "http://auth.example.com/token", + "http://keycloak/realms/openshell/protocol/openid-connect/token", + "http://token-issuer.default.svc.evil.com/token", + "ftp://auth.example.com/token", + "/relative/token", + ] { + assert!( + parse_token_endpoint_url(endpoint).is_err(), + "should be rejected: {endpoint}" + ); + } + } + + #[test] + fn validate_access_token_accepts_token68_values() { + for token in [ + "abcXYZ123-._~+/", + "eyJhbGciOiJSUzI1NiJ9.payload.sig", + "token==", + ] { + validate_access_token(token).expect("should be accepted"); + } + } + + #[test] + fn validate_access_token_rejects_non_token68_values() { + for token in [ + "", + "token with spaces", + "token\r\nX-Injected: yes", + "token\u{7f}", + "tokené", + "token=continued", + "==", + ] { + let err = validate_access_token(token).expect_err("should be rejected"); + assert_eq!( + err.to_string(), + "token grant returned a malformed access token" + ); + } + } + + #[test] + fn failure_message_reports_oauth_error_fields() { + let message = failure_message( + reqwest::StatusCode::UNAUTHORIZED, + r#"{"error":"invalid_client","error_description":"Invalid client credentials"}"#, + ); + assert_eq!( + message, + "token grant failed with status 401 Unauthorized: error=invalid_client; error_description=Invalid client credentials" + ); + } + + #[test] + fn failure_message_omits_unstructured_response_body() { + let message = failure_message( + reqwest::StatusCode::INTERNAL_SERVER_ERROR, + "internal error containing implementation details", + ); + assert_eq!( + message, + "token grant failed with status 500 Internal Server Error" + ); + } + + #[test] + fn failure_message_sanitizes_oauth_error_fields() { + let long_description = "a".repeat(MAX_OAUTH_ERROR_FIELD_LEN + 20); + let body = + format!(r#"{{"error":"invalid_client\n","error_description":"{long_description}"}}"#); + let message = failure_message(reqwest::StatusCode::UNAUTHORIZED, &body); + assert!(!message.contains('\n')); + assert!(message.contains("error=invalid_client")); + assert!(message.contains(&"a".repeat(MAX_OAUTH_ERROR_FIELD_LEN))); + assert!(!message.contains(&"a".repeat(MAX_OAUTH_ERROR_FIELD_LEN + 1))); + } +} diff --git a/crates/openshell-core/src/spiffe.rs b/crates/openshell-core/src/spiffe.rs new file mode 100644 index 0000000000..c288dfd81a --- /dev/null +++ b/crates/openshell-core/src/spiffe.rs @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Shared SPIFFE helpers used by the gateway and sandbox supervisor. + +use std::path::Path; + +use base64::Engine as _; +use serde::Deserialize; + +/// SPIFFE JWT-SVID claims used by `OpenShell` token exchange flows. +#[derive(Debug, Clone, Deserialize)] +pub struct SpiffeJwtClaims { + pub iss: String, + pub sub: String, + pub aud: AudienceClaim, + #[serde(default)] + pub exp: i64, +} + +/// JWT `aud` claim representation accepted by SPIFFE JWT-SVIDs. +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +pub enum AudienceClaim { + One(String), + Many(Vec), +} + +impl AudienceClaim { + pub fn contains(&self, expected: &str) -> bool { + match self { + Self::One(value) => value == expected, + Self::Many(values) => values.iter().any(|value| value == expected), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum JwtSvidParseError { + #[error("invalid JWT-SVID format")] + Format, + #[error("invalid JWT-SVID payload encoding")] + PayloadEncoding, + #[error("invalid JWT-SVID payload")] + Payload, +} + +/// Convert a path to a SPIFFE Workload API endpoint URL. +/// +/// If the path already has a scheme (`unix:` or `tcp:`), use it as-is. +/// Otherwise, assume it is a Unix socket path and prepend `unix:`. +pub fn workload_api_endpoint(path: &Path) -> String { + let path = path.to_string_lossy(); + if path.starts_with("unix:") || path.starts_with("tcp:") { + path.into_owned() + } else { + format!("unix:{path}") + } +} + +/// # Security +/// +/// This function decodes the JWT payload without verifying the signature. +/// Callers must separately verify the JWT signature before trusting the claims. +pub fn parse_unverified_jwt_svid_claims(token: &str) -> Result { + let segments = token.split('.').collect::>(); + if segments.len() != 3 || segments.iter().any(|segment| segment.is_empty()) { + return Err(JwtSvidParseError::Format); + } + let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(segments[1]) + .map_err(|_| JwtSvidParseError::PayloadEncoding)?; + serde_json::from_slice::(&decoded).map_err(|_| JwtSvidParseError::Payload) +} + +pub fn trust_domain(subject: &str) -> Option<&str> { + let rest = subject.strip_prefix("spiffe://")?; + let (trust_domain, _) = rest.split_once('/').unwrap_or((rest, "")); + (!trust_domain.is_empty()).then_some(trust_domain) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn unsigned_svid_fixture(issuer: &str, subject: &str, audience: serde_json::Value) -> String { + let header = serde_json::json!({ "alg": "RS256", "kid": "test-key" }); + let payload = serde_json::json!({ + "iss": issuer, + "sub": subject, + "aud": audience, + "exp": 4_102_444_800_i64 + }); + let encoded_header = base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(serde_json::to_vec(&header).expect("serialize header")); + let encoded_payload = base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(serde_json::to_vec(&payload).expect("serialize payload")); + format!("{encoded_header}.{encoded_payload}.signature") + } + + #[test] + fn workload_api_endpoint_preserves_explicit_scheme() { + assert_eq!( + workload_api_endpoint(Path::new("unix:/run/spire/agent.sock")), + "unix:/run/spire/agent.sock" + ); + assert_eq!( + workload_api_endpoint(Path::new("tcp:127.0.0.1:8081")), + "tcp:127.0.0.1:8081" + ); + } + + #[test] + fn workload_api_endpoint_defaults_to_unix_socket() { + assert_eq!( + workload_api_endpoint(Path::new("/run/spire/agent.sock")), + "unix:/run/spire/agent.sock" + ); + } + + #[test] + fn parse_unverified_jwt_svid_claims_accepts_string_audience() { + let token = unsigned_svid_fixture( + "https://spiffe.example.test", + "spiffe://openshell/openshell/sandbox/sb-a", + serde_json::json!("https://auth.example.com"), + ); + + let claims = parse_unverified_jwt_svid_claims(&token).expect("valid claims"); + + assert_eq!(claims.iss, "https://spiffe.example.test"); + assert_eq!(claims.sub, "spiffe://openshell/openshell/sandbox/sb-a"); + assert!(claims.aud.contains("https://auth.example.com")); + assert!(!claims.aud.contains("https://other.example.com")); + } + + #[test] + fn parse_unverified_jwt_svid_claims_accepts_array_audience() { + let token = unsigned_svid_fixture( + "https://spiffe.example.test", + "spiffe://openshell/openshell/sandbox/sb-a", + serde_json::json!(["https://auth.example.com", "https://other.example.com"]), + ); + + let claims = parse_unverified_jwt_svid_claims(&token).expect("valid claims"); + + assert!(claims.aud.contains("https://auth.example.com")); + assert!(claims.aud.contains("https://other.example.com")); + } + + #[test] + fn parse_unverified_jwt_svid_claims_rejects_truncated_jwt() { + assert!(matches!( + parse_unverified_jwt_svid_claims("header.payload"), + Err(JwtSvidParseError::Format) + )); + } + + #[test] + fn parse_unverified_jwt_svid_claims_rejects_empty_jwt_segments() { + assert!(matches!( + parse_unverified_jwt_svid_claims("header..signature"), + Err(JwtSvidParseError::Format) + )); + } + + #[test] + fn trust_domain_extracts_domain_from_spiffe_id() { + assert_eq!( + trust_domain("spiffe://openshell/openshell/sandbox/sb-a"), + Some("openshell") + ); + assert_eq!(trust_domain("spiffe://openshell"), Some("openshell")); + assert_eq!(trust_domain("not-a-spiffe-id"), None); + assert_eq!(trust_domain("spiffe:///empty"), None); + } +} diff --git a/crates/openshell-driver-podman/src/config.rs b/crates/openshell-driver-podman/src/config.rs index 50311836ce..b4980f2e40 100644 --- a/crates/openshell-driver-podman/src/config.rs +++ b/crates/openshell-driver-podman/src/config.rs @@ -126,6 +126,9 @@ pub struct PodmanComputeConfig { /// `template.driver_config`. #[serde(default)] pub enable_bind_mounts: bool, + /// Host path to a SPIFFE Workload API Unix socket exposed to sandbox + /// supervisors for provider token exchange client assertions. + pub provider_spiffe_workload_api_socket: Option, /// Health check interval in seconds for sandbox containers. /// /// Podman runs the health check command at this interval to determine @@ -374,6 +377,7 @@ impl Default for PodmanComputeConfig { guest_tls_key: None, sandbox_pids_limit: DEFAULT_SANDBOX_PIDS_LIMIT, enable_bind_mounts: false, + provider_spiffe_workload_api_socket: None, health_check_interval_secs: DEFAULT_HEALTH_CHECK_INTERVAL_SECS, https_proxy: None, no_proxy: None, @@ -402,6 +406,10 @@ impl std::fmt::Debug for PodmanComputeConfig { .field("guest_tls_key", &self.guest_tls_key) .field("sandbox_pids_limit", &self.sandbox_pids_limit) .field("enable_bind_mounts", &self.enable_bind_mounts) + .field( + "provider_spiffe_workload_api_socket", + &self.provider_spiffe_workload_api_socket, + ) .field( "health_check_interval_secs", &self.health_check_interval_secs, diff --git a/crates/openshell-driver-podman/src/container.rs b/crates/openshell-driver-podman/src/container.rs index 005f688a19..e2d3e5f31c 100644 --- a/crates/openshell-driver-podman/src/container.rs +++ b/crates/openshell-driver-podman/src/container.rs @@ -62,6 +62,8 @@ const TLS_KEY_MOUNT_PATH: &str = openshell_core::driver_utils::TLS_KEY_MOUNT_PAT const SANDBOX_TOKEN_MOUNT_PATH: &str = openshell_core::driver_utils::SANDBOX_TOKEN_MOUNT_PATH; const UPSTREAM_PROXY_AUTH_MOUNT_PATH: &str = openshell_core::driver_utils::UPSTREAM_PROXY_AUTH_MOUNT_PATH; +const PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR: &str = + openshell_core::driver_utils::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR; /// Directory inside sandbox containers where the supervisor binary is mounted. const SUPERVISOR_MOUNT_DIR: &str = openshell_core::driver_utils::SUPERVISOR_CONTAINER_DIR; @@ -481,6 +483,13 @@ fn build_env( ); } + if let Some(socket_path) = provider_spiffe_workload_api_socket_env_value(config) { + env.insert( + openshell_core::sandbox_env::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET.into(), + socket_path, + ); + } + env.remove(openshell_core::sandbox_env::SANDBOX_TOKEN); env.remove(openshell_core::sandbox_env::SANDBOX_TOKEN_FILE); env.insert( @@ -1172,6 +1181,17 @@ pub fn build_container_spec_for_image( options: ro, }); } + if let Some(path) = provider_spiffe_workload_api_socket_mount_source(config) { + // No SELinux relabel — the SPIRE agent socket is shared host + // infrastructure and must keep its existing SELinux context. + let ro = vec!["ro".into(), "rbind".into()]; + m.push(Mount { + kind: "bind".into(), + source: path.display().to_string(), + destination: PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR.into(), + options: ro, + }); + } m.extend(user_mounts.mounts); m }, @@ -1188,6 +1208,33 @@ pub fn build_container_spec_for_image( Ok(serde_json::to_value(container_spec).expect("ContainerSpec serialization cannot fail")) } +fn provider_spiffe_workload_api_socket_env_value(config: &PodmanComputeConfig) -> Option { + let host_path = config.provider_spiffe_workload_api_socket.as_ref()?; + let raw = host_path.to_str()?; + if raw.starts_with("tcp:") { + return Some(raw.to_string()); + } + let host_path = raw + .strip_prefix("unix:") + .map_or(host_path.as_path(), Path::new); + let file_name = host_path.file_name()?.to_str()?; + Some(format!( + "{PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR}/{file_name}" + )) +} + +fn provider_spiffe_workload_api_socket_mount_source(config: &PodmanComputeConfig) -> Option<&Path> { + let host_path = config.provider_spiffe_workload_api_socket.as_ref()?; + let raw = host_path.to_str()?; + if raw.starts_with("tcp:") { + return None; + } + let host_path = raw + .strip_prefix("unix:") + .map_or(host_path.as_path(), Path::new); + host_path.parent() +} + fn hostadd_entries(config: &PodmanComputeConfig) -> Vec { let host_gateway_ip = config.host_gateway_ip.trim(); if host_gateway_ip.is_empty() { @@ -2765,6 +2812,33 @@ mod tests { ); } + #[test] + fn container_spec_includes_provider_spiffe_socket_when_configured() { + let sandbox = test_sandbox("spiffe-id", "spiffe-name"); + let mut config = test_config(); + config.provider_spiffe_workload_api_socket = + Some(std::path::PathBuf::from("/host/spire-agent.sock")); + + let spec = build_container_spec(&sandbox, &config); + + let env_map = spec["env"].as_object().expect("env should be an object"); + assert_eq!( + env_map + .get(openshell_core::sandbox_env::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET) + .and_then(|v| v.as_str()), + Some("/spiffe-workload-api/spire-agent.sock"), + ); + + let mounts = spec["mounts"] + .as_array() + .expect("mounts should be an array"); + assert!(mounts.iter().any(|m| { + m["type"].as_str() == Some("bind") + && m["source"].as_str() == Some("/host") + && m["destination"].as_str() == Some(PROVIDER_SPIFFE_WORKLOAD_API_SOCKET_MOUNT_DIR) + })); + } + #[test] fn container_spec_omits_tls_without_config() { let sandbox = test_sandbox("notls-id", "notls-name"); diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index 42c82c0c2a..cf7c2faa35 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -9,7 +9,8 @@ use openshell_core::proto::{ GraphqlOperation, L7Allow, L7DenyRule, L7QueryMatcher, L7Rule, McpOptions, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, ProviderCredentialRefresh, ProviderCredentialRefreshMaterial, ProviderCredentialRefreshOutput, - ProviderCredentialRefreshStrategy, ProviderProfile, ProviderProfileCategory, + ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantSubjectToken, + ProviderCredentialTokenGrantType, ProviderProfile, ProviderProfileCategory, ProviderProfileCredential, ProviderProfileDiscovery, }; use openshell_core::secrets::uses_reserved_revision_namespace; @@ -111,6 +112,13 @@ pub struct CredentialProfile { #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct TokenGrantProfile { + #[serde( + default = "default_token_grant_type", + deserialize_with = "deserialize_token_grant_type", + serialize_with = "serialize_token_grant_type", + skip_serializing_if = "is_client_credentials_grant" + )] + pub grant_type: ProviderCredentialTokenGrantType, pub token_endpoint: String, #[serde(default, skip_serializing_if = "String::is_empty")] pub audience: String, @@ -124,6 +132,18 @@ pub struct TokenGrantProfile { pub cache_ttl_seconds: i64, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub audience_overrides: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subject_token: Option, + #[serde(default, skip_serializing_if = "String::is_empty")] + pub requested_token_type: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct TokenGrantSubjectTokenProfile { + pub source: String, + pub credential: String, + #[serde(default, skip_serializing_if = "String::is_empty")] + pub subject_token_type: String, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] @@ -816,6 +836,26 @@ fn default_refresh_strategy() -> ProviderCredentialRefreshStrategy { ProviderCredentialRefreshStrategy::Unspecified } +fn default_token_grant_type() -> ProviderCredentialTokenGrantType { + ProviderCredentialTokenGrantType::ClientCredentials +} + +fn effective_token_grant_type( + grant_type: ProviderCredentialTokenGrantType, +) -> ProviderCredentialTokenGrantType { + match grant_type { + ProviderCredentialTokenGrantType::Unspecified => { + ProviderCredentialTokenGrantType::ClientCredentials + } + other => other, + } +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn is_client_credentials_grant(value: &ProviderCredentialTokenGrantType) -> bool { + effective_token_grant_type(*value) == ProviderCredentialTokenGrantType::ClientCredentials +} + fn deserialize_category<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -858,6 +898,28 @@ where serializer.serialize_str(provider_refresh_strategy_to_yaml(*strategy)) } +fn deserialize_token_grant_type<'de, D>( + deserializer: D, +) -> Result +where + D: Deserializer<'de>, +{ + let raw = String::deserialize(deserializer)?; + provider_token_grant_type_from_yaml(&raw) + .ok_or_else(|| de::Error::custom(format!("unsupported provider token grant type: {raw}"))) +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn serialize_token_grant_type( + grant_type: &ProviderCredentialTokenGrantType, + serializer: S, +) -> Result +where + S: Serializer, +{ + serializer.serialize_str(provider_token_grant_type_to_yaml(*grant_type)) +} + #[must_use] pub fn provider_profile_category_from_yaml(raw: &str) -> Option { match raw.trim().to_ascii_lowercase().replace('-', "_").as_str() { @@ -918,6 +980,26 @@ pub fn provider_refresh_strategy_to_yaml( } } +#[must_use] +pub fn provider_token_grant_type_from_yaml(raw: &str) -> Option { + match raw.trim().to_ascii_lowercase().replace('-', "_").as_str() { + "" | "client_credentials" => Some(ProviderCredentialTokenGrantType::ClientCredentials), + "token_exchange" => Some(ProviderCredentialTokenGrantType::TokenExchange), + _ => None, + } +} + +#[must_use] +pub fn provider_token_grant_type_to_yaml( + grant_type: ProviderCredentialTokenGrantType, +) -> &'static str { + match grant_type { + ProviderCredentialTokenGrantType::TokenExchange => "token_exchange", + ProviderCredentialTokenGrantType::ClientCredentials + | ProviderCredentialTokenGrantType::Unspecified => "client_credentials", + } +} + fn credential_refresh_from_proto(refresh: &ProviderCredentialRefresh) -> CredentialRefreshProfile { CredentialRefreshProfile { strategy: ProviderCredentialRefreshStrategy::try_from(refresh.strategy) @@ -979,6 +1061,10 @@ fn token_grant_from_proto( token_grant: &openshell_core::proto::ProviderCredentialTokenGrant, ) -> TokenGrantProfile { TokenGrantProfile { + grant_type: effective_token_grant_type( + ProviderCredentialTokenGrantType::try_from(token_grant.grant_type) + .unwrap_or(ProviderCredentialTokenGrantType::ClientCredentials), + ), token_endpoint: token_grant.token_endpoint.clone(), audience: token_grant.audience.clone(), jwt_svid_audience: token_grant.jwt_svid_audience.clone(), @@ -990,6 +1076,11 @@ fn token_grant_from_proto( .iter() .map(token_grant_audience_override_from_proto) .collect(), + subject_token: token_grant + .subject_token + .as_ref() + .map(token_grant_subject_token_from_proto), + requested_token_type: token_grant.requested_token_type.clone(), } } @@ -997,6 +1088,7 @@ fn token_grant_to_proto( token_grant: &TokenGrantProfile, ) -> openshell_core::proto::ProviderCredentialTokenGrant { openshell_core::proto::ProviderCredentialTokenGrant { + grant_type: token_grant.grant_type as i32, token_endpoint: token_grant.token_endpoint.clone(), audience: token_grant.audience.clone(), jwt_svid_audience: token_grant.jwt_svid_audience.clone(), @@ -1008,6 +1100,31 @@ fn token_grant_to_proto( .iter() .map(token_grant_audience_override_to_proto) .collect(), + subject_token: token_grant + .subject_token + .as_ref() + .map(token_grant_subject_token_to_proto), + requested_token_type: token_grant.requested_token_type.clone(), + } +} + +fn token_grant_subject_token_from_proto( + subject_token: &ProviderCredentialTokenGrantSubjectToken, +) -> TokenGrantSubjectTokenProfile { + TokenGrantSubjectTokenProfile { + source: subject_token.source.clone(), + credential: subject_token.credential.clone(), + subject_token_type: subject_token.subject_token_type.clone(), + } +} + +fn token_grant_subject_token_to_proto( + subject_token: &TokenGrantSubjectTokenProfile, +) -> ProviderCredentialTokenGrantSubjectToken { + ProviderCredentialTokenGrantSubjectToken { + source: subject_token.source.clone(), + credential: subject_token.credential.clone(), + subject_token_type: subject_token.subject_token_type.clone(), } } @@ -1816,6 +1933,12 @@ pub fn validate_profile_set( message, )); } + diagnostics.extend(validate_token_grant_subject_token( + source, + profile_id, + credential, + &credential_names, + )); diagnostics.extend(validate_token_grant_audience_overrides( source, profile_id, @@ -2227,6 +2350,75 @@ struct TokenGrantOverrideBinding { score: u32, } +fn validate_token_grant_subject_token( + source: &str, + profile_id: &str, + credential: &CredentialProfile, + credential_names: &HashSet, +) -> Vec { + let Some(token_grant) = credential.token_grant.as_ref() else { + return Vec::new(); + }; + let grant_type = effective_token_grant_type(token_grant.grant_type); + let mut diagnostics = Vec::new(); + + match grant_type { + ProviderCredentialTokenGrantType::ClientCredentials => { + if token_grant.subject_token.is_some() { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.token_grant.subject_token", + "subject_token is only valid for token_exchange grants", + )); + } + } + ProviderCredentialTokenGrantType::TokenExchange => { + let Some(subject_token) = token_grant.subject_token.as_ref() else { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.token_grant.subject_token", + "token_exchange grants require subject_token", + )); + return diagnostics; + }; + + let source_value = subject_token.source.trim(); + if source_value != "provider_credential" { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.token_grant.subject_token.source", + "subject_token.source must be provider_credential", + )); + } + + let subject_credential = subject_token.credential.trim(); + if subject_credential.is_empty() { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.token_grant.subject_token.credential", + "subject_token.credential is required", + )); + } else if !credential_names.contains(subject_credential) { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.token_grant.subject_token.credential", + format!("unknown subject token credential: {subject_credential}"), + )); + } + } + ProviderCredentialTokenGrantType::Unspecified => { + unreachable!("effective_token_grant_type must normalize unspecified token grant type") + } + } + + diagnostics +} + fn validate_token_grant_audience_overrides( source: &str, profile_id: &str, @@ -2556,7 +2748,7 @@ pub fn builtin_profiles() -> &'static [ProviderTypeProfile] { mod tests { use std::collections::HashMap; - use openshell_core::proto::ProviderProfileCategory; + use openshell_core::proto::{ProviderCredentialTokenGrantType, ProviderProfileCategory}; use super::{ DiscoveryProfile, L7AllowProfile, L7QueryMatcherProfile, ProfileError, ProviderTypeProfile, @@ -3140,6 +3332,166 @@ credentials: ); } + #[test] + fn token_exchange_grant_round_trips_through_proto_and_yaml() { + let profile = parse_profile_yaml( + r" +id: keycloak-token-exchange +display_name: Keycloak Token Exchange +credentials: + - name: USER_OIDC_TOKEN + required: true + - name: access_token + auth_style: bearer + header_name: Authorization + token_grant: + grant_type: token_exchange + token_endpoint: https://keycloak.example.com/realms/openshell/protocol/openid-connect/token + subject_token: + source: provider_credential + credential: USER_OIDC_TOKEN + subject_token_type: urn:ietf:params:oauth:token-type:access_token + jwt_svid_audience: https://keycloak.example.com/realms/openshell + client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer + audience: https://graph.example.com + scopes: [graph.read] + requested_token_type: urn:ietf:params:oauth:token-type:access_token +", + ) + .expect("profile should parse"); + + let diagnostics = + validate_profile_set(&[("keycloak-token-exchange.yaml".to_string(), profile.clone())]); + assert!( + diagnostics.is_empty(), + "unexpected diagnostics: {diagnostics:?}" + ); + + let token_grant = profile.credentials[1] + .token_grant + .as_ref() + .expect("token grant should parse"); + assert_eq!( + token_grant.grant_type, + ProviderCredentialTokenGrantType::TokenExchange + ); + assert_eq!( + token_grant + .subject_token + .as_ref() + .map(|subject| subject.credential.as_str()), + Some("USER_OIDC_TOKEN") + ); + + let from_proto = ProviderTypeProfile::from_proto(&profile.to_proto()); + assert_eq!( + from_proto.credentials[1].token_grant, + profile.credentials[1].token_grant + ); + + let exported = profile_to_yaml(&from_proto).expect("yaml"); + assert!(exported.contains("grant_type: token_exchange")); + assert!(exported.contains("subject_token:")); + let reparsed = parse_profile_yaml(&exported).expect("re-parse"); + assert_eq!( + reparsed.credentials[1].token_grant, + profile.credentials[1].token_grant + ); + } + + #[test] + fn validate_profile_set_rejects_token_exchange_without_subject_token() { + let profile = parse_profile_yaml( + r" +id: missing-subject-token +display_name: Missing Subject Token +credentials: + - name: access_token + auth_style: bearer + header_name: Authorization + token_grant: + grant_type: token_exchange + token_endpoint: https://keycloak.example.com/realms/openshell/protocol/openid-connect/token +", + ) + .expect("profile should parse"); + + let diagnostics = validate_profile_set(&[("missing.yaml".to_string(), profile)]); + let diagnostic = diagnostics + .iter() + .find(|diagnostic| diagnostic.field == "credentials.token_grant.subject_token") + .expect("expected subject_token diagnostic"); + assert_eq!( + diagnostic.message, + "token_exchange grants require subject_token" + ); + } + + #[test] + fn validate_profile_set_rejects_token_exchange_unknown_subject_credential() { + let profile = parse_profile_yaml( + r" +id: unknown-subject-token +display_name: Unknown Subject Token +credentials: + - name: access_token + auth_style: bearer + header_name: Authorization + token_grant: + grant_type: token_exchange + token_endpoint: https://keycloak.example.com/realms/openshell/protocol/openid-connect/token + subject_token: + source: provider_credential + credential: USER_OIDC_TOKEN +", + ) + .expect("profile should parse"); + + let diagnostics = validate_profile_set(&[("unknown.yaml".to_string(), profile)]); + let diagnostic = diagnostics + .iter() + .find(|diagnostic| { + diagnostic.field == "credentials.token_grant.subject_token.credential" + }) + .expect("expected subject token credential diagnostic"); + assert!( + diagnostic + .message + .contains("unknown subject token credential: USER_OIDC_TOKEN") + ); + } + + #[test] + fn validate_profile_set_rejects_subject_token_on_client_credentials_grant() { + let profile = parse_profile_yaml( + r" +id: misplaced-subject-token +display_name: Misplaced Subject Token +credentials: + - name: USER_OIDC_TOKEN + - name: access_token + auth_style: bearer + header_name: Authorization + token_grant: + token_endpoint: https://keycloak.example.com/realms/openshell/protocol/openid-connect/token + subject_token: + source: provider_credential + credential: USER_OIDC_TOKEN +", + ) + .expect("profile should parse"); + + let diagnostics = validate_profile_set(&[("misplaced.yaml".to_string(), profile)]); + let diagnostic = diagnostics + .iter() + .find(|diagnostic| diagnostic.field == "credentials.token_grant.subject_token") + .expect("expected subject_token diagnostic"); + assert_eq!( + diagnostic.message, + "subject_token is only valid for token_exchange grants" + ); + } + #[test] fn validate_profile_set_rejects_plain_http_token_endpoint() { for token_endpoint in [ diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 4ff23d74d1..0de0add301 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -458,6 +458,13 @@ impl OpenShell for TestOpenShell { Ok(Response::new(proto::GetGatewayConfigResponse::default())) } + async fn exchange_provider_subject_token( + &self, + _: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn update_config( &self, _: tonic::Request, diff --git a/crates/openshell-server/Cargo.toml b/crates/openshell-server/Cargo.toml index 6329bad823..56e2cff2c3 100644 --- a/crates/openshell-server/Cargo.toml +++ b/crates/openshell-server/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" [dependencies] openshell-bootstrap = { path = "../openshell-bootstrap" } -openshell-core = { path = "../openshell-core", default-features = false } +openshell-core = { path = "../openshell-core", default-features = false, features = ["oauth"] } openshell-driver-db-credstore = { path = "../openshell-driver-db-credstore" } openshell-driver-kubernetes-secrets = { path = "../openshell-driver-kubernetes-secrets" } openshell-driver-vault = { path = "../openshell-driver-vault" } @@ -99,6 +99,7 @@ uuid = { workspace = true } hmac = "0.12" sha2 = { workspace = true } jsonwebtoken = { workspace = true } +spiffe = { workspace = true } async-trait = "0.1" url = { workspace = true } glob = { workspace = true } diff --git a/crates/openshell-server/src/auth/oidc.rs b/crates/openshell-server/src/auth/oidc.rs index fd599b501a..475c52ebec 100644 --- a/crates/openshell-server/src/auth/oidc.rs +++ b/crates/openshell-server/src/auth/oidc.rs @@ -240,6 +240,7 @@ impl JwksCache { let Some(ref kid) = key.kid else { continue; }; + crate::install_jsonwebtoken_crypto_provider(); match DecodingKey::from_rsa_components(&key.n, &key.e) { Ok(dk) => { new_keys.insert(kid.clone(), dk); @@ -286,6 +287,8 @@ impl JwksCache { /// This is the authentication step — it verifies the caller's identity /// but does not check authorization (that's `authz::AuthzPolicy::check`). pub async fn validate_token(&self, token: &str) -> Result { + crate::install_jsonwebtoken_crypto_provider(); + self.refresh_if_stale().await.map_err(|e| { warn!(error = %e, "JWKS refresh failed"); Status::internal("OIDC key refresh failed") @@ -568,6 +571,8 @@ mod tests { /// Sign `claims` with the test key, tagging the header with `kid`. fn mint_rs256(claims: &serde_json::Value, kid: &str) -> String { + crate::install_jsonwebtoken_crypto_provider(); + let mut header = jsonwebtoken::Header::new(Algorithm::RS256); header.kid = Some(kid.to_owned()); let key = jsonwebtoken::EncodingKey::from_rsa_pem(TEST_RSA_KEY.private_pem.as_bytes()) @@ -682,6 +687,7 @@ mod tests { let cache = cache_with_mock_issuer(&server).await; let other = TestRsaKey::generate(); + crate::install_jsonwebtoken_crypto_provider(); let mut header = jsonwebtoken::Header::new(Algorithm::RS256); header.kid = Some(TEST_KID.to_owned()); let token = jsonwebtoken::encode( diff --git a/crates/openshell-server/src/auth/sandbox_jwt.rs b/crates/openshell-server/src/auth/sandbox_jwt.rs index 39f5982ca0..093b75bf2f 100644 --- a/crates/openshell-server/src/auth/sandbox_jwt.rs +++ b/crates/openshell-server/src/auth/sandbox_jwt.rs @@ -85,6 +85,8 @@ impl SandboxJwtIssuer { gateway_id: &str, ttl: Duration, ) -> Result { + crate::install_jsonwebtoken_crypto_provider(); + let encoding_key = EncodingKey::from_ed_pem(signing_key_pem) .map_err(|e| format!("failed to parse Ed25519 signing key PEM: {e}"))?; let identity = format!("openshell-gateway:{gateway_id}"); @@ -100,6 +102,8 @@ impl SandboxJwtIssuer { /// Mint a fresh token for `sandbox_id`. #[allow(clippy::result_large_err)] // `tonic::Status` is the natural error here pub fn mint(&self, sandbox_id: &str) -> Result { + crate::install_jsonwebtoken_crypto_provider(); + let now = now_secs(); let exp = if self.ttl.is_zero() { 0 @@ -151,6 +155,8 @@ impl std::fmt::Debug for SandboxJwtAuthenticator { impl SandboxJwtAuthenticator { pub fn from_pem(public_key_pem: &[u8], kid: String, gateway_id: &str) -> Result { + crate::install_jsonwebtoken_crypto_provider(); + let decoding_key = DecodingKey::from_ed_pem(public_key_pem) .map_err(|e| format!("failed to parse Ed25519 public key PEM: {e}"))?; let identity = format!("openshell-gateway:{gateway_id}"); @@ -164,6 +170,8 @@ impl SandboxJwtAuthenticator { #[allow(clippy::result_large_err)] fn validate_bearer(&self, token: &str) -> Result, Status> { + crate::install_jsonwebtoken_crypto_provider(); + let header = decode_header(token).map_err(|e| { debug!(error = %e, "sandbox JWT header decode failed"); Status::unauthenticated("invalid token") diff --git a/crates/openshell-server/src/auth/sandbox_methods.rs b/crates/openshell-server/src/auth/sandbox_methods.rs index a74b1280ce..3638282ef9 100644 --- a/crates/openshell-server/src/auth/sandbox_methods.rs +++ b/crates/openshell-server/src/auth/sandbox_methods.rs @@ -32,6 +32,9 @@ mod tests { assert!(is_sandbox_callable( "/openshell.inference.v1.Inference/GetInferenceBundle" )); + assert!(is_sandbox_callable( + "/openshell.v1.OpenShell/ExchangeProviderSubjectToken" + )); } #[test] diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index d84ca41557..45b09f62ae 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -23,7 +23,8 @@ use openshell_core::proto::{ DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, DeleteServiceRequest, DeleteServiceResponse, DeleteWorkspaceRequest, DeleteWorkspaceResponse, DetachSandboxProviderRequest, DetachSandboxProviderResponse, EditDraftChunkRequest, - EditDraftChunkResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, + EditDraftChunkResponse, ExchangeProviderSubjectTokenRequest, + ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, ExposeServiceRequest, GatewayMessage, GetCurrentUserRequest, GetCurrentUserResponse, GetDraftHistoryRequest, GetDraftHistoryResponse, GetDraftPolicyRequest, GetDraftPolicyResponse, GetGatewayConfigRequest, GetGatewayConfigResponse, GetGatewayInfoRequest, @@ -527,6 +528,13 @@ impl OpenShell for OpenShellService { policy::handle_get_sandbox_provider_environment(&self.state, request).await } + async fn exchange_provider_subject_token( + &self, + request: Request, + ) -> Result, Status> { + provider::handle_exchange_provider_subject_token(&self.state, request).await + } + async fn update_config( &self, request: Request, diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index 6dc4318a8a..b10eda5780 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -31,6 +31,8 @@ use super::{ MAX_MAP_KEY_LEN, MAX_MAP_VALUE_LEN, MAX_PAGE_SIZE, MAX_PROVIDER_CONFIG_ENTRIES, clamp_limit, }; +const GATEWAY_SPIFFE_WORKLOAD_API_SOCKET: &str = "OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET"; + // --------------------------------------------------------------------------- // CRUD helpers // --------------------------------------------------------------------------- @@ -2004,25 +2006,114 @@ use openshell_core::proto::{ ConfigureProviderRefreshRequest, ConfigureProviderRefreshResponse, CreateProviderRequest, DeleteProviderProfileRequest, DeleteProviderProfileResponse, DeleteProviderRefreshRequest, DeleteProviderRefreshResponse, DeleteProviderRequest, DeleteProviderResponse, + ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, GetProviderProfileRequest, GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse, GetProviderRequest, ImportProviderProfilesRequest, ImportProviderProfilesResponse, LintProviderProfilesRequest, LintProviderProfilesResponse, ListProviderProfilesRequest, ListProviderProfilesResponse, ListProvidersRequest, ListProvidersResponse, - ProviderCredentialRefreshStrategy, ProviderProfileDiagnostic, ProviderProfileImportItem, - ProviderProfileResponse, ProviderResponse, RotateProviderCredentialRequest, - RotateProviderCredentialResponse, StoredProviderProfile, UpdateProviderProfilesRequest, - UpdateProviderProfilesResponse, UpdateProviderRequest, + ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantType, ProviderProfileDiagnostic, + ProviderProfileImportItem, ProviderProfileResponse, ProviderResponse, + RotateProviderCredentialRequest, RotateProviderCredentialResponse, StoredProviderProfile, + UpdateProviderProfilesRequest, UpdateProviderProfilesResponse, UpdateProviderRequest, +}; +use openshell_core::spiffe::{ + JwtSvidParseError, SpiffeJwtClaims, parse_unverified_jwt_svid_claims, + trust_domain as spiffe_trust_domain, workload_api_endpoint, }; use openshell_providers::{ CredentialRefreshProfile, ProfileValidationDiagnostic, ProviderTypeProfile, normalize_profile_id, normalize_provider_type, strategy_output_env_key, strategy_output_spec, strategy_primary_env_key, validate_profile_set, }; -use std::sync::Arc; +use std::sync::{Arc, LazyLock, RwLock}; use tonic::{Request, Response}; use crate::auth::principal::Principal; use crate::auth::workspace_authz::{MinWorkspaceRole, authorize_workspace, require_platform_admin}; +use openshell_core::oauth::{ + self, TokenExchangeParams, effective_client_assertion_type, effective_token_type, +}; +const DEFAULT_INTERMEDIATE_TOKEN_CACHE_TTL_SECONDS: i64 = 300; +const MAX_INTERMEDIATE_TOKEN_CACHE_TTL_SECONDS: i64 = 3600; +const INTERMEDIATE_TOKEN_CACHE_EXPIRY_SKEW_SECONDS: i64 = 30; +const MAX_INTERMEDIATE_TOKEN_CACHE_ENTRIES: usize = 1024; + +static TOKEN_EXCHANGE_HTTP_CLIENT: LazyLock> = + LazyLock::new(|| { + reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .connect_timeout(std::time::Duration::from_secs(30)) + .no_proxy() + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|err| { + format!("provider token exchange HTTP client configuration failed: {err}") + }) + }); +static INTERMEDIATE_TOKEN_CACHE: LazyLock = + LazyLock::new(IntermediateTokenCache::new); + +fn token_exchange_http_client() -> Result<&'static reqwest::Client, Status> { + TOKEN_EXCHANGE_HTTP_CLIENT + .as_ref() + .map_err(|err| Status::internal(err.clone())) +} + +#[derive(Clone)] +struct CachedIntermediateToken { + access_token: String, + token_type: String, + expires_at_ms: i64, +} + +struct IntermediateTokenCache { + tokens: Arc>>, +} + +impl IntermediateTokenCache { + fn new() -> Self { + Self { + tokens: Arc::new(RwLock::new(HashMap::new())), + } + } + + fn get(&self, key: &str) -> Option { + let now_ms = crate::persistence::current_time_ms(); + let tokens = self.tokens.read().ok()?; + let cached = tokens.get(key)?; + if cached.expires_at_ms <= now_ms { + return None; + } + Some(oauth::OAuthTokenResponse { + access_token: cached.access_token.clone(), + expires_in: cached.expires_at_ms.saturating_sub(now_ms) / 1000, + token_type: cached.token_type.clone(), + }) + } + + fn set(&self, key: String, token: &oauth::OAuthTokenResponse, expires_at_ms: i64) { + if let Ok(mut tokens) = self.tokens.write() { + let now_ms = crate::persistence::current_time_ms(); + tokens.retain(|_, cached| cached.expires_at_ms > now_ms); + if tokens.len() >= MAX_INTERMEDIATE_TOKEN_CACHE_ENTRIES + && let Some(evict_key) = tokens + .iter() + .min_by_key(|(_, cached)| cached.expires_at_ms) + .map(|(k, _)| k.clone()) + { + tokens.remove(&evict_key); + } + tokens.insert( + key, + CachedIntermediateToken { + access_token: token.access_token.clone(), + token_type: token.token_type.clone(), + expires_at_ms, + }, + ); + } + } +} async fn authorize_and_resolve_profile_workspace( state: &Arc, @@ -3267,6 +3358,469 @@ pub(super) async fn handle_update_provider( } } +pub(super) async fn handle_exchange_provider_subject_token( + state: &Arc, + request: Request, +) -> Result, Status> { + let req = request.get_ref().clone(); + let principal = crate::auth::guard::enforce_sandbox_scope(&request, &req.sandbox_id)?; + crate::auth::guard::ensure_sandbox_principal_scope(&principal, &req.sandbox_id)?; + drop(request); + + if req.provider.trim().is_empty() { + return Err(Status::invalid_argument("provider is required")); + } + if req.credential_key.trim().is_empty() { + return Err(Status::invalid_argument("credential_key is required")); + } + if req.supervisor_jwt_svid.trim().is_empty() { + return Err(Status::invalid_argument("supervisor_jwt_svid is required")); + } + + let sandbox = state + .store + .get_message::(&req.sandbox_id) + .await + .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? + .ok_or_else(|| Status::not_found("sandbox not found"))?; + let workspace = sandbox.object_workspace().to_string(); + let spec = sandbox + .spec + .as_ref() + .ok_or_else(|| Status::internal("sandbox has no spec"))?; + if !spec + .providers + .iter() + .any(|provider| provider == &req.provider) + { + return Err(Status::permission_denied( + "provider is not attached to this sandbox", + )); + } + + let provider = state + .store + .get_message_by_name::(&workspace, &req.provider) + .await + .map_err(|e| Status::internal(format!("fetch provider failed: {e}")))? + .ok_or_else(|| Status::not_found("provider not found"))?; + let profile_id = normalize_provider_type(&provider.r#type).unwrap_or(provider.r#type.as_str()); + let catalog = state + .provider_profile_sources + .snapshot_catalog(state.store.as_ref(), &workspace) + .await?; + let profile = + get_provider_type_profile_for_scope(&catalog, profile_id, &provider.profile_workspace) + .ok_or_else(|| Status::failed_precondition("provider profile not found"))?; + let profile_proto = profile.to_proto(); + let credential = profile_proto + .credentials + .iter() + .find(|credential| credential.name == req.credential_key) + .ok_or_else(|| { + Status::failed_precondition("credential not declared by provider profile") + })?; + let token_grant = credential + .token_grant + .as_ref() + .ok_or_else(|| Status::failed_precondition("credential does not declare token_grant"))?; + let grant_type = ProviderCredentialTokenGrantType::try_from(token_grant.grant_type) + .unwrap_or(ProviderCredentialTokenGrantType::ClientCredentials); + if grant_type != ProviderCredentialTokenGrantType::TokenExchange { + return Err(Status::failed_precondition( + "credential token_grant is not token_exchange", + )); + } + let subject_token = token_grant + .subject_token + .as_ref() + .ok_or_else(|| Status::failed_precondition("token_exchange subject_token is missing"))?; + if subject_token.source != "provider_credential" { + return Err(Status::failed_precondition( + "unsupported subject_token source", + )); + } + if !profile_proto + .credentials + .iter() + .any(|credential| credential.name == subject_token.credential) + { + return Err(Status::failed_precondition( + "subject token credential not declared by provider profile", + )); + } + let stored_subject_token = + resolve_subject_token_credential(&state.credentials, &provider, &subject_token.credential) + .await?; + + let jwt_svid_audience = + effective_jwt_svid_audience(&token_grant.token_endpoint, &token_grant.jwt_svid_audience); + let gateway_jwt_svid = fetch_gateway_jwt_svid(&jwt_svid_audience).await?; + let gateway_claims = parse_unverified_spiffe_claims(&gateway_jwt_svid)?; + validate_gateway_jwt_svid_claims(&gateway_claims, &jwt_svid_audience)?; + let supervisor_claims = validate_supervisor_jwt_svid( + &req.supervisor_jwt_svid, + &gateway_claims, + &jwt_svid_audience, + ) + .await?; + + let intermediate_cache_key = intermediate_token_cache_key(IntermediateTokenCacheKeyInput { + provider: &provider, + dynamic_credential: &req.credential_key, + subject_credential: &subject_token.credential, + token_endpoint: &token_grant.token_endpoint, + client_assertion_type: effective_client_assertion_type(&token_grant.client_assertion_type), + subject_token_type: effective_token_type(&subject_token.subject_token_type), + audience: &supervisor_claims.sub, + requested_token_type: effective_token_type(&token_grant.requested_token_type), + supervisor_subject: &supervisor_claims.sub, + gateway_subject: &gateway_claims.sub, + }); + if let Some(cached) = INTERMEDIATE_TOKEN_CACHE.get(&intermediate_cache_key) { + return Ok(Response::new(ExchangeProviderSubjectTokenResponse { + access_token: cached.access_token, + expires_in: cached.expires_in, + token_type: cached.token_type, + })); + } + + let token_response = perform_intermediate_token_exchange( + &token_grant.token_endpoint, + &gateway_jwt_svid, + &token_grant.client_assertion_type, + &stored_subject_token, + &subject_token.subject_token_type, + &supervisor_claims.sub, + &token_grant.requested_token_type, + ) + .await + .inspect_err(|status| { + warn!( + sandbox_id = %req.sandbox_id, + provider = %req.provider, + credential_key = %req.credential_key, + subject_credential = %subject_token.credential, + client_assertion_type = %effective_client_assertion_type(&token_grant.client_assertion_type), + gateway_svid_issuer = %gateway_claims.iss, + gateway_svid_subject = %gateway_claims.sub, + gateway_svid_audience = ?gateway_claims.aud, + supervisor_svid_issuer = %supervisor_claims.iss, + supervisor_svid_subject = %supervisor_claims.sub, + supervisor_svid_audience = ?supervisor_claims.aud, + status = ?status.code(), + error = %status.message(), + "intermediate provider token exchange failed" + ); + })?; + let cache_expires_at_ms = intermediate_token_cache_expires_at_ms( + &token_response, + token_grant.cache_ttl_seconds, + provider_credential_expires_at_ms(&provider, &subject_token.credential), + supervisor_claims.exp, + ); + if cache_expires_at_ms > crate::persistence::current_time_ms() { + INTERMEDIATE_TOKEN_CACHE.set(intermediate_cache_key, &token_response, cache_expires_at_ms); + } + + Ok(Response::new(ExchangeProviderSubjectTokenResponse { + access_token: token_response.access_token, + expires_in: token_response.expires_in, + token_type: token_response.token_type, + })) +} + +async fn resolve_subject_token_credential( + credentials: &crate::credentials::CredentialRuntime, + provider: &Provider, + credential_key: &str, +) -> Result { + if let Some(value) = provider + .credentials + .get(credential_key) + .filter(|value| !value.is_empty()) + { + ensure_subject_token_credential_not_expired(provider, credential_key)?; + return Ok(value.clone()); + } + + if provider.credential_handles.contains_key(credential_key) { + let resolved = credentials + .resolve_provider_handles(provider, crate::persistence::current_time_ms()) + .await?; + if let Some(value) = resolved + .values + .get(credential_key) + .filter(|value| !value.is_empty()) + { + return Ok(value.clone()); + } + } + + Err(Status::failed_precondition( + "subject token credential is not configured", + )) +} + +fn ensure_subject_token_credential_not_expired( + provider: &Provider, + credential_key: &str, +) -> Result<(), Status> { + let expires_at_ms = provider_credential_expires_at_ms(provider, credential_key); + if expires_at_ms > 0 && expires_at_ms <= crate::persistence::current_time_ms() { + return Err(Status::failed_precondition( + "subject token credential has expired", + )); + } + Ok(()) +} + +fn provider_credential_expires_at_ms(provider: &Provider, credential_key: &str) -> i64 { + provider + .credential_expires_at_ms + .get(credential_key) + .copied() + .unwrap_or_default() +} + +struct IntermediateTokenCacheKeyInput<'a> { + provider: &'a Provider, + dynamic_credential: &'a str, + subject_credential: &'a str, + token_endpoint: &'a str, + client_assertion_type: &'a str, + subject_token_type: &'a str, + audience: &'a str, + requested_token_type: &'a str, + supervisor_subject: &'a str, + gateway_subject: &'a str, +} + +fn intermediate_token_cache_key(input: IntermediateTokenCacheKeyInput<'_>) -> String { + let provider_id = input + .provider + .metadata + .as_ref() + .map(|metadata| metadata.id.as_str()) + .filter(|id| !id.is_empty()) + .unwrap_or_else(|| input.provider.object_name()); + let provider_resource_version = input + .provider + .metadata + .as_ref() + .map_or(0, |metadata| metadata.resource_version); + format!( + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + provider_id, + provider_resource_version, + input.dynamic_credential, + input.subject_credential, + input.token_endpoint, + input.client_assertion_type, + input.subject_token_type, + input.audience, + input.requested_token_type, + input.supervisor_subject, + input.gateway_subject + ) +} + +fn intermediate_token_cache_expires_at_ms( + token: &oauth::OAuthTokenResponse, + cache_ttl_seconds: i64, + subject_token_expires_at_ms: i64, + supervisor_svid_exp_seconds: i64, +) -> i64 { + let now_ms = crate::persistence::current_time_ms(); + let mut ttl_seconds = if token.expires_in > 0 { + token + .expires_in + .min(MAX_INTERMEDIATE_TOKEN_CACHE_TTL_SECONDS) + } else { + DEFAULT_INTERMEDIATE_TOKEN_CACHE_TTL_SECONDS + }; + if cache_ttl_seconds > 0 { + ttl_seconds = ttl_seconds.min(cache_ttl_seconds); + } + ttl_seconds = ttl_seconds + .saturating_sub(INTERMEDIATE_TOKEN_CACHE_EXPIRY_SKEW_SECONDS) + .max(1); + let mut expires_at_ms = now_ms.saturating_add(ttl_seconds.saturating_mul(1000)); + expires_at_ms = cap_cache_expiry_ms(expires_at_ms, jwt_exp_ms(&token.access_token)); + expires_at_ms = cap_cache_expiry_ms(expires_at_ms, Some(subject_token_expires_at_ms)); + expires_at_ms = cap_cache_expiry_ms( + expires_at_ms, + (supervisor_svid_exp_seconds > 0).then(|| supervisor_svid_exp_seconds.saturating_mul(1000)), + ); + expires_at_ms +} + +fn cap_cache_expiry_ms(current_expires_at_ms: i64, cap_expires_at_ms: Option) -> i64 { + let Some(cap_expires_at_ms) = cap_expires_at_ms.filter(|value| *value > 0) else { + return current_expires_at_ms; + }; + current_expires_at_ms.min( + cap_expires_at_ms + .saturating_sub(INTERMEDIATE_TOKEN_CACHE_EXPIRY_SKEW_SECONDS.saturating_mul(1000)), + ) +} + +fn jwt_exp_ms(token: &str) -> Option { + use base64::Engine as _; + let payload = token.split('.').nth(1)?; + let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(payload) + .ok()?; + let claims = serde_json::from_slice::(&decoded).ok()?; + claims + .get("exp")? + .as_i64() + .map(|exp| exp.saturating_mul(1000)) +} + +async fn fetch_gateway_jwt_svid(audience: &str) -> Result { + let socket_path = std::env::var(GATEWAY_SPIFFE_WORKLOAD_API_SOCKET) + .ok() + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| { + Status::failed_precondition(format!( + "{GATEWAY_SPIFFE_WORKLOAD_API_SOCKET} is required for provider token exchange" + )) + })?; + let endpoint = workload_api_endpoint(std::path::Path::new(&socket_path)); + let client = spiffe::WorkloadApiClient::connect_to(&endpoint) + .await + .map_err(|e| { + Status::failed_precondition(format!("SPIFFE Workload API unavailable: {e}")) + })?; + client + .fetch_jwt_token([audience], None) + .await + .map_err(|e| Status::failed_precondition(format!("failed to fetch gateway JWT-SVID: {e}"))) +} + +fn validate_gateway_jwt_svid_claims( + claims: &SpiffeJwtClaims, + expected_audience: &str, +) -> Result<(), Status> { + if !claims.aud.contains(expected_audience) { + return Err(Status::failed_precondition( + "gateway SVID audience does not match token grant audience", + )); + } + if spiffe_trust_domain(&claims.sub).is_none() { + return Err(Status::failed_precondition( + "gateway SVID subject is not a SPIFFE ID", + )); + } + if claims.exp > 0 && claims.exp.saturating_mul(1000) <= crate::persistence::current_time_ms() { + return Err(Status::failed_precondition("gateway SVID has expired")); + } + Ok(()) +} + +async fn validate_supervisor_jwt_svid( + token: &str, + gateway_claims: &SpiffeJwtClaims, + expected_audience: &str, +) -> Result { + let unverified = parse_unverified_spiffe_claims(token)?; + if unverified.iss != gateway_claims.iss { + return Err(Status::permission_denied( + "supervisor SVID issuer does not match gateway SVID issuer", + )); + } + if !unverified.aud.contains(expected_audience) { + return Err(Status::permission_denied( + "supervisor SVID audience does not match token grant audience", + )); + } + let supervisor_trust_domain = spiffe_trust_domain(&unverified.sub) + .ok_or_else(|| Status::permission_denied("supervisor SVID subject is not a SPIFFE ID"))?; + let gateway_trust_domain = spiffe_trust_domain(&gateway_claims.sub) + .ok_or_else(|| Status::failed_precondition("gateway SVID subject is not a SPIFFE ID"))?; + if supervisor_trust_domain != gateway_trust_domain { + return Err(Status::permission_denied( + "supervisor SVID trust domain does not match gateway SVID trust domain", + )); + } + + let socket_path = std::env::var(GATEWAY_SPIFFE_WORKLOAD_API_SOCKET) + .ok() + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| { + Status::failed_precondition(format!( + "{GATEWAY_SPIFFE_WORKLOAD_API_SOCKET} is required for supervisor JWT-SVID validation" + )) + })?; + let endpoint = workload_api_endpoint(std::path::Path::new(&socket_path)); + let client = spiffe::WorkloadApiClient::connect_to(&endpoint) + .await + .map_err(|e| { + Status::failed_precondition(format!("SPIFFE Workload API unavailable: {e}")) + })?; + let bundles = client + .fetch_jwt_bundles() + .await + .map_err(|e| Status::internal(format!("SPIFFE JWT bundle fetch failed: {e}")))?; + spiffe::JwtSvid::parse_and_validate(token, &bundles, &[expected_audience]) + .map_err(|e| Status::permission_denied(format!("invalid supervisor JWT-SVID: {e}")))?; + Ok(unverified) +} + +fn parse_unverified_spiffe_claims(token: &str) -> Result { + parse_unverified_jwt_svid_claims(token).map_err(jwt_svid_parse_error_status) +} + +fn jwt_svid_parse_error_status(error: JwtSvidParseError) -> Status { + Status::permission_denied(error.to_string()) +} + +async fn perform_intermediate_token_exchange( + token_endpoint: &str, + gateway_jwt_svid: &str, + client_assertion_type: &str, + subject_token: &str, + subject_token_type: &str, + audience: &str, + requested_token_type: &str, +) -> Result { + let client = token_exchange_http_client()?; + oauth::post_oauth_token_exchange( + client, + token_endpoint, + &TokenExchangeParams { + client_assertion: gateway_jwt_svid, + client_assertion_type, + subject_token, + subject_token_type, + audience, + scopes: &[], + requested_token_type, + }, + ) + .await + .map_err(|e| Status::failed_precondition(e.to_string())) +} + +fn effective_jwt_svid_audience(token_endpoint: &str, jwt_svid_audience: &str) -> String { + if !jwt_svid_audience.trim().is_empty() { + return jwt_svid_audience.to_string(); + } + derive_issuer_from_token_endpoint(token_endpoint) +} + +fn derive_issuer_from_token_endpoint(token_endpoint: &str) -> String { + if let Some(realms_idx) = token_endpoint.find("/realms/") { + let after_realms = &token_endpoint[realms_idx + "/realms/".len()..]; + if let Some(slash_idx) = after_realms.find('/') { + let realm_end = realms_idx + "/realms/".len() + slash_idx; + return token_endpoint[..realm_end].to_string(); + } + } + token_endpoint.to_string() +} + pub(super) async fn handle_get_provider_refresh_status( state: &Arc, request: Request, @@ -3962,13 +4516,16 @@ mod tests { refresh: None, path_template: String::new(), token_grant: Some(ProviderCredentialTokenGrant { + grant_type: ProviderCredentialTokenGrantType::ClientCredentials as i32, token_endpoint: "http://keycloak.default.svc.cluster.local/realms/openshell/protocol/openid-connect/token".to_string(), audience: "api://default".to_string(), jwt_svid_audience: "http://keycloak.default.svc.cluster.local/realms/openshell" .to_string(), client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string(), + subject_token: None, scopes: vec!["openid".to_string()], + requested_token_type: String::new(), cache_ttl_seconds: 300, audience_overrides: service_audiences .iter() @@ -4738,12 +5295,15 @@ mod tests { refresh: None, path_template: String::new(), token_grant: Some(ProviderCredentialTokenGrant { + grant_type: ProviderCredentialTokenGrantType::ClientCredentials as i32, token_endpoint: "https://auth.example.com/token".to_string(), audience: "api://default".to_string(), jwt_svid_audience: "https://auth.example.com".to_string(), client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" .to_string(), + subject_token: None, scopes: vec!["read".to_string()], + requested_token_type: String::new(), cache_ttl_seconds: 300, audience_overrides: Vec::new(), }), @@ -6674,6 +7234,33 @@ mod tests { ); } + #[tokio::test] + async fn token_exchange_subject_token_resolves_credential_handle() { + let config = openshell_core::Config::new(None).with_credential_drivers(["test-static"]); + let credentials = crate::credentials::CredentialRuntime::from_config(&config).unwrap(); + let mut provider = provider_with_values("exchange-local", "custom"); + provider.credentials.clear(); + provider.metadata.as_mut().expect("provider metadata").id = "provider-id".to_string(); + let handles = credentials + .store_provider_credentials( + provider.object_name(), + provider.object_workspace(), + provider.object_id(), + &HashMap::from([("subject_token".to_string(), "user-token".to_string())]), + &HashMap::new(), + ) + .await + .unwrap(); + provider.credential_handles = handles; + + let subject_token = + resolve_subject_token_credential(&credentials, &provider, "subject_token") + .await + .unwrap(); + + assert_eq!(subject_token, "user-token"); + } + #[tokio::test] async fn update_provider_record_overwrites_credentials_with_runtime() { let store = test_store().await; diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index a7bf847d9a..567521e067 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -79,6 +79,10 @@ pub(crate) static TEST_ENV_LOCK: LazyLock> = LazyLock::new(|| Mutex::n #[cfg(test)] pub(crate) static TEST_TRACING_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); +pub(crate) fn install_jsonwebtoken_crypto_provider() { + let _ = jsonwebtoken::crypto::aws_lc::DEFAULT_PROVIDER.install_default(); +} + use compute::ComputeRuntime; use gateway_listener::{BoundGatewayListener, GatewayListenerScope, bind_gateway_listeners}; pub use grpc::OpenShellService; diff --git a/crates/openshell-server/src/provider_refresh.rs b/crates/openshell-server/src/provider_refresh.rs index 9a655babb4..ada34e31f4 100644 --- a/crates/openshell-server/src/provider_refresh.rs +++ b/crates/openshell-server/src/provider_refresh.rs @@ -781,6 +781,8 @@ async fn mint_oauth2_client_credentials( async fn mint_google_service_account_jwt( state: &StoredProviderCredentialRefreshState, ) -> Result { + crate::install_jsonwebtoken_crypto_provider(); + let token_url = google_token_url(state); let client_email = required_material(&state.material, "client_email")?; let private_key = required_material(&state.material, "private_key")?; diff --git a/crates/openshell-server/tests/common/mod.rs b/crates/openshell-server/tests/common/mod.rs index cfd7faa3d6..351efa28c9 100644 --- a/crates/openshell-server/tests/common/mod.rs +++ b/crates/openshell-server/tests/common/mod.rs @@ -16,8 +16,9 @@ use hyper_util::{ use openshell_core::proto::{ CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, - ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, GatewayMessage, - GetGatewayConfigRequest, GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, + ExchangeProviderSubjectTokenRequest, ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, + ExecSandboxInput, ExecSandboxRequest, GatewayMessage, GetGatewayConfigRequest, + GetGatewayConfigResponse, GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, GetSandboxRequest, HealthRequest, HealthResponse, IssueSandboxTokenRequest, IssueSandboxTokenResponse, ListProvidersRequest, @@ -198,6 +199,13 @@ impl OpenShell for TestOpenShell { Ok(Response::new(RevokeSshSessionResponse::default())) } + async fn exchange_provider_subject_token( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn create_provider( &self, _request: tonic::Request, diff --git a/crates/openshell-server/tests/supervisor_relay_integration.rs b/crates/openshell-server/tests/supervisor_relay_integration.rs index be1af8f48c..9247cfdc90 100644 --- a/crates/openshell-server/tests/supervisor_relay_integration.rs +++ b/crates/openshell-server/tests/supervisor_relay_integration.rs @@ -224,6 +224,12 @@ impl OpenShell for RelayGateway { ) -> Result, Status> { Err(Status::unimplemented("unused")) } + async fn exchange_provider_subject_token( + &self, + _: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } async fn create_provider( &self, _: tonic::Request, diff --git a/crates/openshell-supervisor-network/Cargo.toml b/crates/openshell-supervisor-network/Cargo.toml index 58360aa56c..b4924e9e6f 100644 --- a/crates/openshell-supervisor-network/Cargo.toml +++ b/crates/openshell-supervisor-network/Cargo.toml @@ -11,7 +11,7 @@ repository.workspace = true rust-version.workspace = true [dependencies] -openshell-core = { path = "../openshell-core" } +openshell-core = { path = "../openshell-core", features = ["oauth"] } openshell-ocsf = { path = "../openshell-ocsf" } openshell-policy = { path = "../openshell-policy" } openshell-router = { path = "../openshell-router" } diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index aae3c042b9..fa26de2944 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -2825,6 +2825,37 @@ network_policies: (config, tunnel_engine, ctx, fixture) } + fn rest_token_exchange_relay_context( + resolver_response: std::result::Result<&str, &str>, + ) -> ( + L7EndpointConfig, + TunnelPolicyEngine, + L7EvalContext, + crate::l7::token_grant_injection::test_support::TokenGrantTestFixture, + ) { + let (config, tunnel_engine, mut ctx, _) = + rest_token_grant_relay_context(Ok("unused-token")); + let provider_key = "api.example.test\t8080\t/v1/**\tprovider:access_token"; + let fixture = match resolver_response { + Ok(token) => { + crate::l7::token_grant_injection::test_support::TokenGrantTestFixture::success_token_exchange( + provider_key, + token, + ) + } + Err(error) => { + crate::l7::token_grant_injection::test_support::TokenGrantTestFixture::failure_token_exchange( + provider_key, + error, + ) + } + }; + ctx.dynamic_credentials = Some(fixture.dynamic_credentials()); + ctx.token_grant_resolver = Some(fixture.resolver()); + + (config, tunnel_engine, ctx, fixture) + } + fn middleware_relay_context( middleware_impl: &str, on_error: &str, @@ -2935,6 +2966,36 @@ network_policies: (generation_guard, ctx, fixture) } + fn passthrough_token_exchange_relay_context( + resolver_response: std::result::Result<&str, &str>, + ) -> ( + PolicyGenerationGuard, + L7EvalContext, + crate::l7::token_grant_injection::test_support::TokenGrantTestFixture, + ) { + let (generation_guard, mut ctx, _) = + passthrough_token_grant_relay_context(Ok("unused-token")); + let provider_key = "api.example.test\t8080\t/v1/**\tprovider:access_token"; + let fixture = match resolver_response { + Ok(token) => { + crate::l7::token_grant_injection::test_support::TokenGrantTestFixture::success_token_exchange( + provider_key, + token, + ) + } + Err(error) => { + crate::l7::token_grant_injection::test_support::TokenGrantTestFixture::failure_token_exchange( + provider_key, + error, + ) + } + }; + ctx.dynamic_credentials = Some(fixture.dynamic_credentials()); + ctx.token_grant_resolver = Some(fixture.resolver()); + + (generation_guard, ctx, fixture) + } + fn jsonrpc_test_relay_context() -> (L7EndpointConfig, TunnelPolicyEngine, L7EvalContext) { jsonrpc_test_relay_context_with_path("/rpc") } @@ -3309,6 +3370,128 @@ network_policies: fixture.assert_one_request("api.example.test\t8080\t/v1/**\tprovider:access_token"); } + #[tokio::test] + async fn l7_rest_relay_injects_token_exchange_authorization_header() { + let (config, tunnel_engine, ctx, fixture) = + rest_token_exchange_relay_context(Ok("grant-token")); + let (mut app, mut relay_client) = tokio::io::duplex(8192); + let (mut relay_upstream, mut upstream) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + relay_with_inspection( + &config, + tunnel_engine, + &mut relay_client, + &mut relay_upstream, + &ctx, + ) + .await + }); + + app.write_all( + b"GET /v1/projects HTTP/1.1\r\nHost: api.example.test\r\nAuthorization: Bearer stale-token\r\nConnection: close\r\n\r\n", + ) + .await + .unwrap(); + + let mut upstream_request = [0u8; 1024]; + let n = tokio::time::timeout( + std::time::Duration::from_secs(1), + upstream.read(&mut upstream_request), + ) + .await + .expect("request should reach upstream") + .unwrap(); + let upstream_request = String::from_utf8_lossy(&upstream_request[..n]); + + assert!( + upstream_request.starts_with("GET /v1/projects HTTP/1.1\r\n"), + "unexpected upstream request: {upstream_request:?}" + ); + assert!(upstream_request.contains("Authorization: Bearer grant-token\r\n")); + assert!(!upstream_request.contains("stale-token")); + assert_eq!(authorization_header_count(&upstream_request), 1); + + upstream + .write_all(b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") + .await + .unwrap(); + + let mut client_response = [0u8; 512]; + let n = tokio::time::timeout( + std::time::Duration::from_secs(1), + app.read(&mut client_response), + ) + .await + .expect("response should reach client") + .unwrap(); + assert!(String::from_utf8_lossy(&client_response[..n]).contains("204 No Content")); + drop(app); + + tokio::time::timeout(std::time::Duration::from_secs(1), relay) + .await + .expect("relay should finish") + .unwrap() + .unwrap(); + + fixture.assert_one_token_exchange_request( + "api.example.test\t8080\t/v1/**\tprovider:access_token", + ); + } + + #[tokio::test] + async fn l7_rest_relay_token_exchange_failure_does_not_forward_request() { + let (config, tunnel_engine, ctx, fixture) = + rest_token_exchange_relay_context(Err("oauth unavailable")); + let (mut app, mut relay_client) = tokio::io::duplex(8192); + let (mut relay_upstream, mut upstream) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + relay_with_inspection( + &config, + tunnel_engine, + &mut relay_client, + &mut relay_upstream, + &ctx, + ) + .await + }); + + app.write_all( + b"GET /v1/projects HTTP/1.1\r\nHost: api.example.test\r\nConnection: close\r\n\r\n", + ) + .await + .unwrap(); + + tokio::time::timeout(std::time::Duration::from_secs(1), relay) + .await + .expect("relay should finish") + .unwrap() + .unwrap(); + + let mut client_response = [0u8; 512]; + let n = tokio::time::timeout( + std::time::Duration::from_secs(1), + app.read(&mut client_response), + ) + .await + .expect("bad gateway response should reach client") + .unwrap(); + assert!(String::from_utf8_lossy(&client_response[..n]).contains("502 Bad Gateway")); + + let mut upstream_request = [0u8; 128]; + let n = tokio::time::timeout( + std::time::Duration::from_secs(1), + upstream.read(&mut upstream_request), + ) + .await + .expect("upstream should close without forwarded data") + .unwrap(); + assert_eq!(n, 0, "unauthenticated request must not reach upstream"); + + fixture.assert_one_token_exchange_request( + "api.example.test\t8080\t/v1/**\tprovider:access_token", + ); + } + #[tokio::test] async fn l7_rest_middleware_redacts_body_before_upstream() { let (config, tunnel_engine, ctx) = @@ -5568,6 +5751,125 @@ network_policies: fixture.assert_one_request("api.example.test\t8080\t/v1/**\tprovider:access_token"); } + #[tokio::test] + async fn passthrough_relay_injects_token_exchange_authorization_header() { + let (generation_guard, ctx, fixture) = + passthrough_token_exchange_relay_context(Ok("grant-token")); + let (mut app, mut relay_client) = tokio::io::duplex(8192); + let (mut relay_upstream, mut upstream) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + relay_passthrough_with_credentials( + &mut relay_client, + &mut relay_upstream, + &ctx, + &generation_guard, + None, + ) + .await + }); + + app.write_all( + b"GET /v1/projects HTTP/1.1\r\nHost: api.example.test\r\nAuthorization: Bearer stale-token\r\nConnection: close\r\n\r\n", + ) + .await + .unwrap(); + + let mut upstream_request = [0u8; 1024]; + let n = tokio::time::timeout( + std::time::Duration::from_secs(1), + upstream.read(&mut upstream_request), + ) + .await + .expect("request should reach upstream") + .unwrap(); + let upstream_request = String::from_utf8_lossy(&upstream_request[..n]); + + assert!(upstream_request.starts_with("GET /v1/projects HTTP/1.1\r\n")); + assert!(upstream_request.contains("Authorization: Bearer grant-token\r\n")); + assert!(!upstream_request.contains("stale-token")); + assert_eq!(authorization_header_count(&upstream_request), 1); + + upstream + .write_all(b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") + .await + .unwrap(); + + let mut client_response = [0u8; 512]; + let n = tokio::time::timeout( + std::time::Duration::from_secs(1), + app.read(&mut client_response), + ) + .await + .expect("response should reach client") + .unwrap(); + assert!(String::from_utf8_lossy(&client_response[..n]).contains("204 No Content")); + drop(app); + + tokio::time::timeout(std::time::Duration::from_secs(1), relay) + .await + .expect("relay should finish") + .unwrap() + .unwrap(); + + fixture.assert_one_token_exchange_request( + "api.example.test\t8080\t/v1/**\tprovider:access_token", + ); + } + + #[tokio::test] + async fn passthrough_relay_token_exchange_failure_returns_bad_gateway_without_forwarding() { + let (generation_guard, ctx, fixture) = + passthrough_token_exchange_relay_context(Err("oauth unavailable")); + let (mut app, mut relay_client) = tokio::io::duplex(8192); + let (mut relay_upstream, mut upstream) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + relay_passthrough_with_credentials( + &mut relay_client, + &mut relay_upstream, + &ctx, + &generation_guard, + None, + ) + .await + }); + + app.write_all( + b"GET /v1/projects HTTP/1.1\r\nHost: api.example.test\r\nConnection: close\r\n\r\n", + ) + .await + .unwrap(); + + tokio::time::timeout(std::time::Duration::from_secs(1), relay) + .await + .expect("relay should finish") + .unwrap() + .unwrap(); + + let mut client_response = [0u8; 512]; + let n = tokio::time::timeout( + std::time::Duration::from_secs(1), + app.read(&mut client_response), + ) + .await + .expect("bad gateway response should reach client") + .unwrap(); + assert!(String::from_utf8_lossy(&client_response[..n]).contains("502 Bad Gateway")); + + let mut upstream_request = [0u8; 128]; + let n = tokio::time::timeout( + std::time::Duration::from_secs(1), + upstream.read(&mut upstream_request), + ) + .await + .expect("upstream should close without forwarded data") + .unwrap(); + assert_eq!(n, 0, "unauthenticated request must not reach upstream"); + + fixture.assert_one_token_exchange_request( + "api.example.test\t8080\t/v1/**\tprovider:access_token", + ); + } + #[test] fn websocket_text_policy_requires_explicit_message_rule() { let data = r#" diff --git a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs index 2ca25e6e81..8d2df6cb7b 100644 --- a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs +++ b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs @@ -26,6 +26,8 @@ pub struct TokenGrantRequest<'a> { pub audience: &'a str, pub scopes: &'a [String], pub cache_ttl_seconds: i64, + pub grant_type: i32, + pub requested_token_type: &'a str, } pub trait TokenGrantResolver: Send + Sync { @@ -45,13 +47,17 @@ impl TokenGrantResolver for SpiffeTokenGrantResolver { ) -> Pin> + Send + 'a>> { Box::pin(async move { crate::token_grant::obtain_provider_token( - request.provider_key, - request.token_endpoint, - request.jwt_svid_audience, - request.client_assertion_type, - request.audience, - request.scopes, - request.cache_ttl_seconds, + crate::token_grant::ObtainProviderTokenRequest { + provider_name: request.provider_key, + token_endpoint: request.token_endpoint, + jwt_svid_audience: request.jwt_svid_audience, + client_assertion_type: request.client_assertion_type, + audience: request.audience, + scopes: request.scopes, + cache_ttl_override: request.cache_ttl_seconds, + grant_type: request.grant_type, + requested_token_type: request.requested_token_type, + }, ) .await }) @@ -127,7 +133,7 @@ pub async fn inject_if_needed(req: L7Request, ctx: &L7EvalContext) -> Result( audience: &token_grant.audience, scopes: &token_grant.scopes, cache_ttl_seconds: token_grant.cache_ttl_seconds, + grant_type: token_grant.grant_type, + requested_token_type: &token_grant.requested_token_type, } } @@ -348,7 +356,10 @@ fn inject_header(raw_header: &[u8], header_name: &str, header_value: &str) -> Re #[cfg(test)] pub mod test_support { use super::*; - use openshell_core::proto::{ProviderCredentialTokenGrant, ProviderProfileCredential}; + use openshell_core::proto::{ + ProviderCredentialTokenGrant, ProviderCredentialTokenGrantSubjectToken, + ProviderCredentialTokenGrantType, ProviderProfileCredential, + }; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -366,6 +377,8 @@ pub mod test_support { audience: String, scopes: Vec, cache_ttl_seconds: i64, + grant_type: i32, + requested_token_type: String, } pub struct TokenGrantTestFixture { @@ -379,11 +392,27 @@ pub mod test_support { Self::new(key, Ok(token)) } + pub fn success_token_exchange(key: &str, token: &str) -> Self { + Self::new_with_grant(key, Ok(token), token_exchange_grant()) + } + pub fn failure(key: &str, error: &str) -> Self { Self::new(key, Err(error)) } + pub fn failure_token_exchange(key: &str, error: &str) -> Self { + Self::new_with_grant(key, Err(error), token_exchange_grant()) + } + fn new(key: &str, response: std::result::Result<&str, &str>) -> Self { + Self::new_with_grant(key, response, token_grant()) + } + + fn new_with_grant( + key: &str, + response: std::result::Result<&str, &str>, + token_grant: ProviderCredentialTokenGrant, + ) -> Self { let requests = Arc::new(Mutex::new(Vec::new())); let resolver = Arc::new(FakeTokenGrantResolver { requests: requests.clone(), @@ -397,7 +426,7 @@ pub mod test_support { name: "access_token".to_string(), auth_style: "bearer".to_string(), header_name: "Authorization".to_string(), - token_grant: Some(token_grant()), + token_grant: Some(token_grant), ..Default::default() }, ); @@ -437,6 +466,39 @@ pub mod test_support { assert_eq!(request.audience, "api://example"); assert_eq!(request.scopes, ["read"]); assert_eq!(request.cache_ttl_seconds, 300); + assert_eq!( + request.grant_type, + ProviderCredentialTokenGrantType::ClientCredentials as i32 + ); + assert!(request.requested_token_type.is_empty()); + } + + pub fn assert_one_token_exchange_request(&self, expected_provider_key: &str) { + let requests = self + .requests + .lock() + .expect("fake token grant requests lock poisoned"); + assert_eq!(requests.len(), 1); + + let request = &requests[0]; + assert_eq!(request.provider_key, expected_provider_key); + assert_eq!(request.token_endpoint, "https://auth.example.com/token"); + assert_eq!(request.jwt_svid_audience, "https://auth.example.com"); + assert_eq!( + request.client_assertion_type, + "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe" + ); + assert_eq!(request.audience, "api://example"); + assert_eq!(request.scopes, ["read"]); + assert_eq!(request.cache_ttl_seconds, 300); + assert_eq!( + request.grant_type, + ProviderCredentialTokenGrantType::TokenExchange as i32 + ); + assert_eq!( + request.requested_token_type, + "urn:ietf:params:oauth:token-type:access_token" + ); } } @@ -450,6 +512,24 @@ pub mod test_support { scopes: vec!["read".to_string()], cache_ttl_seconds: 300, audience_overrides: Vec::new(), + grant_type: ProviderCredentialTokenGrantType::ClientCredentials as i32, + subject_token: None, + requested_token_type: String::new(), + } + } + + fn token_exchange_grant() -> ProviderCredentialTokenGrant { + ProviderCredentialTokenGrant { + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe" + .to_string(), + grant_type: ProviderCredentialTokenGrantType::TokenExchange as i32, + subject_token: Some(ProviderCredentialTokenGrantSubjectToken { + source: "provider_credential".to_string(), + credential: "user_oidc_token".to_string(), + subject_token_type: "urn:ietf:params:oauth:token-type:id_token".to_string(), + }), + requested_token_type: "urn:ietf:params:oauth:token-type:access_token".to_string(), + ..token_grant() } } @@ -466,6 +546,8 @@ pub mod test_support { audience: request.audience.to_string(), scopes: request.scopes.to_vec(), cache_ttl_seconds: request.cache_ttl_seconds, + grant_type: request.grant_type, + requested_token_type: request.requested_token_type.to_string(), }; Box::pin(async move { self.requests @@ -502,6 +584,12 @@ mod tests { 443, "/repos/owner/repo" )); + assert!(dynamic_credential_key_matches( + "api.example.com\t443\t/repos/**\trev:42\tgithub:access_token", + "api.example.com", + 443, + "/repos/owner/repo" + )); assert!(!dynamic_credential_key_matches( key, "uploads.example.com", @@ -743,6 +831,46 @@ mod tests { fixture.assert_one_request("api.example.com\t443\t/v1/**\tprovider:access_token"); } + #[tokio::test] + async fn inject_if_needed_passes_token_exchange_grant_to_resolver() { + let fixture = TokenGrantTestFixture::success_token_exchange( + "api.example.com\t443\t/v1/**\tprovider:access_token", + "grant-token", + ); + + let ctx = L7EvalContext { + host: "api.example.com".into(), + port: 443, + policy_name: "api".into(), + binary_path: "/usr/bin/curl".into(), + ancestors: vec![], + cmdline_paths: vec![], + secret_resolver: None, + activity_tx: None, + dynamic_credentials: Some(fixture.dynamic_credentials()), + token_grant_resolver: Some(fixture.resolver()), + ..Default::default() + }; + let req = L7Request { + action: "GET".to_string(), + target: "/v1/projects".to_string(), + query_params: std::collections::HashMap::new(), + raw_header: b"GET /v1/projects HTTP/1.1\r\nHost: api.example.com\r\n\r\n".to_vec(), + body_length: BodyLength::None, + }; + + let rewritten = inject_if_needed(req, &ctx) + .await + .expect("fake token exchange grant should inject"); + let rewritten = + String::from_utf8(rewritten.raw_header).expect("rewritten request should be UTF-8"); + + assert!(rewritten.contains("Authorization: Bearer grant-token\r\n")); + fixture.assert_one_token_exchange_request( + "api.example.com\t443\t/v1/**\tprovider:access_token", + ); + } + #[tokio::test] async fn inject_if_needed_rejects_malformed_resolver_token() { let fixture = TokenGrantTestFixture::success( diff --git a/crates/openshell-supervisor-network/src/lib.rs b/crates/openshell-supervisor-network/src/lib.rs index f5d0205e3a..2f4c9e2638 100644 --- a/crates/openshell-supervisor-network/src/lib.rs +++ b/crates/openshell-supervisor-network/src/lib.rs @@ -17,7 +17,6 @@ pub mod procfs; pub mod proxy; pub mod run; pub mod sigv4; -mod spiffe_endpoint; mod token_grant; pub mod upstream_proxy; diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index 6104e0f66b..b5f4c99115 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -20,7 +20,7 @@ use openshell_core::net::{ set_tcp_nodelay_best_effort, }; use openshell_core::policy::ProxyPolicy; -use openshell_core::provider_credentials::ProviderCredentialState; +use openshell_core::provider_credentials::{ProviderCredentialSnapshot, ProviderCredentialState}; use openshell_core::secrets::{self, SecretResolver, rewrite_header_line_checked}; use openshell_ocsf::{ ActionId, ActivityId, DispositionId, Endpoint, HttpActivityBuilder, HttpRequest, @@ -96,6 +96,27 @@ const HOST_GATEWAY_ALIASES: &[&str] = &[ "host.docker.internal", ]; +fn revision_scoped_dynamic_credentials( + snapshot: &ProviderCredentialSnapshot, +) -> std::collections::HashMap { + snapshot + .dynamic_credentials + .iter() + .map(|(key, credential)| { + let scoped_key = key.rsplit_once('\t').map_or_else( + || format!("rev:{}\t{key}", snapshot.revision), + |(endpoint_selector, provider_credential)| { + format!( + "{endpoint_selector}\trev:{}\t{provider_credential}", + snapshot.revision + ) + }, + ); + (scoped_key, credential.clone()) + }) + .collect() +} + /// Cloud instance metadata IPs that are NEVER exempted from SSRF blocking, /// even when they coincidentally match a host-gateway alias resolution. /// This list covers the well-known IMDS endpoints across major cloud providers. @@ -356,9 +377,9 @@ impl ProxyHandle { .as_ref() .and_then(ProviderCredentialState::resolver); let dynamic_credentials = provider_credentials.as_ref().map(|state| { - Arc::new(std::sync::RwLock::new( - state.snapshot().dynamic_credentials.clone(), - )) + Arc::new(std::sync::RwLock::new(revision_scoped_dynamic_credentials( + &state.snapshot(), + ))) }); let dtx = denial_tx.clone(); let atx = activity_tx.clone(); @@ -5915,6 +5936,29 @@ network_policies: ); } + #[test] + fn revision_scoped_dynamic_credentials_preserves_endpoint_selector_and_adds_revision() { + let mut dynamic_credentials = std::collections::HashMap::new(); + dynamic_credentials.insert( + "api.example.test\t443\t/v1/**\tprovider:access_token".to_string(), + openshell_core::proto::ProviderProfileCredential { + name: "access_token".to_string(), + ..Default::default() + }, + ); + let snapshot = ProviderCredentialSnapshot { + revision: 42, + child_env: std::collections::HashMap::new(), + dynamic_credentials, + }; + + let scoped = revision_scoped_dynamic_credentials(&snapshot); + + assert!( + scoped.contains_key("api.example.test\t443\t/v1/**\trev:42\tprovider:access_token") + ); + } + #[test] fn connect_activity_is_skipped_when_l7_will_count_the_request() { let (tx, mut rx) = mpsc::channel(4); @@ -6408,20 +6452,7 @@ network_policies: crate::l7::token_grant_injection::test_support::TokenGrantTestFixture, ) { let provider_key = "api.example.test\t8080\t/v1/**\tprovider:access_token"; - let fixture = match resolver_response { - Ok(token) => { - crate::l7::token_grant_injection::test_support::TokenGrantTestFixture::success( - provider_key, - token, - ) - } - Err(error) => { - crate::l7::token_grant_injection::test_support::TokenGrantTestFixture::failure( - provider_key, - error, - ) - } - }; + let fixture = forward_token_grant_fixture(provider_key, resolver_response, false); let ctx = crate::l7::relay::L7EvalContext { host: "api.example.test".into(), port: 8080, @@ -6439,6 +6470,54 @@ network_policies: (ctx, fixture) } + fn forward_token_exchange_context( + resolver_response: std::result::Result<&str, &str>, + ) -> ( + crate::l7::relay::L7EvalContext, + crate::l7::token_grant_injection::test_support::TokenGrantTestFixture, + ) { + let (mut ctx, _) = forward_token_grant_context(Ok("unused-token")); + let provider_key = "api.example.test\t8080\t/v1/**\tprovider:access_token"; + let fixture = forward_token_grant_fixture(provider_key, resolver_response, true); + ctx.dynamic_credentials = Some(fixture.dynamic_credentials()); + ctx.token_grant_resolver = Some(fixture.resolver()); + + (ctx, fixture) + } + + fn forward_token_grant_fixture( + provider_key: &str, + resolver_response: std::result::Result<&str, &str>, + token_exchange: bool, + ) -> crate::l7::token_grant_injection::test_support::TokenGrantTestFixture { + match (resolver_response, token_exchange) { + (Ok(token), false) => { + crate::l7::token_grant_injection::test_support::TokenGrantTestFixture::success( + provider_key, + token, + ) + } + (Ok(token), true) => { + crate::l7::token_grant_injection::test_support::TokenGrantTestFixture::success_token_exchange( + provider_key, + token, + ) + } + (Err(error), false) => { + crate::l7::token_grant_injection::test_support::TokenGrantTestFixture::failure( + provider_key, + error, + ) + } + (Err(error), true) => { + crate::l7::token_grant_injection::test_support::TokenGrantTestFixture::failure_token_exchange( + provider_key, + error, + ) + } + } + } + fn authorization_header_count(headers: &str) -> usize { headers .lines() @@ -9192,6 +9271,34 @@ network_policies: fixture.assert_one_request("api.example.test\t8080\t/v1/**\tprovider:access_token"); } + #[tokio::test] + async fn forward_proxy_injects_token_exchange_before_rewriting_request() { + let (ctx, fixture) = forward_token_exchange_context(Ok("grant-token")); + let raw = b"GET http://api.example.test:8080/v1/projects HTTP/1.1\r\nHost: api.example.test:8080\r\nAuthorization: Bearer stale-token\r\nConnection: close\r\n\r\n".to_vec(); + + let with_token = inject_token_grant_for_forward_request("GET", "/v1/projects", raw, &ctx) + .await + .expect("forward token exchange should inject"); + let rewritten = rewrite_forward_request( + &with_token, + with_token.len(), + "/v1/projects", + "api.example.test:8080", + None, + false, + ) + .expect("forward request should rewrite"); + let rewritten = String::from_utf8_lossy(&rewritten); + + assert!(rewritten.starts_with("GET /v1/projects HTTP/1.1\r\n")); + assert!(rewritten.contains("Authorization: Bearer grant-token\r\n")); + assert!(!rewritten.contains("stale-token")); + assert_eq!(authorization_header_count(&rewritten), 1); + fixture.assert_one_token_exchange_request( + "api.example.test\t8080\t/v1/**\tprovider:access_token", + ); + } + #[tokio::test] async fn forward_proxy_token_grant_failure_returns_error_before_rewrite() { let (ctx, fixture) = forward_token_grant_context(Err("oauth unavailable")); @@ -9206,6 +9313,22 @@ network_policies: fixture.assert_one_request("api.example.test\t8080\t/v1/**\tprovider:access_token"); } + #[tokio::test] + async fn forward_proxy_token_exchange_failure_returns_error_before_rewrite() { + let (ctx, fixture) = forward_token_exchange_context(Err("oauth unavailable")); + let raw = b"GET http://api.example.test:8080/v1/projects HTTP/1.1\r\nHost: api.example.test:8080\r\nConnection: close\r\n\r\n".to_vec(); + + let err = inject_token_grant_for_forward_request("GET", "/v1/projects", raw, &ctx) + .await + .expect_err("forward token exchange failure should stop request rewriting"); + + assert!(err.to_string().contains("Token grant failed")); + assert!(err.to_string().contains("oauth unavailable")); + fixture.assert_one_token_exchange_request( + "api.example.test\t8080\t/v1/**\tprovider:access_token", + ); + } + #[test] fn test_rewrite_get_request() { let raw = diff --git a/crates/openshell-supervisor-network/src/spiffe_endpoint.rs b/crates/openshell-supervisor-network/src/spiffe_endpoint.rs deleted file mode 100644 index 4494626275..0000000000 --- a/crates/openshell-supervisor-network/src/spiffe_endpoint.rs +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::path::Path; - -/// Convert a path to a SPIFFE Workload API endpoint URL. -/// -/// If the path already has a scheme (`unix:` or `tcp:`), use it as-is. -/// Otherwise, assume it is a Unix socket path and prepend `unix:`. -pub fn workload_api_endpoint(path: &Path) -> String { - let path = path.to_string_lossy(); - if path.starts_with("unix:") || path.starts_with("tcp:") { - path.into_owned() - } else { - format!("unix:{path}") - } -} diff --git a/crates/openshell-supervisor-network/src/token_grant.rs b/crates/openshell-supervisor-network/src/token_grant.rs index 03e9bfb398..15aea6fccf 100644 --- a/crates/openshell-supervisor-network/src/token_grant.rs +++ b/crates/openshell-supervisor-network/src/token_grant.rs @@ -34,13 +34,16 @@ use std::collections::HashMap; use std::future::Future; -use std::net::IpAddr; use std::sync::{Arc, LazyLock, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use miette::{IntoDiagnostic, Result, WrapErr}; +use openshell_core::oauth::{ + self, ACCESS_TOKEN_TYPE, OAuthTokenResponse, TokenExchangeParams, TokenGrantParams, + effective_client_assertion_type, effective_token_type, +}; +use openshell_core::proto::ProviderCredentialTokenGrantType; use openshell_core::sandbox_env; -use serde::Deserialize; use spiffe::WorkloadApiClient; /// Token cache shared across all provider token grants. @@ -54,32 +57,9 @@ static TOKEN_GRANT_HTTP_CLIENT: LazyLock = LazyLock::new(|| { .build() .expect("token grant HTTP client configuration should be valid") }); -const MAX_OAUTH_ERROR_FIELD_LEN: usize = 256; const DEFAULT_TOKEN_CACHE_TTL_SECONDS: i64 = 300; const TOKEN_CACHE_EXPIRY_SKEW_SECONDS: i64 = 30; const MAX_TOKEN_EXPIRES_IN_SECONDS: i64 = 3600; -const DEFAULT_CLIENT_ASSERTION_TYPE: &str = - "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; - -/// `OAuth2` token response from the authorization server. -#[derive(Debug, Clone, Deserialize)] -struct TokenResponse { - access_token: String, - #[serde(default)] - #[allow(dead_code)] - token_type: String, - #[serde(default)] - expires_in: i64, - #[serde(default)] - #[allow(dead_code)] - scope: String, -} - -#[derive(Debug, Deserialize)] -struct OAuthErrorResponse { - error: Option, - error_description: Option, -} /// Cached access token with expiration metadata. #[derive(Debug, Clone)] @@ -153,41 +133,98 @@ impl TokenCache { /// - JWT-SVID fetch fails /// - Token service request fails /// - Token response is invalid -pub async fn obtain_provider_token( - provider_name: &str, - token_endpoint: &str, - jwt_svid_audience: &str, - client_assertion_type: &str, - audience: &str, - scopes: &[String], - cache_ttl_override: i64, -) -> Result { +pub struct ObtainProviderTokenRequest<'a> { + pub provider_name: &'a str, + pub token_endpoint: &'a str, + pub jwt_svid_audience: &'a str, + pub client_assertion_type: &'a str, + pub audience: &'a str, + pub scopes: &'a [String], + pub cache_ttl_override: i64, + pub grant_type: i32, + pub requested_token_type: &'a str, +} + +pub async fn obtain_provider_token(request: ObtainProviderTokenRequest<'_>) -> Result { + let grant_type = + ProviderCredentialTokenGrantType::try_from(request.grant_type).map_err(|_| { + tracing::error!( + grant_type = request.grant_type, + provider = request.provider_name, + "unrecognised token grant_type" + ); + miette::miette!( + "unrecognised token grant_type {} for provider credential", + request.grant_type + ) + })?; obtain_provider_token_with_grant( ObtainProviderTokenInput { cache: &TOKEN_CACHE, - provider_name, - token_endpoint, - jwt_svid_audience, - client_assertion_type, - audience, - scopes, - cache_ttl_override, + provider_name: request.provider_name, + token_endpoint: request.token_endpoint, + jwt_svid_audience: request.jwt_svid_audience, + client_assertion_type: request.client_assertion_type, + audience: request.audience, + scopes: request.scopes, + cache_ttl_override: request.cache_ttl_override, + grant_type, + requested_token_type: request.requested_token_type, }, |jwt_audience| async move { // Fetch JWT-SVID with authorization server as audience // For RFC 7523, the JWT assertion's aud claim identifies the issuer/realm let jwt_svid = fetch_jwt_svid_for_token_grant(&jwt_audience).await?; - // Perform OAuth2 JWT client assertion grant - // The audience parameter in the token request specifies the resource server - perform_token_grant( - token_endpoint, - &jwt_svid, - client_assertion_type, - audience, - scopes, - ) - .await + match grant_type { + ProviderCredentialTokenGrantType::ClientCredentials + | ProviderCredentialTokenGrantType::Unspecified => { + // Perform OAuth2 JWT client assertion grant. The audience + // parameter in the token request specifies the resource server. + perform_token_grant( + request.token_endpoint, + &jwt_svid, + request.client_assertion_type, + request.audience, + request.scopes, + ) + .await + } + ProviderCredentialTokenGrantType::TokenExchange => { + let (provider, credential_key) = + parse_provider_credential_key(request.provider_name)?; + let endpoint = supervisor_gateway_endpoint_from_env()?; + let sandbox_id = supervisor_sandbox_id_from_env()?; + let intermediate = + openshell_core::grpc_client::exchange_provider_subject_token( + &endpoint, + &sandbox_id, + provider, + credential_key, + &jwt_svid, + ) + .await + .map_err(|err| { + miette::miette!( + "gateway intermediate provider token exchange failed: {err}" + ) + })?; + validate_access_token(&intermediate.access_token)?; + perform_token_exchange( + request.token_endpoint, + &TokenExchangeParams { + client_assertion: &jwt_svid, + client_assertion_type: request.client_assertion_type, + subject_token: &intermediate.access_token, + subject_token_type: ACCESS_TOKEN_TYPE, + audience: request.audience, + scopes: request.scopes, + requested_token_type: request.requested_token_type, + }, + ) + .await + } + } }, ) .await @@ -202,6 +239,8 @@ struct ObtainProviderTokenInput<'a> { audience: &'a str, scopes: &'a [String], cache_ttl_override: i64, + grant_type: ProviderCredentialTokenGrantType, + requested_token_type: &'a str, } async fn obtain_provider_token_with_grant( @@ -210,34 +249,30 @@ async fn obtain_provider_token_with_grant( ) -> Result where F: FnOnce(String) -> Fut, - Fut: Future>, + Fut: Future>, { - // Derive authorization server audience from token endpoint - // For Keycloak: https://auth.example.com/realms/openshell/protocol/openid-connect/token - // -> https://auth.example.com/realms/openshell let jwt_audience = effective_jwt_svid_audience(input.token_endpoint, input.jwt_svid_audience); - let cache_key = token_cache_key( - input.provider_name, - input.token_endpoint, - &jwt_audience, - effective_client_assertion_type(input.client_assertion_type), - input.audience, - input.scopes, - ); + let cache_key = token_cache_key(TokenCacheKeyInput { + provider_name: input.provider_name, + token_endpoint: input.token_endpoint, + jwt_svid_audience: &jwt_audience, + client_assertion_type: effective_client_assertion_type(input.client_assertion_type), + audience: input.audience, + scopes: input.scopes, + grant_type: input.grant_type, + requested_token_type: effective_token_type(input.requested_token_type), + }); - // Check cache first if let Some(cached) = input.cache.get(&cache_key) { return Ok(cached); } let token_response = grant(jwt_audience).await?; - validate_access_token(&token_response.access_token)?; let cache_ttl_seconds = token_cache_ttl_seconds(input.cache_ttl_override, token_response.expires_in); let expires_at_ms = current_time_ms().saturating_add(cache_ttl_seconds.saturating_mul(1000)); - // Cache the token input.cache.set( cache_key, token_response.access_token.clone(), @@ -256,7 +291,7 @@ async fn fetch_jwt_svid_for_token_grant(audience: &str) -> Result { let socket_path = provider_spiffe_workload_api_socket_from_env()?; let endpoint = - crate::spiffe_endpoint::workload_api_endpoint(std::path::Path::new(&socket_path)); + openshell_core::spiffe::workload_api_endpoint(std::path::Path::new(&socket_path)); // Connect to SPIRE agent let client = WorkloadApiClient::connect_to(&endpoint) @@ -287,153 +322,34 @@ fn provider_spiffe_workload_api_socket_from_env() -> Result { }) } -/// Perform `OAuth2` JWT client assertion grant. -/// -/// POSTs to the token endpoint with: -/// - `grant_type=client_credentials` -/// - `client_assertion_type=` -/// - `client_assertion=` (client identity is in the JWT's `sub` claim) -/// - `audience=` (if provided) -/// - `scope=` (if provided) -/// -/// Note: `client_id` is NOT included - the client is identified by the `sub` claim -/// in the JWT-SVID itself. async fn perform_token_grant( token_endpoint: &str, jwt_svid: &str, client_assertion_type: &str, audience: &str, scopes: &[String], -) -> Result { - let token_endpoint_url = parse_token_endpoint_url(token_endpoint)?; - let client_assertion_type = effective_client_assertion_type(client_assertion_type); - let mut form_params = vec![ - ("grant_type", "client_credentials"), - ("client_assertion_type", client_assertion_type), - ("client_assertion", jwt_svid), - ]; - - // Add audience if provided - let audience_param; - if !audience.is_empty() { - audience_param = audience.to_string(); - form_params.push(("audience", &audience_param)); - } - - // Add scopes if provided - let scope_param; - if !scopes.is_empty() { - scope_param = scopes.join(" "); - form_params.push(("scope", &scope_param)); - } - - // POST to token endpoint - let response = TOKEN_GRANT_HTTP_CLIENT - .post(token_endpoint_url) - .form(&form_params) - .send() - .await - .into_diagnostic() - .wrap_err_with(|| format!("failed to POST to token endpoint {token_endpoint}"))?; - - // Check response status - if !response.status().is_success() { - let status = response.status(); - let body = response - .text() - .await - .unwrap_or_else(|_| "".to_string()); - return Err(miette::miette!( - "{}", - token_grant_failure_message(status, &body) - )); - } - - // Parse token response - let token_response = response - .json::() - .await - .into_diagnostic() - .wrap_err("failed to parse token response as JSON")?; - validate_access_token(&token_response.access_token)?; - Ok(token_response) -} - -fn parse_token_endpoint_url(token_endpoint: &str) -> Result { - let url = reqwest::Url::parse(token_endpoint) - .into_diagnostic() - .wrap_err("token_endpoint must be an absolute URL")?; - if token_endpoint_transport_allowed(&url) { - return Ok(url); - } - - Err(miette::miette!( - "token_endpoint must use https, except http for loopback or in-cluster service hosts" - )) -} - -fn token_endpoint_transport_allowed(url: &reqwest::Url) -> bool { - match url.scheme() { - "https" => true, - "http" => url - .host_str() - .is_some_and(|host| is_loopback_host(host) || is_kubernetes_service_host(host)), - _ => false, - } -} - -fn is_loopback_host(host: &str) -> bool { - let host = host.trim_matches(['[', ']']); - if host.eq_ignore_ascii_case("localhost") { - return true; - } - - match host.parse::() { - Ok(IpAddr::V4(v4)) => v4.is_loopback(), - Ok(IpAddr::V6(v6)) => { - v6.is_loopback() || v6.to_ipv4_mapped().is_some_and(|v4| v4.is_loopback()) - } - Err(_) => false, - } -} - -fn is_kubernetes_service_host(host: &str) -> bool { - let host = host.trim_end_matches('.').to_ascii_lowercase(); - let labels = host.split('.').collect::>(); - let is_service_name = labels.len() == 3 && labels[2] == "svc"; - let is_cluster_local_service = - labels.len() == 5 && labels[2] == "svc" && labels[3] == "cluster" && labels[4] == "local"; - (is_service_name || is_cluster_local_service) && labels.iter().all(|label| !label.is_empty()) -} - -pub fn validate_access_token(token: &str) -> Result<()> { - if token.is_empty() || !is_token68(token) { - return Err(miette::miette!( - "token grant returned a malformed access token" - )); - } - Ok(()) +) -> Result { + oauth::post_oauth_token_grant( + &TOKEN_GRANT_HTTP_CLIENT, + token_endpoint, + &TokenGrantParams { + client_assertion: jwt_svid, + client_assertion_type, + audience, + scopes, + }, + ) + .await } -fn is_token68(token: &str) -> bool { - let mut padding_started = false; - let mut saw_value = false; - for byte in token.bytes() { - if byte == b'=' { - padding_started = true; - continue; - } - if padding_started || !is_token68_value_byte(byte) { - return false; - } - saw_value = true; - } - saw_value +async fn perform_token_exchange( + token_endpoint: &str, + params: &TokenExchangeParams<'_>, +) -> Result { + oauth::post_oauth_token_exchange(&TOKEN_GRANT_HTTP_CLIENT, token_endpoint, params).await } -fn is_token68_value_byte(byte: u8) -> bool { - byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'+' | b'/') -} +pub use oauth::validate_access_token; fn token_cache_ttl_seconds(cache_ttl_override: i64, expires_in: i64) -> i64 { if cache_ttl_override > 0 { @@ -483,73 +399,52 @@ fn effective_jwt_svid_audience(token_endpoint: &str, jwt_svid_audience: &str) -> } } -fn effective_client_assertion_type(client_assertion_type: &str) -> &str { - if client_assertion_type.trim().is_empty() { - DEFAULT_CLIENT_ASSERTION_TYPE - } else { - client_assertion_type - } +fn supervisor_gateway_endpoint_from_env() -> Result { + std::env::var(sandbox_env::ENDPOINT) + .ok() + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| miette::miette!("{} not set", sandbox_env::ENDPOINT)) } -fn token_cache_key( - provider_name: &str, - token_endpoint: &str, - jwt_svid_audience: &str, - client_assertion_type: &str, - audience: &str, - scopes: &[String], -) -> String { - format!( - "{}\t{}\t{}\t{}\t{}\t{}", - provider_name, - token_endpoint, - jwt_svid_audience, - client_assertion_type, - audience, - scopes.join(" ") - ) +fn supervisor_sandbox_id_from_env() -> Result { + std::env::var(sandbox_env::SANDBOX_ID) + .ok() + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| miette::miette!("{} not set", sandbox_env::SANDBOX_ID)) } -fn token_grant_failure_message(status: reqwest::StatusCode, body: &str) -> String { - let Ok(error_response) = serde_json::from_str::(body) else { - return format!("token grant failed with status {status}"); - }; +fn parse_provider_credential_key(key: &str) -> Result<(&str, &str)> { + let provider_and_credential = key + .rsplit_once('\t') + .map_or(key, |(_, provider_and_credential)| provider_and_credential); + provider_and_credential.split_once(':').ok_or_else(|| { + miette::miette!("dynamic token grant key is missing provider credential identity") + }) +} - let error = error_response - .error - .as_deref() - .map(sanitize_oauth_error_field) - .filter(|value| !value.is_empty()); - let description = error_response - .error_description - .as_deref() - .map(sanitize_oauth_error_field) - .filter(|value| !value.is_empty()); - - match (error, description) { - (Some(error), Some(description)) => { - format!( - "token grant failed with status {status}: error={error}; error_description={description}" - ) - } - (Some(error), None) => { - format!("token grant failed with status {status}: error={error}") - } - (None, Some(description)) => { - format!("token grant failed with status {status}: error_description={description}") - } - (None, None) => format!("token grant failed with status {status}"), - } +struct TokenCacheKeyInput<'a> { + provider_name: &'a str, + token_endpoint: &'a str, + jwt_svid_audience: &'a str, + client_assertion_type: &'a str, + audience: &'a str, + scopes: &'a [String], + grant_type: ProviderCredentialTokenGrantType, + requested_token_type: &'a str, } -fn sanitize_oauth_error_field(value: &str) -> String { - value - .chars() - .map(|ch| if ch.is_control() { ' ' } else { ch }) - .take(MAX_OAUTH_ERROR_FIELD_LEN) - .collect::() - .trim() - .to_string() +fn token_cache_key(input: TokenCacheKeyInput<'_>) -> String { + format!( + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + input.provider_name, + input.token_endpoint, + input.jwt_svid_audience, + input.client_assertion_type, + input.audience, + input.scopes.join(" "), + input.grant_type as i32, + input.requested_token_type + ) } /// Get current Unix timestamp in milliseconds. @@ -564,6 +459,7 @@ fn current_time_ms() -> i64 { #[cfg(test)] mod tests { use super::*; + use openshell_core::oauth::{ACCESS_TOKEN_TYPE, DEFAULT_CLIENT_ASSERTION_TYPE}; use std::collections::HashMap; use std::sync::{ Arc, @@ -778,16 +674,17 @@ mod tests { audience: input.audience, scopes: input.scopes, cache_ttl_override: input.cache_ttl_override, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, + requested_token_type: "", }, move |_| { let grant_calls = input.grant_calls.clone(); async move { let call = grant_calls.fetch_add(1, Ordering::SeqCst) + 1; - Ok(TokenResponse { + Ok(OAuthTokenResponse { access_token: format!("token-{call}"), token_type: "Bearer".to_string(), expires_in: input.expires_in, - scope: input.scopes.join(" "), }) } }, @@ -814,6 +711,8 @@ mod tests { audience, scopes, cache_ttl_override, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, + requested_token_type: "", }, |_| async { Err(miette::miette!("grant should not be called on cache hit")) }, ) @@ -855,103 +754,98 @@ mod tests { } #[test] - fn validate_access_token_accepts_token68_values() { - for token in [ - "abcXYZ123-._~+/", - "eyJhbGciOiJSUzI1NiJ9.payload.sig", - "token==", - ] { - validate_access_token(token).expect("token68 bearer token should be accepted"); - } - } + fn token_cache_key_varies_by_resource_audience_and_scopes() { + let provider_name = "alpha.default.svc.cluster.local\t80\t\tprovider:access_token"; + let token_endpoint = + "https://auth.example.com/realms/openshell/protocol/openid-connect/token"; + let jwt_svid_audience = "https://auth.example.com/realms/openshell"; + let alpha_scopes = ["alpha".to_string()]; + let delta_scopes = ["delta".to_string()]; + let base = token_cache_key(TokenCacheKeyInput { + provider_name, + token_endpoint, + jwt_svid_audience, + client_assertion_type: DEFAULT_CLIENT_ASSERTION_TYPE, + audience: "alpha", + scopes: &alpha_scopes, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, + requested_token_type: ACCESS_TOKEN_TYPE, + }); + let different_audience = token_cache_key(TokenCacheKeyInput { + provider_name, + token_endpoint, + jwt_svid_audience, + client_assertion_type: DEFAULT_CLIENT_ASSERTION_TYPE, + audience: "delta", + scopes: &alpha_scopes, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, + requested_token_type: ACCESS_TOKEN_TYPE, + }); + let different_scopes = token_cache_key(TokenCacheKeyInput { + provider_name, + token_endpoint, + jwt_svid_audience, + client_assertion_type: DEFAULT_CLIENT_ASSERTION_TYPE, + audience: "alpha", + scopes: &delta_scopes, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, + requested_token_type: ACCESS_TOKEN_TYPE, + }); + let different_assertion_type = token_cache_key(TokenCacheKeyInput { + provider_name, + token_endpoint, + jwt_svid_audience, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe", + audience: "alpha", + scopes: &alpha_scopes, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, + requested_token_type: ACCESS_TOKEN_TYPE, + }); - #[test] - fn validate_access_token_rejects_header_injection_and_non_token68_values() { - for token in [ - "", - "token with spaces", - "token\r\nX-Injected: yes", - "token\u{7f}", - "tokené", - "token=continued", - "==", - ] { - let err = validate_access_token(token) - .expect_err("malformed bearer token should be rejected"); - assert_eq!( - err.to_string(), - "token grant returned a malformed access token" - ); - } + assert_ne!(base, different_audience); + assert_ne!(base, different_scopes); + assert_ne!(base, different_assertion_type); } #[test] - fn token_endpoint_url_allows_https_loopback_and_in_cluster_http() { - for endpoint in [ - "https://auth.example.com/token", - "http://127.0.0.1:8080/token", - "http://[::1]:8080/token", - "http://token-issuer.default.svc.cluster.local/token", - "http://token-issuer.default.svc/token", - ] { - parse_token_endpoint_url(endpoint).expect("token endpoint should be allowed"); - } - } + fn token_cache_key_varies_by_provider_env_revision_prefix() { + let token_endpoint = + "https://auth.example.com/realms/openshell/protocol/openid-connect/token"; + let jwt_svid_audience = "https://auth.example.com/realms/openshell"; + let scopes = ["alpha".to_string()]; + let revision_one = token_cache_key(TokenCacheKeyInput { + provider_name: "api.example.test\t443\t/v1/**\trev:1\tprovider:access_token", + token_endpoint, + jwt_svid_audience, + client_assertion_type: DEFAULT_CLIENT_ASSERTION_TYPE, + audience: "alpha", + scopes: &scopes, + grant_type: ProviderCredentialTokenGrantType::TokenExchange, + requested_token_type: ACCESS_TOKEN_TYPE, + }); + let revision_two = token_cache_key(TokenCacheKeyInput { + provider_name: "api.example.test\t443\t/v1/**\trev:2\tprovider:access_token", + token_endpoint, + jwt_svid_audience, + client_assertion_type: DEFAULT_CLIENT_ASSERTION_TYPE, + audience: "alpha", + scopes: &scopes, + grant_type: ProviderCredentialTokenGrantType::TokenExchange, + requested_token_type: ACCESS_TOKEN_TYPE, + }); - #[test] - fn token_endpoint_url_rejects_plain_http_non_cluster_hosts() { - for endpoint in [ - "http://auth.example.com/token", - "http://keycloak/realms/openshell/protocol/openid-connect/token", - "http://token-issuer.default.svc.evil.com/token", - "ftp://auth.example.com/token", - "/relative/token", - ] { - assert!( - parse_token_endpoint_url(endpoint).is_err(), - "token endpoint should be rejected: {endpoint}" - ); - } + assert_ne!(revision_one, revision_two); } #[test] - fn token_cache_key_varies_by_resource_audience_and_scopes() { - let base = token_cache_key( - "alpha.default.svc.cluster.local\t80\t\tprovider:access_token", - "https://auth.example.com/realms/openshell/protocol/openid-connect/token", - "https://auth.example.com/realms/openshell", - DEFAULT_CLIENT_ASSERTION_TYPE, - "alpha", - &["alpha".to_string()], - ); - let different_audience = token_cache_key( - "alpha.default.svc.cluster.local\t80\t\tprovider:access_token", - "https://auth.example.com/realms/openshell/protocol/openid-connect/token", - "https://auth.example.com/realms/openshell", - DEFAULT_CLIENT_ASSERTION_TYPE, - "delta", - &["alpha".to_string()], - ); - let different_scopes = token_cache_key( - "alpha.default.svc.cluster.local\t80\t\tprovider:access_token", - "https://auth.example.com/realms/openshell/protocol/openid-connect/token", - "https://auth.example.com/realms/openshell", - DEFAULT_CLIENT_ASSERTION_TYPE, - "alpha", - &["delta".to_string()], - ); - let different_assertion_type = token_cache_key( - "alpha.default.svc.cluster.local\t80\t\tprovider:access_token", - "https://auth.example.com/realms/openshell/protocol/openid-connect/token", - "https://auth.example.com/realms/openshell", - "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe", - "alpha", - &["alpha".to_string()], + fn provider_credential_key_parser_ignores_revision_segment() { + assert_eq!( + parse_provider_credential_key( + "api.example.test\t443\t/v1/**\trev:42\tprovider:access_token" + ) + .expect("parse provider credential key"), + ("provider", "access_token") ); - - assert_ne!(base, different_audience); - assert_ne!(base, different_scopes); - assert_ne!(base, different_assertion_type); } #[test] @@ -1077,14 +971,16 @@ mod tests { let jwt_svid_audience = "https://auth.example.com"; let audience = "api://resource"; - let cache_key = token_cache_key( + let cache_key = token_cache_key(TokenCacheKeyInput { provider_name, token_endpoint, jwt_svid_audience, - DEFAULT_CLIENT_ASSERTION_TYPE, + client_assertion_type: DEFAULT_CLIENT_ASSERTION_TYPE, audience, - &scopes, - ); + scopes: &scopes, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, + requested_token_type: ACCESS_TOKEN_TYPE, + }); cache.set( cache_key, "expired-token".to_string(), @@ -1109,57 +1005,6 @@ mod tests { assert_eq!(grant_calls.load(Ordering::SeqCst), 1); } - #[tokio::test] - async fn obtain_provider_token_rejects_malformed_token_before_cache() { - let cache = TokenCache::new(); - let scopes = vec!["read".to_string()]; - let provider_name = "api.example.test\t443\t/v1/**\tprovider:access_token"; - let token_endpoint = "https://auth.example.com/token"; - let jwt_svid_audience = "https://auth.example.com"; - let audience = "api://resource"; - - let err = obtain_provider_token_with_grant( - ObtainProviderTokenInput { - cache: &cache, - provider_name, - token_endpoint, - jwt_svid_audience, - client_assertion_type: DEFAULT_CLIENT_ASSERTION_TYPE, - audience, - scopes: &scopes, - cache_ttl_override: 0, - }, - |_| async { - Ok(TokenResponse { - access_token: "access-123\r\nX-Injected: yes".to_string(), - token_type: "Bearer".to_string(), - expires_in: 60, - scope: "read".to_string(), - }) - }, - ) - .await - .expect_err("malformed access token should fail before caching"); - - let cache_key = token_cache_key( - provider_name, - token_endpoint, - jwt_svid_audience, - DEFAULT_CLIENT_ASSERTION_TYPE, - audience, - &scopes, - ); - - assert_eq!( - err.to_string(), - "token grant returned a malformed access token" - ); - assert!( - cache.get(&cache_key).is_none(), - "malformed access token must not be cached" - ); - } - #[tokio::test] async fn obtain_provider_token_cache_ttl_override_extends_zero_expires_in() { let cache = TokenCache::new(); @@ -1382,43 +1227,4 @@ mod tests { .contains("failed to parse token response as JSON") ); } - - #[test] - fn token_grant_failure_message_reports_oauth_error_fields() { - let message = token_grant_failure_message( - reqwest::StatusCode::UNAUTHORIZED, - r#"{"error":"invalid_client","error_description":"Invalid client credentials"}"#, - ); - - assert_eq!( - message, - "token grant failed with status 401 Unauthorized: error=invalid_client; error_description=Invalid client credentials" - ); - } - - #[test] - fn token_grant_failure_message_omits_unstructured_response_body() { - let message = token_grant_failure_message( - reqwest::StatusCode::INTERNAL_SERVER_ERROR, - "internal error containing implementation details", - ); - - assert_eq!( - message, - "token grant failed with status 500 Internal Server Error" - ); - } - - #[test] - fn token_grant_failure_message_sanitizes_oauth_error_fields() { - let long_description = "a".repeat(MAX_OAUTH_ERROR_FIELD_LEN + 20); - let body = - format!(r#"{{"error":"invalid_client\n","error_description":"{long_description}"}}"#); - let message = token_grant_failure_message(reqwest::StatusCode::UNAUTHORIZED, &body); - - assert!(!message.contains('\n')); - assert!(message.contains("error=invalid_client")); - assert!(message.contains(&"a".repeat(MAX_OAUTH_ERROR_FIELD_LEN))); - assert!(!message.contains(&"a".repeat(MAX_OAUTH_ERROR_FIELD_LEN + 1))); - } } diff --git a/crates/openshell-supervisor-process/src/process.rs b/crates/openshell-supervisor-process/src/process.rs index 659fe3dc06..2193a6d5d6 100644 --- a/crates/openshell-supervisor-process/src/process.rs +++ b/crates/openshell-supervisor-process/src/process.rs @@ -9,14 +9,14 @@ use crate::managed_children; #[cfg(target_os = "linux")] use crate::netns::NetworkNamespace; use crate::sandbox; -use miette::{IntoDiagnostic, Result}; +use miette::{IntoDiagnostic, Result, WrapErr}; use nix::sys::signal::{self, Signal}; use nix::unistd::{Gid, Group, Pid, Uid, User}; use openshell_core::policy::{NetworkMode, SandboxPolicy}; use std::collections::HashMap; use std::ffi::CString; #[cfg(target_os = "linux")] -use std::os::fd::{AsRawFd, OwnedFd, RawFd}; +use std::os::fd::RawFd; #[cfg(target_os = "linux")] use std::os::unix::ffi::OsStrExt; #[cfg(unix)] @@ -27,6 +27,8 @@ use std::path::PathBuf; use std::process::Stdio; #[cfg(target_os = "linux")] use std::sync::OnceLock; +#[cfg(target_os = "linux")] +use std::sync::mpsc; use tokio::process::{Child, Command}; use tracing::{debug, info}; @@ -321,11 +323,13 @@ static SUPERVISOR_IDENTITY_MOUNT_NS: OnceLock, } #[cfg(target_os = "linux")] type SupervisorIdentityNsRef = &'static SupervisorIdentityMountNamespace; +#[cfg(target_os = "linux")] +type SupervisorIdentitySpawnJob = Box; #[cfg(target_os = "linux")] impl SupervisorIdentityMountNamespace { @@ -334,13 +338,9 @@ impl SupervisorIdentityMountNamespace { return Ok(None); }; Ok(Some(Self { - fd: create_supervisor_identity_mount_namespace(&target)?, + spawn_tx: start_supervisor_identity_spawn_worker(target)?, })) } - - pub fn enter_for_child(&self) -> std::io::Result<()> { - set_mount_namespace(self.fd.as_raw_fd()) - } } #[cfg(target_os = "linux")] @@ -371,6 +371,100 @@ pub fn supervisor_identity_mount_from_env() -> Result std::io::Result { + let namespace = supervisor_identity_mount_from_env() + .map_err(|err| std::io::Error::other(err.to_string()))?; + let Some(namespace) = namespace else { + return cmd.spawn(); + }; + namespace.spawn_tokio_command(cmd) +} + +#[cfg(target_os = "linux")] +pub fn spawn_std_command_with_supervisor_identity_namespace( + mut cmd: std::process::Command, +) -> std::io::Result { + let namespace = supervisor_identity_mount_from_env() + .map_err(|err| std::io::Error::other(err.to_string()))?; + let Some(namespace) = namespace else { + return cmd.spawn(); + }; + namespace.spawn_std_command(cmd) +} + +#[cfg(target_os = "linux")] +impl SupervisorIdentityMountNamespace { + fn spawn_tokio_command(&self, mut cmd: Command) -> std::io::Result { + let (result_tx, result_rx) = mpsc::channel(); + let handle = tokio::runtime::Handle::current(); + self.spawn_tx + .send(Box::new(move || { + let _guard = handle.enter(); + let _ = result_tx.send(cmd.spawn()); + })) + .map_err(|_| std::io::Error::other("supervisor identity spawn worker stopped"))?; + result_rx + .recv() + .map_err(|_| std::io::Error::other("supervisor identity spawn worker dropped result"))? + } + + fn spawn_std_command( + &self, + mut cmd: std::process::Command, + ) -> std::io::Result { + let (result_tx, result_rx) = mpsc::channel(); + self.spawn_tx + .send(Box::new(move || { + let _ = result_tx.send(cmd.spawn()); + })) + .map_err(|_| std::io::Error::other("supervisor identity spawn worker stopped"))?; + result_rx + .recv() + .map_err(|_| std::io::Error::other("supervisor identity spawn worker dropped result"))? + } +} + +#[cfg(target_os = "linux")] +fn start_supervisor_identity_spawn_worker( + target: PathBuf, +) -> Result> { + let (spawn_tx, spawn_rx) = mpsc::channel::(); + let (ready_tx, ready_rx) = mpsc::channel::>(); + std::thread::Builder::new() + .name("openshell-identity-spawn".into()) + .spawn(move || { + let setup = (|| -> std::io::Result<()> { + private_mount_namespace()?; + let target = + cstring_path(&target).map_err(|err| std::io::Error::other(err.to_string()))?; + mount_empty_tmpfs(&target) + })(); + let ready = match &setup { + Ok(()) => Ok(()), + Err(err) => Err(std::io::Error::new( + err.kind(), + format!("supervisor identity setup failed: {err}"), + )), + }; + let _ = ready_tx.send(ready); + if setup.is_err() { + return; + } + while let Ok(job) = spawn_rx.recv() { + job(); + } + }) + .map_err(|err| miette::miette!("failed to spawn supervisor identity worker: {err}"))?; + ready_rx + .recv() + .map_err(|err| miette::miette!("supervisor identity worker did not start: {err}"))? + .map_err(|err| miette::miette!("{err}"))?; + Ok(spawn_tx) +} + #[cfg(target_os = "linux")] fn supervisor_identity_socket_path_from_env() -> Option<(&'static str, String)> { std::env::var(openshell_core::sandbox_env::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET) @@ -391,10 +485,7 @@ fn supervisor_identity_mount_target(socket_path: &str) -> Result return Ok(None); } if trimmed.starts_with("tcp:") { - return Err(miette::miette!( - "{} must be a UNIX socket path so sandbox child processes can hide it", - openshell_core::sandbox_env::PROVIDER_SPIFFE_WORKLOAD_API_SOCKET - )); + return Ok(None); } let path = trimmed.strip_prefix("unix:").unwrap_or(trimmed); let path = Path::new(path); @@ -437,52 +528,12 @@ fn cstring_path(path: &Path) -> Result { .map_err(|_| miette::miette!("path contains an interior NUL byte: {}", path.display())) } -#[cfg(target_os = "linux")] -fn create_supervisor_identity_mount_namespace(target: &Path) -> Result { - let original_ns = open_current_mount_namespace() - .map_err(|err| miette::miette!("failed to open original mount namespace: {err}"))?; - - private_mount_namespace() - .map_err(|err| miette::miette!("failed to create supervisor identity namespace: {err}"))?; - - let target = cstring_path(target)?; - let result = (|| -> Result { - mount_empty_tmpfs(&target).map_err(|err| { - miette::miette!("failed to hide supervisor identity mount from child namespace: {err}") - })?; - open_current_mount_namespace() - .map_err(|err| miette::miette!("failed to open sanitized mount namespace: {err}")) - })(); - - set_mount_namespace(original_ns.as_raw_fd()).map_err(|restore_err| { - let result_msg = result.as_ref().err().map_or_else( - || "sanitized namespace was created".to_string(), - ToString::to_string, - ); - miette::miette!( - "failed to restore original mount namespace after supervisor identity isolation setup: \ - {restore_err}; setup result: {result_msg}" - ) - })?; - - result -} - -#[cfg(target_os = "linux")] -fn open_current_mount_namespace() -> std::io::Result { - let file = std::fs::File::open("/proc/thread-self/ns/mnt")?; - Ok(file.into()) -} - #[cfg(target_os = "linux")] fn private_mount_namespace() -> std::io::Result<()> { #[allow(unsafe_code)] let rc = unsafe { libc::unshare(libc::CLONE_NEWNS) }; if rc != 0 { - return Err(std::io::Error::other(format!( - "failed to create private mount namespace: {}", - std::io::Error::last_os_error() - ))); + return Err(std::io::Error::last_os_error()); } #[allow(unsafe_code)] @@ -497,23 +548,7 @@ fn private_mount_namespace() -> std::io::Result<()> { ) }; if rc != 0 { - return Err(std::io::Error::other(format!( - "failed to mark mount namespace private: {}", - std::io::Error::last_os_error() - ))); - } - Ok(()) -} - -#[cfg(target_os = "linux")] -fn set_mount_namespace(fd: RawFd) -> std::io::Result<()> { - #[allow(unsafe_code)] - let rc = unsafe { libc::setns(fd, libc::CLONE_NEWNS) }; - if rc != 0 { - return Err(std::io::Error::other(format!( - "failed to enter mount namespace: {}", - std::io::Error::last_os_error() - ))); + return Err(std::io::Error::last_os_error()); } Ok(()) } @@ -533,10 +568,7 @@ fn mount_empty_tmpfs(target: &CString) -> std::io::Result<()> { ) }; if rc != 0 { - return Err(std::io::Error::other(format!( - "failed to hide supervisor identity mount from child process: {}", - std::io::Error::last_os_error() - ))); + return Err(std::io::Error::last_os_error()); } Ok(()) } @@ -696,15 +728,6 @@ impl ProcessHandle { #[cfg(target_os = "linux")] let prepared_sandbox = prepare_child_sandbox(policy, workspace.root(), enforcement_mode) .map_err(|err| miette::miette!("Failed to prepare sandbox: {err}"))?; - #[cfg(target_os = "linux")] - let supervisor_identity_mount = if enforcement_mode.uses_privileged_process_setup() { - supervisor_identity_mount_from_env().map_err(|err| { - miette::miette!("Failed to prepare supervisor identity isolation: {err}") - })? - } else { - None - }; - // Set up process group for signal handling (non-interactive mode only). // In interactive mode, we inherit the parent's process group to maintain // proper terminal control for shells and interactive programs. @@ -724,19 +747,17 @@ impl ProcessHandle { libc::setpgid(0, 0); } - // Enter network namespace before applying other restrictions + // Enter network namespace before applying other restrictions. if let Some(fd) = netns_fd { let result = libc::setns(fd, libc::CLONE_NEWNET); if result != 0 { - return Err(std::io::Error::last_os_error()); + return Err(std::io::Error::other(format!( + "failed to enter network namespace: {}", + std::io::Error::last_os_error() + ))); } } - #[cfg(target_os = "linux")] - if let Some(mount) = supervisor_identity_mount { - mount.enter_for_child()?; - } - // Drop privileges. initgroups/setgid/setuid need access to // /etc/group and /etc/passwd which would be blocked if // Landlock were already enforced. @@ -761,7 +782,15 @@ impl ProcessHandle { } } - let child = cmd.spawn().into_diagnostic()?; + #[cfg(target_os = "linux")] + let child = spawn_command_with_supervisor_identity_namespace(cmd) + .into_diagnostic() + .wrap_err("failed to spawn sandbox entrypoint process")?; + #[cfg(not(target_os = "linux"))] + let child = cmd + .spawn() + .into_diagnostic() + .wrap_err("failed to spawn sandbox entrypoint process")?; let pid = child.id().unwrap_or(0); managed_children::register(pid); @@ -3642,7 +3671,11 @@ mod tests { #[test] fn supervisor_identity_mount_target_rejects_unhideable_endpoints() { - assert!(supervisor_identity_mount_target("tcp:127.0.0.1:8081").is_err()); + assert_eq!( + supervisor_identity_mount_target("tcp:127.0.0.1:8081") + .expect("tcp endpoint should not require mount hiding"), + None + ); assert!(supervisor_identity_mount_target("spiffe-workload-api/spire-agent.sock").is_err()); assert!(supervisor_identity_mount_target("/spire-agent.sock").is_err()); } diff --git a/crates/openshell-supervisor-process/src/run.rs b/crates/openshell-supervisor-process/src/run.rs index 91e56b7ec8..3ea2cbce89 100644 --- a/crates/openshell-supervisor-process/src/run.rs +++ b/crates/openshell-supervisor-process/src/run.rs @@ -110,10 +110,18 @@ pub async fn run_process( // the flag stays at its default (false) and no skill is installed. install_initial_agent_skill(sandbox_id, openshell_endpoint, &agent_proposals).await; + // Provider token grants may mount supervisor-only identity sockets such as + // the SPIFFE Workload API. Prepare the child mount namespace that hides + // those mounts before supervisor seccomp hardening removes the needed + // namespace syscalls. + #[cfg(target_os = "linux")] + crate::process::prepare_supervisor_identity_mount_namespace_from_env()?; + // Install the supervisor seccomp prelude before spawning any workload-side // tasks. By this point the orchestrator has finished privileged startup - // helpers (network namespace setup, nftables probes via run_networking), - // and the SSH listener and entrypoint child have not been exposed yet. + // helpers (network namespace setup, identity mount namespace setup, + // nftables probes via run_networking), and the SSH listener and entrypoint + // child have not been exposed yet. crate::sandbox::apply_supervisor_startup_hardening()?; // Spawn the bypass detection monitor. It tails dmesg for nftables LOG diff --git a/crates/openshell-supervisor-process/src/ssh.rs b/crates/openshell-supervisor-process/src/ssh.rs index 07302da953..968bfd90ef 100644 --- a/crates/openshell-supervisor-process/src/ssh.rs +++ b/crates/openshell-supervisor-process/src/ssh.rs @@ -885,9 +885,12 @@ fn spawn_pty_shell( enforcement_mode, #[cfg(target_os = "linux")] prepared_sandbox, - )?; + ); } + #[cfg(target_os = "linux")] + let mut child = crate::process::spawn_std_command_with_supervisor_identity_namespace(cmd)?; + #[cfg(not(target_os = "linux"))] let mut child = cmd.spawn()?; #[cfg(target_os = "linux")] let child_pid = child.id(); @@ -1040,9 +1043,12 @@ fn spawn_pipe_exec( enforcement_mode, #[cfg(target_os = "linux")] prepared_sandbox, - )?; + ); } + #[cfg(target_os = "linux")] + let mut child = crate::process::spawn_std_command_with_supervisor_identity_namespace(cmd)?; + #[cfg(not(target_os = "linux"))] let mut child = cmd.spawn()?; #[cfg(target_os = "linux")] let child_pid = child.id(); @@ -1183,19 +1189,11 @@ mod unsafe_pty { resolved_identity: ResolvedProcessIdentity, enforcement_mode: ProcessEnforcementMode, #[cfg(target_os = "linux")] prepared: Option, - ) -> anyhow::Result<()> { + ) { // Wrap in Option so we can .take() it out of the FnMut closure. // pre_exec is only called once (after fork, before exec). #[cfg(target_os = "linux")] let mut prepared = prepared; - #[cfg(target_os = "linux")] - let supervisor_identity_mount = if enforcement_mode.uses_privileged_process_setup() { - crate::process::supervisor_identity_mount_from_env().map_err(|err| { - anyhow::anyhow!("failed to prepare supervisor identity isolation: {err}") - })? - } else { - None - }; unsafe { cmd.pre_exec(move || { setsid().map_err(|err| std::io::Error::other(err.to_string()))?; @@ -1207,13 +1205,10 @@ mod unsafe_pty { resolved_identity, enforcement_mode, #[cfg(target_os = "linux")] - supervisor_identity_mount, - #[cfg(target_os = "linux")] prepared.take(), ) }); } - Ok(()) } /// Pre-exec hook for pipe-based (non-PTY) exec. @@ -1235,17 +1230,9 @@ mod unsafe_pty { resolved_identity: ResolvedProcessIdentity, enforcement_mode: ProcessEnforcementMode, #[cfg(target_os = "linux")] prepared: Option, - ) -> anyhow::Result<()> { + ) { #[cfg(target_os = "linux")] let mut prepared = prepared; - #[cfg(target_os = "linux")] - let supervisor_identity_mount = if enforcement_mode.uses_privileged_process_setup() { - crate::process::supervisor_identity_mount_from_env().map_err(|err| { - anyhow::anyhow!("failed to prepare supervisor identity isolation: {err}") - })? - } else { - None - }; unsafe { cmd.pre_exec(move || { enter_netns_and_sandbox( @@ -1254,13 +1241,10 @@ mod unsafe_pty { resolved_identity, enforcement_mode, #[cfg(target_os = "linux")] - supervisor_identity_mount, - #[cfg(target_os = "linux")] prepared.take(), ) }); } - Ok(()) } fn enter_netns_and_sandbox( @@ -1268,9 +1252,6 @@ mod unsafe_pty { policy: &SandboxPolicy, resolved_identity: ResolvedProcessIdentity, enforcement_mode: ProcessEnforcementMode, - #[cfg(target_os = "linux")] supervisor_identity_mount: Option< - &crate::process::SupervisorIdentityMountNamespace, - >, #[cfg(target_os = "linux")] prepared: Option, ) -> std::io::Result<()> { // Enter network namespace before dropping privileges. @@ -1289,11 +1270,6 @@ mod unsafe_pty { #[cfg(not(target_os = "linux"))] let _ = netns_fd; - #[cfg(target_os = "linux")] - if let Some(mount) = supervisor_identity_mount { - mount.enter_for_child()?; - } - // Drop privileges. initgroups/setgid/setuid need /etc/group and // /etc/passwd which would be blocked if Landlock were already enforced. if enforcement_mode.uses_privileged_process_setup() { @@ -1894,8 +1870,7 @@ mod tests { ) .expect("prepare should succeed in test environment"), ), - ) - .expect("install pre_exec should succeed"); + ); let output = cmd .spawn() @@ -1950,8 +1925,7 @@ mod tests { ProcessEnforcementMode::Full, #[cfg(target_os = "linux")] None, - ) - .expect("install pre_exec should succeed"); + ); let output = cmd .spawn() diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index 13d821213b..4824721447 100644 --- a/deploy/helm/openshell/README.md +++ b/deploy/helm/openshell/README.md @@ -139,15 +139,20 @@ JWT signing Secret. ## SPIFFE/SPIRE provider token grants -Set `server.providerTokenGrants.spiffe.enabled=true` to let sandbox supervisors -use SPIFFE JWT-SVIDs for dynamic provider token grants. The chart keeps -supervisor-to-gateway authentication on gateway-minted sandbox JWTs and passes -the SPIFFE Workload API socket path to the Kubernetes driver so sandbox pods can -mount the SPIFFE CSI socket. +Set `server.providerTokenGrants.spiffe.enabled=true` to let the gateway and +sandbox supervisors use SPIFFE JWT-SVIDs for dynamic provider token grants. The +chart keeps supervisor-to-gateway authentication on gateway-minted sandbox JWTs, +mounts the SPIFFE CSI socket into the gateway pod, exports +`OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET`, and passes the socket path to +the Kubernetes driver so sandbox pods can mount the same socket. For local development, uncomment the SPIRE Helm releases in `skaffold.yaml` and add `ci/values-spire.yaml` to the OpenShell release values files. +The gateway verifies supervisor JWT-SVIDs with JWT bundles fetched from the +SPIFFE Workload API, so this path does not require access to the SPIRE OIDC +discovery endpoint or its TLS CA. + ## Values | Key | Type | Default | Description | @@ -242,8 +247,8 @@ add `ci/values-spire.yaml` to the OpenShell release values files. | server.oidc.scopesClaim | string | `""` | Dot-separated path to the scopes array in the JWT claims. | | server.oidc.userRole | string | `""` | Role name for standard user access. | | server.policyValidationFailureMode | string | `"fail_closed"` | Posture when a candidate sandbox policy fails validation. `fail_closed` deactivates the previous policy; `retain_last_valid` keeps it active. | -| server.providerTokenGrants.spiffe.enabled | bool | `false` | Mount the SPIFFE Workload API socket into sandbox pods for dynamic provider token grants. | -| server.providerTokenGrants.spiffe.workloadApiSocketPath | string | `"/spiffe-workload-api/spire-agent.sock"` | Path to the SPIFFE Workload API socket mounted into sandbox pods. | +| server.providerTokenGrants.spiffe.enabled | bool | `false` | Mount the SPIFFE Workload API socket into gateway and sandbox pods for dynamic provider token grants. | +| server.providerTokenGrants.spiffe.workloadApiSocketPath | string | `"/spiffe-workload-api/spire-agent.sock"` | Path to the SPIFFE Workload API socket mounted into gateway and sandbox pods. | | server.sandboxImage | string | `"ghcr.io/nvidia/openshell-community/sandboxes/base:latest"` | Default sandbox image used when requests do not specify one. | | server.sandboxImagePullPolicy | string | `""` | Kubernetes imagePullPolicy for sandbox pods. Empty = Kubernetes default (Always for :latest, IfNotPresent otherwise). Set to "Always" for dev clusters so new images are picked up without manual eviction. | | server.sandboxImagePullSecrets | list | `[]` | Image pull secrets attached to sandbox pods. Referenced Secrets must exist in the sandbox namespace. | diff --git a/deploy/helm/openshell/README.md.gotmpl b/deploy/helm/openshell/README.md.gotmpl index 0242d8118c..3757abc795 100644 --- a/deploy/helm/openshell/README.md.gotmpl +++ b/deploy/helm/openshell/README.md.gotmpl @@ -139,14 +139,19 @@ JWT signing Secret. ## SPIFFE/SPIRE provider token grants -Set `server.providerTokenGrants.spiffe.enabled=true` to let sandbox supervisors -use SPIFFE JWT-SVIDs for dynamic provider token grants. The chart keeps -supervisor-to-gateway authentication on gateway-minted sandbox JWTs and passes -the SPIFFE Workload API socket path to the Kubernetes driver so sandbox pods can -mount the SPIFFE CSI socket. +Set `server.providerTokenGrants.spiffe.enabled=true` to let the gateway and +sandbox supervisors use SPIFFE JWT-SVIDs for dynamic provider token grants. The +chart keeps supervisor-to-gateway authentication on gateway-minted sandbox JWTs, +mounts the SPIFFE CSI socket into the gateway pod, exports +`OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET`, and passes the socket path to +the Kubernetes driver so sandbox pods can mount the same socket. For local development, uncomment the SPIRE Helm releases in `skaffold.yaml` and add `ci/values-spire.yaml` to the OpenShell release values files. +The gateway verifies supervisor JWT-SVIDs with JWT bundles fetched from the +SPIFFE Workload API, so this path does not require access to the SPIRE OIDC +discovery endpoint or its TLS CA. + {{ template "chart.valuesSection" . }} {{ template "helm-docs.versionFooter" . }} diff --git a/deploy/helm/openshell/templates/_gateway-workload.tpl b/deploy/helm/openshell/templates/_gateway-workload.tpl index 3db50a5ee6..ec67d77fc8 100644 --- a/deploy/helm/openshell/templates/_gateway-workload.tpl +++ b/deploy/helm/openshell/templates/_gateway-workload.tpl @@ -66,7 +66,8 @@ spec: {{- end }} # Most gateway settings live in the ConfigMap-backed TOML file # mounted at /etc/openshell/gateway.toml. Secret-bearing settings use - # env vars that the TOML references by name. + # env vars that the TOML references by name. Some process-level + # settings consumed by libraries outside gateway code also remain here. {{- if and .Values.server.oidc.issuer .Values.server.oidc.caConfigMapName }} # OIDC issuer custom-CA: rustls/reqwest read SSL_CERT_FILE for # outbound TLS verification. This is a process-level env var @@ -77,6 +78,10 @@ spec: {{- end }} - name: OPENSHELL_TELEMETRY_ENABLED value: {{ .Values.server.telemetryEnabled | quote }} + {{- if .Values.server.providerTokenGrants.spiffe.enabled }} + - name: OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET + value: {{ .Values.server.providerTokenGrants.spiffe.workloadApiSocketPath | quote }} + {{- end }} volumeMounts: {{- if eq (include "openshell.workloadKind" .) "statefulset" }} - name: openshell-data @@ -103,6 +108,11 @@ spec: mountPath: /etc/openshell-tls/oidc-ca readOnly: true {{- end }} + {{- if .Values.server.providerTokenGrants.spiffe.enabled }} + - name: spiffe-workload-api + mountPath: {{ dir .Values.server.providerTokenGrants.spiffe.workloadApiSocketPath | quote }} + readOnly: true + {{- end }} ports: - name: grpc containerPort: {{ .Values.service.port }} @@ -170,6 +180,12 @@ spec: configMap: name: {{ .Values.server.oidc.caConfigMapName }} {{- end }} + {{- if .Values.server.providerTokenGrants.spiffe.enabled }} + - name: spiffe-workload-api + csi: + driver: csi.spiffe.io + readOnly: true + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 4 }} diff --git a/deploy/helm/openshell/tests/gateway_config_test.yaml b/deploy/helm/openshell/tests/gateway_config_test.yaml index 8125559e71..0bb7c0f3b1 100644 --- a/deploy/helm/openshell/tests/gateway_config_test.yaml +++ b/deploy/helm/openshell/tests/gateway_config_test.yaml @@ -513,11 +513,39 @@ tests: path: data["gateway.toml"] pattern: '\[openshell\.gateway\.spiffe\]' - - it: keeps the gateway sandbox JWT secret mounted when provider SPIFFE grants are enabled + - it: mounts the gateway SPIFFE socket while keeping sandbox JWT auth set: server.providerTokenGrants.spiffe.enabled: true template: templates/statefulset.yaml asserts: - - matchRegex: - path: spec.template.spec.volumes[1].name - pattern: '^sandbox-jwt$' + - contains: + path: spec.template.spec.containers[0].env + content: + name: OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET + value: /spiffe-workload-api/spire-agent.sock + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: sandbox-jwt + mountPath: /etc/openshell-jwt + readOnly: true + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: spiffe-workload-api + mountPath: /spiffe-workload-api + readOnly: true + - contains: + path: spec.template.spec.volumes + content: + name: sandbox-jwt + secret: + defaultMode: 256 + secretName: openshell-jwt-keys + - contains: + path: spec.template.spec.volumes + content: + name: spiffe-workload-api + csi: + driver: csi.spiffe.io + readOnly: true diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index 3b9ba3f96a..fcf0b35aee 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -335,15 +335,15 @@ server: # (owner-read only). Override to 0440 or 0444 if the container UID # does not match the volume file owner. secretDefaultMode: "" - # Dynamic provider token grants. When SPIFFE is enabled here, sandbox - # supervisors mount the SPIFFE Workload API socket so provider profiles can - # exchange JWT-SVIDs for upstream access tokens. Supervisor-to-gateway - # authentication still uses gateway-minted sandbox JWTs. + # Dynamic provider token grants. When SPIFFE is enabled here, both the + # gateway and sandbox supervisors mount the SPIFFE Workload API socket so + # token-exchange profiles can use gateway- and sandbox-scoped JWT-SVIDs. + # Supervisor-to-gateway authentication still uses gateway-minted sandbox JWTs. providerTokenGrants: spiffe: - # -- Mount the SPIFFE Workload API socket into sandbox pods for dynamic provider token grants. + # -- Mount the SPIFFE Workload API socket into gateway and sandbox pods for dynamic provider token grants. enabled: false - # -- Path to the SPIFFE Workload API socket mounted into sandbox pods. + # -- Path to the SPIFFE Workload API socket mounted into gateway and sandbox pods. workloadApiSocketPath: /spiffe-workload-api/spire-agent.sock # OIDC (OpenID Connect) configuration for JWT-based authentication. # When issuer is set, the server validates Bearer tokens on gRPC requests. diff --git a/docs/kubernetes/access-control.mdx b/docs/kubernetes/access-control.mdx index 5409a4b11d..e3addad032 100644 --- a/docs/kubernetes/access-control.mdx +++ b/docs/kubernetes/access-control.mdx @@ -23,9 +23,11 @@ For how the CLI resolves gateways and stores credentials, refer to [Gateway Auth Kubernetes sandbox supervisors authenticate back to the gateway as sandbox workloads. By default, the gateway mints its own sandbox JWTs and Kubernetes sandboxes bootstrap them with a projected ServiceAccount token. -Dynamic provider token grants can use SPIFFE without changing supervisor-to-gateway authentication. Set `server.providerTokenGrants.spiffe.enabled=true` to mount the SPIFFE CSI Workload API socket into sandbox pods while keeping the projected ServiceAccount token bootstrap and gateway-minted sandbox JWT path. +Dynamic provider token grants can use SPIFFE without changing supervisor-to-gateway authentication. Set `server.providerTokenGrants.spiffe.enabled=true` to mount the SPIFFE CSI Workload API socket into gateway and sandbox pods while keeping the projected ServiceAccount token bootstrap and gateway-minted sandbox JWT path. -Provider token grants require a SPIFFE implementation such as SPIRE and a `ClusterSPIFFEID` that assigns per-sandbox IDs from the pod's `openshell.io/sandbox-id` annotation. Provider profiles with `token_grant` metadata cause the sandbox supervisor to request JWT-SVIDs and exchange them for upstream OAuth2 access tokens. +Provider token grants require a SPIFFE implementation such as SPIRE and identities for the gateway and sandbox pods. The repository's local SPIRE overlay assigns sandbox IDs from the pod's `openshell.io/sandbox-id` annotation, but the gateway validation path only requires the supervisor SVID to be valid and in the same SPIFFE trust domain as the gateway SVID. Provider profiles with `token_grant` metadata cause the sandbox supervisor to request JWT-SVIDs and exchange them for upstream OAuth2 access tokens. Token-exchange profiles also require a gateway SPIFFE identity because the gateway brokers the intermediate token exchange with its own JWT-SVID. + +The gateway verifies supervisor JWT-SVIDs with JWT bundles fetched from the SPIFFE Workload API, so intermediate token exchange does not require gateway access to the SPIRE OIDC discovery endpoint or its TLS CA. ## OIDC User Authentication @@ -74,6 +76,8 @@ helm upgrade openshell \ Both `adminRole` and `userRole` must be set, or both must be empty. Setting only one is not supported. +OIDC RBAC is method-level authorization. It controls which API operations a caller can perform, but provider and sandbox records are not owned by individual OIDC subjects. In shared clusters, treat provider credentials as gateway-wide resources and use separate gateways or external tenancy controls when users must not see or attach each other's providers and sandboxes. + ### Provider-specific rolesClaim paths | Provider | rolesClaim value | diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index 2cd10b8a0b..217bbf8d37 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -469,6 +469,14 @@ proxy_uid = 1337 process_binary_aware_network_policy = true ``` +For token-exchange provider profiles, the gateway also needs access to its own +SPIFFE Workload API socket. In Helm deployments, set +`server.providerTokenGrants.spiffe.enabled=true`; the chart mounts the socket +into the gateway pod and sets `OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET`. +The gateway verifies supervisor JWT-SVIDs with JWT bundles fetched from the +SPIFFE Workload API, so this validation path does not require gateway access to +the SPIRE OIDC discovery endpoint or its TLS CA. + ### Docker Sandboxes run as containers on a local bridge network. The supervisor binary is bind-mounted from the host (no in-cluster image pull required); guest mTLS material is supplied as host paths. diff --git a/docs/sandboxes/manage-providers.mdx b/docs/sandboxes/manage-providers.mdx index 8cf1e3715c..efe498da70 100644 --- a/docs/sandboxes/manage-providers.mdx +++ b/docs/sandboxes/manage-providers.mdx @@ -60,6 +60,33 @@ openshell provider create --name my-nvidia --type nvidia --credential NVIDIA_API This looks up the current value of `$NVIDIA_API_KEY` in your shell and stores it. +### From the Current OIDC Login + +Profile-backed token-exchange providers can store the current gateway OIDC +access token as their subject credential: + +```shell +openshell provider create \ + --name custom-api \ + --type custom-api \ + --from-oidc-token +``` + +OpenShell infers the destination credential from the provider profile when the +profile has exactly one `token_grant.subject_token.credential`. If the profile +has more than one, pass `--credential `. Refresh the stored subject token +later with: + +```shell +openshell provider update custom-api --from-oidc-token +``` + +This copies the current OIDC access token and expiry from the active gateway +login. This requires an active named gateway that was registered for OIDC. If +the stored gateway access token is expired and a refresh token is available, the +CLI refreshes it before storing the provider credential. It does not store the +OIDC refresh token in the provider. + Provider profile metadata is available for known provider types. Provider profile network policy is gateway opt-in: diff --git a/docs/sandboxes/providers-v2.mdx b/docs/sandboxes/providers-v2.mdx index f867b84bd5..c01b9963b8 100644 --- a/docs/sandboxes/providers-v2.mdx +++ b/docs/sandboxes/providers-v2.mdx @@ -312,6 +312,12 @@ category: data inference_capable: false credentials: + - name: user_oidc_token + description: User OIDC token used as a token-exchange subject token + env_vars: [CUSTOM_API_USER_OIDC_TOKEN] + required: true + auth_style: bearer + - name: api_token description: API access token env_vars: [CUSTOM_API_TOKEN] @@ -344,17 +350,23 @@ credentials: required: true secret: true - # Optional dynamic credential. The sandbox supervisor requests a - # SPIFFE JWT-SVID, exchanges it at token_endpoint, caches the returned - # access token, and injects it according to auth_style/header_name for - # matching endpoint traffic. + # Optional dynamic credential. The sandbox supervisor resolves this on + # demand for matching endpoint traffic, caches the returned access token, + # and injects it according to auth_style/header_name. token_grant: + # Accepted values: client_credentials, token_exchange. + grant_type: token_exchange token_endpoint: https://login.example.com/realms/custom/protocol/openid-connect/token audience: api://custom-api jwt_svid_audience: https://login.example.com/realms/custom - client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer + client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-spiffe scopes: [api.read, api.write] cache_ttl_seconds: 300 + requested_token_type: urn:ietf:params:oauth:token-type:access_token + subject_token: + source: provider_credential + credential: user_oidc_token + subject_token_type: urn:ietf:params:oauth:token-type:access_token audience_overrides: - host: api.example.com port: 443 @@ -486,7 +498,14 @@ The refresh attaches to the primary credential (`access_key_id`). Each reference ### Dynamic Token Grants -`token_grant` belongs to one credential declaration. When a sandbox with the provider attached sends HTTP traffic to a matching profile endpoint, the supervisor requests a SPIFFE JWT-SVID from the local Workload API, exchanges it at `token_endpoint`, caches the returned access token, and injects it before forwarding the request upstream. Use `auth_style: bearer` to inject `Authorization: Bearer `, or `auth_style: header` with `header_name` to inject the raw access token into a custom header. Token grants do not support `query` or `path` placement. +`token_grant` belongs to one credential declaration. When a sandbox with the provider attached sends HTTP traffic to a matching profile endpoint, the supervisor resolves the dynamic credential, caches the returned access token, and injects it before forwarding the request upstream. Use `auth_style: bearer` to inject `Authorization: Bearer `, or `auth_style: header` with `header_name` to inject the raw access token into a custom header. Token grants do not support `query` or `path` placement. + +OpenShell supports two dynamic grant types: + +| Grant type | Behavior | +|---|---| +| `client_credentials` | The supervisor requests a SPIFFE JWT-SVID from the local Workload API and sends it directly to `token_endpoint` as the OAuth2 client assertion. This is the default when `grant_type` is omitted. | +| `token_exchange` | The supervisor first asks the gateway for an intermediate token. The request includes the supervisor JWT-SVID; the gateway verifies it, uses the SVID subject as the intermediate token audience, and exchanges the stored subject credential at the same `token_endpoint` using the gateway's own JWT-SVID as the client assertion. The supervisor then exchanges that intermediate token for the final upstream token using its own JWT-SVID as the client assertion. | Create provider instances for token-grant-only profiles with `--runtime-credentials`. This records an empty provider instance and makes the runtime-resolved credential source explicit: @@ -497,19 +516,39 @@ openshell provider create \ --runtime-credentials ``` +For `token_exchange` profiles, the provider also stores the user subject token referenced by `token_grant.subject_token.credential`. Create or update that provider credential from the current gateway OIDC login with `--from-oidc-token`. This requires an active named gateway that was registered for OIDC. The CLI copies the current OIDC access token and its expiry into the provider. If the stored gateway access token is expired and a refresh token is available, the CLI refreshes it first. OpenShell does not store the OIDC refresh token in the provider. When the stored subject-token credential expires, the gateway rejects intermediate token exchange until the provider is updated with a fresh token. + +```shell +openshell provider create \ + --name custom-api \ + --type custom-api \ + --from-oidc-token + +openshell provider update custom-api \ + --from-oidc-token +``` + +OpenShell infers the destination credential when the provider profile has exactly one `token_grant.subject_token.credential`. If a profile declares more than one token-exchange subject credential, pass `--credential ` to choose one. + Token grant fields: | Field | Required | Behavior | |---|---|---| +| `grant_type` | No | `client_credentials` or `token_exchange`. Defaults to `client_credentials` for backward compatibility. | | `token_endpoint` | Yes | OAuth2 token endpoint that accepts a SPIFFE JWT-SVID client assertion. Use `https://` unless the endpoint is loopback or a Kubernetes service DNS name such as `token-issuer.default.svc.cluster.local`. | -| `audience` | No | Resource audience requested from the token service. | +| `audience` | No | Resource audience requested from the token service. For `token_exchange`, this is the final exchange audience; the gateway intermediate exchange always uses the verified supervisor SVID subject as its audience. | | `jwt_svid_audience` | No | Audience used when requesting the JWT-SVID. When omitted, OpenShell derives an issuer-style audience from Keycloak token endpoint paths or falls back to the full token endpoint URL. | -| `client_assertion_type` | No | OAuth2 `client_assertion_type` form value. Defaults to RFC 7523 `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. Set a provider-specific value, such as `urn:ietf:params:oauth:client-assertion-type:jwt-spiffe`, only when the token issuer explicitly requires it. | +| `client_assertion_type` | No | OAuth2 `client_assertion_type` form value. Defaults to RFC 7523 `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. Set `urn:ietf:params:oauth:client-assertion-type:jwt-spiffe` when the token issuer expects the SPIFFE assertion type. | | `scopes` | No | OAuth2 scopes sent as a space-separated `scope` parameter. | | `cache_ttl_seconds` | No | Token cache TTL override. When omitted or `0`, OpenShell uses the token response `expires_in` with a 30-second safety margin and one-hour cap, or five minutes minus the margin if the response does not include an expiry. | -| `audience_overrides` | No | Endpoint-specific `audience` and `scopes` overrides selected by host, port, and path. | +| `requested_token_type` | No | RFC 8693 `requested_token_type` sent during token exchange. Defaults to `urn:ietf:params:oauth:token-type:access_token`. | +| `subject_token` | Required for `token_exchange` | Subject-token source used for the gateway-brokered intermediate exchange. Phase one supports `source: provider_credential`, where `credential` names another credential declared in the same profile. | +| `subject_token.subject_token_type` | No | RFC 8693 `subject_token_type` for the stored subject token. Defaults to `urn:ietf:params:oauth:token-type:access_token`. | +| `audience_overrides` | No | Endpoint-specific final-exchange `audience` and `scopes` overrides selected by host, port, and path. These overrides do not affect the gateway intermediate exchange. | + +Token grants require the sandbox supervisor to have access to a SPIFFE Workload API socket. `token_exchange` also requires the gateway to have its own Workload API socket so it can present a gateway JWT-SVID during the intermediate exchange. They apply to HTTP traffic that the proxy can inspect. Endpoints with `tls: skip` bypass TLS termination and cannot receive dynamic token grant injection for HTTPS traffic. The token service must return a token value that is safe for HTTP header placement; malformed values are rejected before caching or header injection. -Token grants require the sandbox supervisor to have access to a SPIFFE Workload API socket. They apply to HTTP traffic that the proxy can inspect. Endpoints with `tls: skip` bypass TLS termination and cannot receive dynamic token grant injection for HTTPS traffic. The token service must return a token value that is safe for HTTP header placement; malformed values are rejected before caching or header injection. +The gateway only brokers an intermediate token for a sandbox principal, and only when the requested provider is attached to that sandbox. It verifies the supervisor JWT-SVID issuer, audience, signature, and SPIFFE trust domain against the gateway's own JWT-SVID, then uses the verified supervisor SVID subject as the intermediate-token audience. ## Provider Instances diff --git a/e2e/rust/Cargo.lock b/e2e/rust/Cargo.lock index 5a8028779a..448801c415 100644 --- a/e2e/rust/Cargo.lock +++ b/e2e/rust/Cargo.lock @@ -8,12 +8,72 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base64" version = "0.22.1" @@ -78,12 +138,28 @@ dependencies = [ "serde_repr", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -109,6 +185,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "digest" version = "0.10.7" @@ -121,20 +203,20 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "equivalent" @@ -149,7 +231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -158,6 +240,18 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -239,6 +333,19 @@ dependencies = [ "version_check", ] +[[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" @@ -262,6 +369,25 @@ dependencies = [ "r-efi 6.0.0", ] +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -276,9 +402,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -329,6 +455,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -354,6 +481,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -517,6 +657,32 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "libc" version = "0.2.189" @@ -560,12 +726,24 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.2.2" @@ -574,7 +752,41 @@ checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", ] [[package]] @@ -595,6 +807,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", + "jsonwebtoken", "prost", "rand", "serde", @@ -605,6 +818,10 @@ dependencies = [ "sha2", "tempfile", "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tower", "url", ] @@ -631,12 +848,42 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -652,6 +899,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -752,6 +1005,20 @@ dependencies = [ "bitflags", ] +[[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 = "rustix" version = "1.1.4" @@ -762,9 +1029,15 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "ryu" version = "1.0.23" @@ -905,6 +1178,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -915,6 +1194,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.12" @@ -934,7 +1225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -965,6 +1256,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "synstructure" version = "0.13.2" @@ -986,7 +1283,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1009,6 +1306,36 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -1033,18 +1360,29 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", ] [[package]] @@ -1061,6 +1399,71 @@ dependencies = [ "tokio", ] +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[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" @@ -1074,9 +1477,21 @@ 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.119", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -1104,6 +1519,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[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" @@ -1152,6 +1573,51 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1180,6 +1646,15 @@ 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.61.2" @@ -1189,6 +1664,70 @@ 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 = "wit-bindgen" version = "0.57.1" diff --git a/e2e/rust/Cargo.toml b/e2e/rust/Cargo.toml index 3353f07af7..203e1d9fbe 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -79,6 +79,11 @@ name = "podman_oci_identity" path = "tests/podman_oci_identity.rs" required-features = ["e2e-podman"] +[[test]] +name = "provider_token_exchange" +path = "tests/provider_token_exchange.rs" +required-features = ["e2e-podman"] + [[test]] name = "vm_gateway_resume" path = "tests/vm_gateway_resume.rs" @@ -147,8 +152,10 @@ futures-util = "0.3" http-body-util = "0.1" hyper = { version = "1", features = ["client", "http1"] } hyper-util = { version = "0.1", features = ["tokio"] } +jsonwebtoken = "9" prost = "0.14" tokio = { version = "1.43", features = ["full"] } +tokio-stream = { version = "0.1", features = ["net"] } tempfile = "3" sha1 = "0.10" sha2 = "0.10" @@ -157,6 +164,9 @@ rand = "0.9" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yml = "0.0.12" +tonic = { version = "0.14", features = ["transport"] } +tonic-prost = "0.14" +tower = "0.5" url = "2" [dev-dependencies] diff --git a/e2e/rust/e2e-podman.sh b/e2e/rust/e2e-podman.sh index 16796c0562..548a80af7f 100755 --- a/e2e/rust/e2e-podman.sh +++ b/e2e/rust/e2e-podman.sh @@ -17,6 +17,12 @@ if [ "${E2E_TEST}" = "gpu" ] && [ -z "${OPENSHELL_E2E_WORKLOAD_MANIFEST:-}" ] && echo "note: running Podman GPU e2e without a workload manifest; workload validation will log an explicit skip. Build one with 'CONTAINER_ENGINE=podman mise run e2e:workloads:build' or set OPENSHELL_E2E_WORKLOAD_MANIFEST." fi +if [ "${E2E_TEST}" = "provider_token_exchange" ]; then + export OPENSHELL_E2E_SPIFFE_FIXTURE="${OPENSHELL_E2E_SPIFFE_FIXTURE:-1}" +fi + +cargo build -p openshell-cli + TEST_ARGS=( cargo test --manifest-path "${ROOT}/e2e/rust/Cargo.toml" --features "${E2E_FEATURES}" diff --git a/e2e/rust/tests/provider_token_exchange.rs b/e2e/rust/tests/provider_token_exchange.rs new file mode 100644 index 0000000000..fe6ce39405 --- /dev/null +++ b/e2e/rust/tests/provider_token_exchange.rs @@ -0,0 +1,784 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(feature = "e2e-podman")] + +use std::collections::HashMap; +use std::convert::Infallible; +use std::fs; +use std::io::Write as _; +use std::net::{Ipv4Addr, SocketAddr}; +use std::os::unix::fs::PermissionsExt as _; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use base64::Engine as _; +use futures_util::future::BoxFuture; +use jsonwebtoken::{Algorithm, EncodingKey, Header}; +use openshell_e2e::harness::binary::openshell_cmd; +use openshell_e2e::harness::port::find_free_port; +use openshell_e2e::harness::sandbox::SandboxGuard; +use serde_json::json; +use tempfile::NamedTempFile; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; +use tokio::net::{TcpListener, UnixListener}; +use tokio::process::Command; +use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream, UnixListenerStream}; +use tonic::body::Body as TonicBody; +use tonic::codegen::{Body, http}; +use tonic::{Request, Response, Status}; + +const TRUST_DOMAIN: &str = "openshell-e2e.test"; +const ISSUER: &str = "https://spiffe.openshell-e2e.test"; +const KEY_ID: &str = "openshell-e2e-test-key"; +const USER_SUBJECT_TOKEN: &str = "stored-user-token"; +const INTERMEDIATE_TOKEN: &str = "intermediate-token"; +const FINAL_ACCESS_TOKEN: &str = "final-access-token"; +const TOKEN_TYPE_ACCESS_TOKEN: &str = "urn:ietf:params:oauth:token-type:access_token"; +const CLIENT_ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe"; + +const TEST_RSA_PRIVATE_KEY: &str = r"-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvCoZ0mVHpCHsF +zeeqw2caNIe/eb4BQUccFPhZfRnF7sCfyB84zTBmuwG2umRBdjFnVsfIIZRp2HcD +OESrRYYiE1RGfjBXImGVg2Wtza0HYhL1sLyX1eaEefylxoilmApAgWDh9p36h8J2 +s5YHwyXPTttx4DpdWDnxju1iNmwoIB8uVE/5amWgbNvlETMBOcB1RxDHtnVy+xJz +jjjrzK4Qz9WsUTHAvngdi4Yyxvci+yKpjYTg5+UWxmAN6iW522TpLe32MDb5Ug1d +trBvvepWmdQ6CBwPhBHCt/sMoSJAYSO4RKeBnBjeLQBXFTxaOv5iTGIsRTX3K471 +epHp3cT5AgMBAAECggEASQlRv/4nZN5SgsH/K8v7zb3kdHsmUly8AJYpaCGgauvr +uN/mUyueyga2uNl+MqhQBef6VWHZjO6y/gdw86v/Q2GgVQebQQhKAnpAp2w+Ceoc +siKMFqi8VkOWLU+xPbM6d97kH3TpRxt1g1T8wYFmWeF0BEiE4eUJzGaQW14M9BJ+ +G0QxmP/zjX9cNpVeApKTjBWKiH4CXG3DuI3pJ93VOMpUlOsrdLXvKGTze0e01itr +MX/MHHTE+VXB4FB+/zKSA4c36egi676OSXrGC/GDmM8ntJ4CUGeD5uZsMSADiAUn +iccv5iGRWVMIKxUS5Q4k0jy8uWuK+QVP4Y6cQWYArwKBgQDhuSNORBNpIGRfsKGN +iJo/h+qinz6pEIpa3D3oVl7rpkyvgIyaTwfXvC1vfdS9V5VIel2gV2Cx0OrI8yrr +nQu1JuNV/rLmtvqX321fgBLRdoiqF3pAy1gbmdUz1elerAIYL578gXQ6jg1bbdic +kJpn0MsoDUJGwvJnXcgLqG7q3wKBgQDGhRIa4oJsj1vqICc8zt8YsCAcot3vjWLH +588X7JdBGOWJdWxfdmGXQRn5Zw9UhMQnYa3uyTBPeVcXopThlPotYeuFhLSU856T +IJzfpzCJzC4zIQayoyvJFrKe7N70iUQ986dewYy9oxQhHvFKd/qe4ylbzZJXpthX +eWEuuBSjJwKBgGkqXt6qLPj/1IQYwUw15tfOtW0LEKCoSi3HCzjidNsJ4hSqqdeD +Fr5WuDyHvcRxt+XKzTBVRYHTOnBhiw+3XasK8UQxpJyFh/+WY1jpTNs2hLnqslTZ +6LUDWSgLc+1d6qPmHAa9Ma/OWz7L0O4xGR9hUiXY95YMYe/y668yzGq1AoGBAJyU +Gsqfu7U6gYmxoKEine6QBFPx1dD7GF2KJdq93jMXGvyHZFoLOkAdtgnz0rCcI0bY +kWKUxwj4MMxQjNM8OPMQl75xBCmz2XA8Od9htDQLmqjzNKAzePabc3lMZTJFDlE6 +29kuGf79IIRbLn/JECDAFT/2baW60Ep2T0OVJ5njAoGAfaCaQ4aVgjI027q7Y5qP +KfNSI8uuA8PLqmUY30I9KFWzN6VDLu00eKa90F4w3CeWRRQWXW1+007tTz3V1mNw +20A24Fi3HGQmXc7NyuLDODTJsWBICuOemCnRkvcxIlxb+ec7jp+XRmzDwKkzSnVN +pM2zFU8SeVkvHKlEuoHaP0s= +-----END PRIVATE KEY-----"; //notsecret + +const TEST_RSA_MODULUS_HEX: &str = concat!( + "af0a86749951e9087b05cde7aac3671a3487bf79be0141471c14f8597d19c5eec09fc81f38cd3066bb01b6ba", + "644176316756c7c8219469d877033844ab4586221354467e30572261958365adcdad076212f5b0bc97d5e684", + "79fca5c688a5980a408160e1f69dfa87c276b39607c325cf4edb71e03a5d5839f18eed62366c28201f2e54", + "4ff96a65a06cdbe511330139c0754710c7b67572fb12738e38ebccae10cfd5ac5131c0be781d8b8632c6", + "f722fb22a98d84e0e7e516c6600dea25b9db64e92dedf63036f9520d5db6b06fbdea5699d43a081c0f84", + "11c2b7fb0ca122406123b844a7819c18de2d0057153c5a3afe624c622c4535f72b8ef57a91e9ddc4f9" +); + +#[derive(Clone, PartialEq, prost::Message)] +struct JwtsvidRequest { + #[prost(string, repeated, tag = "1")] + audience: Vec, + #[prost(string, tag = "2")] + spiffe_id: String, +} + +#[derive(Clone, PartialEq, prost::Message)] +struct JwtsvidResponse { + #[prost(message, repeated, tag = "1")] + svids: Vec, +} + +#[derive(Clone, PartialEq, prost::Message)] +struct Jwtsvid { + #[prost(string, tag = "1")] + spiffe_id: String, + #[prost(string, tag = "2")] + svid: String, + #[prost(string, tag = "3")] + hint: String, +} + +#[derive(Clone, PartialEq, prost::Message)] +struct JwtBundlesRequest {} + +#[derive(Clone, PartialEq, prost::Message)] +struct JwtBundlesResponse { + #[prost(map = "string, bytes", tag = "1")] + bundles: HashMap>, +} + +#[derive(Clone)] +struct SpiffeWorkloadApi { + subject: Arc, + jwks: Arc>, + encoding_key: Arc, +} + +impl SpiffeWorkloadApi { + fn jwt_svid(&self, audience: Vec) -> Result { + let now = unix_timestamp(); + let mut header = Header::new(Algorithm::RS256); + header.kid = Some(KEY_ID.to_string()); + let claims = json!({ + "iss": ISSUER, + "sub": self.subject.as_ref(), + "aud": audience, + "iat": now, + "exp": now + 3600, + }); + jsonwebtoken::encode(&header, &claims, &self.encoding_key) + .map_err(|err| Status::internal(format!("sign JWT-SVID: {err}"))) + } +} + +#[derive(Clone)] +struct SpiffeWorkloadApiServer { + inner: Arc, +} + +impl SpiffeWorkloadApiServer { + fn new(inner: SpiffeWorkloadApi) -> Self { + Self { + inner: Arc::new(inner), + } + } +} + +impl tower::Service> for SpiffeWorkloadApiServer +where + B: Body + Send + 'static, + B::Error: Into> + Send + 'static, +{ + type Response = http::Response; + type Error = Infallible; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/SpiffeWorkloadAPI/FetchJWTSVID" => { + #[derive(Clone)] + struct FetchJwtsvidSvc(Arc); + impl tonic::server::UnaryService for FetchJwtsvidSvc { + type Response = JwtsvidResponse; + type Future = BoxFuture<'static, Result, Status>>; + + fn call(&mut self, request: Request) -> Self::Future { + let inner = Arc::clone(&self.0); + Box::pin(async move { + let request = request.into_inner(); + let svid = inner.jwt_svid(request.audience)?; + Ok(Response::new(JwtsvidResponse { + svids: vec![Jwtsvid { + spiffe_id: inner.subject.to_string(), + svid, + hint: String::new(), + }], + })) + }) + } + } + + let inner = Arc::clone(&self.inner); + Box::pin(async move { + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec); + Ok(grpc.unary(FetchJwtsvidSvc(inner), req).await) + }) + } + "/SpiffeWorkloadAPI/FetchJWTBundles" => { + #[derive(Clone)] + struct FetchJwtBundlesSvc(Arc); + impl tonic::server::ServerStreamingService for FetchJwtBundlesSvc { + type Response = JwtBundlesResponse; + type ResponseStream = ReceiverStream>; + type Future = + BoxFuture<'static, Result, Status>>; + + fn call(&mut self, _request: Request) -> Self::Future { + let inner = Arc::clone(&self.0); + Box::pin(async move { + let mut bundles = HashMap::new(); + bundles.insert(TRUST_DOMAIN.to_string(), inner.jwks.as_ref().clone()); + let (tx, rx) = tokio::sync::mpsc::channel(1); + tx.send(Ok(JwtBundlesResponse { bundles })) + .await + .map_err(|err| Status::internal(format!("send bundle: {err}")))?; + Ok(Response::new(ReceiverStream::new(rx))) + }) + } + } + + let inner = Arc::clone(&self.inner); + Box::pin(async move { + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec); + Ok(grpc.server_streaming(FetchJwtBundlesSvc(inner), req).await) + }) + } + _ => Box::pin(async move { + let mut response = http::Response::new(TonicBody::empty()); + response.headers_mut().insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + response.headers_mut().insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }), + } + } +} + +impl tonic::server::NamedService for SpiffeWorkloadApiServer { + const NAME: &'static str = "SpiffeWorkloadAPI"; +} + +struct FixtureHandle { + task: tokio::task::JoinHandle<()>, +} + +impl Drop for FixtureHandle { + fn drop(&mut self) { + self.task.abort(); + } +} + +fn unix_timestamp() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time should be after unix epoch") + .as_secs() + .try_into() + .expect("timestamp should fit i64") +} + +fn jwks() -> Vec { + let modulus = hex::decode(TEST_RSA_MODULUS_HEX).expect("valid test RSA modulus hex"); + let n = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(modulus); + let e = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0x01, 0x00, 0x01]); + serde_json::to_vec(&json!({ + "keys": [{ + "kty": "RSA", + "kid": KEY_ID, + "use": "sig", + "alg": "RS256", + "n": n, + "e": e, + }] + })) + .expect("JWKS should serialize") +} + +async fn start_spiffe_workload_api(path: &Path, subject: &str) -> FixtureHandle { + let api = SpiffeWorkloadApi { + subject: Arc::::from(subject), + jwks: Arc::new(jwks()), + encoding_key: Arc::new( + EncodingKey::from_rsa_pem(TEST_RSA_PRIVATE_KEY.as_bytes()) + .expect("test RSA key should parse"), + ), + }; + let endpoint = path.to_string_lossy(); + if endpoint.starts_with("tcp:") { + let listen = std::env::var("OPENSHELL_E2E_PROVIDER_SPIFFE_LISTEN") + .expect("OPENSHELL_E2E_PROVIDER_SPIFFE_LISTEN must be set for TCP SPIFFE fixture"); + let listener = TcpListener::bind(&listen) + .await + .expect("bind TCP SPIFFE Workload API fixture"); + let incoming = TcpListenerStream::new(listener); + let task = tokio::spawn(async move { + let result = tonic::transport::Server::builder() + .add_service(SpiffeWorkloadApiServer::new(api)) + .serve_with_incoming(incoming) + .await; + if let Err(err) = result { + eprintln!("SPIFFE Workload API fixture failed: {err}"); + } + }); + return FixtureHandle { task }; + } + let _ = fs::remove_file(path); + let listener = UnixListener::bind(path).expect("bind SPIFFE Workload API socket"); + let mut permissions = fs::metadata(path) + .expect("stat SPIFFE Workload API socket") + .permissions(); + permissions.set_mode(0o777); + fs::set_permissions(path, permissions).expect("chmod SPIFFE Workload API socket"); + let incoming = UnixListenerStream::new(listener); + let task = tokio::spawn(async move { + let result = tonic::transport::Server::builder() + .add_service(SpiffeWorkloadApiServer::new(api)) + .serve_with_incoming(incoming) + .await; + if let Err(err) = result { + eprintln!("SPIFFE Workload API fixture failed: {err}"); + } + }); + FixtureHandle { task } +} + +async fn start_gateway_token_endpoint(port: u16) -> FixtureHandle { + let listener = TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, port))) + .await + .expect("bind gateway token endpoint"); + let task = tokio::spawn(async move { + loop { + let Ok((mut stream, _peer)) = listener.accept().await else { + break; + }; + tokio::spawn(async move { + let mut buf = vec![0_u8; 8192]; + let n = stream.read(&mut buf).await.unwrap_or(0); + let request = String::from_utf8_lossy(&buf[..n]); + let access_token = if request.starts_with("POST /token ") + && request.contains("subject_token=stored-user-token") + && request.contains("client_assertion=") + { + Some(INTERMEDIATE_TOKEN) + } else if request.starts_with("POST /token ") + && request.contains("subject_token=intermediate-token") + && request.contains("client_assertion=") + { + Some(FINAL_ACCESS_TOKEN) + } else { + None + }; + let (status, body) = if let Some(access_token) = access_token { + ( + "HTTP/1.1 200 OK", + json!({ + "access_token": access_token, + "token_type": "Bearer", + "expires_in": 300 + }) + .to_string(), + ) + } else { + ( + "HTTP/1.1 400 Bad Request", + json!({"error": "unexpected_token_exchange"}).to_string(), + ) + }; + let response = format!( + "{status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}", + body.len() + ); + let _ = stream.write_all(response.as_bytes()).await; + }); + } + }); + FixtureHandle { task } +} + +async fn start_protected_target(port: u16) -> FixtureHandle { + let listener = TcpListener::bind(SocketAddr::from((Ipv4Addr::UNSPECIFIED, port))) + .await + .expect("bind protected target"); + let task = tokio::spawn(async move { + loop { + let Ok((mut stream, _peer)) = listener.accept().await else { + break; + }; + tokio::spawn(async move { + let mut buf = vec![0_u8; 8192]; + let n = stream.read(&mut buf).await.unwrap_or(0); + let request = String::from_utf8_lossy(&buf[..n]); + let ok = request.lines().any(|line| { + line.eq_ignore_ascii_case(&format!( + "authorization: Bearer {FINAL_ACCESS_TOKEN}" + )) + }); + let (status, body) = if ok { + ("HTTP/1.1 200 OK", "token-exchange-ok") + } else { + ("HTTP/1.1 401 Unauthorized", "missing-final-token") + }; + let response = format!( + "{status}\r\ncontent-type: text/plain\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}", + body.len() + ); + let _ = stream.write_all(response.as_bytes()).await; + }); + } + }); + FixtureHandle { task } +} + +async fn run_cli(args: &[&str]) -> Result { + let output = openshell_cmd() + .args(args) + .output() + .await + .map_err(|err| format!("spawn openshell: {err}"))?; + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let combined = format!("{stdout}{stderr}"); + if output.status.success() { + Ok(combined) + } else { + Err(format!( + "openshell {:?} failed with {:?}:\n{combined}", + args, + output.status.code() + )) + } +} + +async fn run_cli_ignore_error(args: &[&str]) { + let _ = openshell_cmd().args(args).output().await; +} + +fn write_profile(profile_type: &str, token_port: u16, target_port: u16) -> NamedTempFile { + let token_endpoint = format!("http://127.0.0.1:{token_port}/token"); + let mut file = tempfile::Builder::new() + .suffix(".yaml") + .tempfile() + .expect("create provider profile temp file"); + let profile = format!( + r#"id: {profile_type} +display_name: Podman token exchange e2e +description: Podman e2e provider profile for two-stage token exchange +category: other +credentials: + - name: subject_token + description: Stored user subject token + required: true + - name: access_token + description: Access token obtained through token exchange + required: false + auth_style: bearer + header_name: Authorization + token_grant: + grant_type: token_exchange + token_endpoint: {token_endpoint} + audience: final-audience + jwt_svid_audience: {token_endpoint} + client_assertion_type: {CLIENT_ASSERTION_TYPE} + requested_token_type: {TOKEN_TYPE_ACCESS_TOKEN} + cache_ttl_seconds: 30 + subject_token: + source: provider_credential + credential: subject_token + subject_token_type: {TOKEN_TYPE_ACCESS_TOKEN} +endpoints: + - host: host.openshell.internal + port: {target_port} + protocol: rest + tls: none + access: read-write + enforcement: enforce + allowed_ips: + - 10.0.0.0/8 + - 172.0.0.0/8 + - 192.168.0.0/16 +binaries: + - /usr/bin/curl + - /usr/local/bin/curl +"# + ); + file.write_all(profile.as_bytes()) + .expect("write provider profile"); + file.flush().expect("flush provider profile"); + file +} + +fn sandbox_script(token_port: u16) -> String { + let _ = token_port; + r#"set -eu +echo token-server-ready +while true; do sleep 60; done +"# + .to_string() +} + +fn container_token_endpoint_script() -> String { + format!( + r#" +import json +import sys +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import parse_qs + +PORT = int(sys.argv[1]) + +class Handler(BaseHTTPRequestHandler): + def do_POST(self): + if self.path != "/token": + self.send_response(404) + self.end_headers() + return + length = int(self.headers.get("content-length", "0")) + params = parse_qs(self.rfile.read(length).decode()) + subject_token = params.get("subject_token", [""])[0] + client_assertion = params.get("client_assertion", [""])[0] + if subject_token == "{USER_SUBJECT_TOKEN}" and client_assertion: + access_token = "{INTERMEDIATE_TOKEN}" + elif subject_token == "{INTERMEDIATE_TOKEN}" and client_assertion: + access_token = "{FINAL_ACCESS_TOKEN}" + else: + self.send_response(400) + body = json.dumps({{"error": "unexpected_token_exchange"}}).encode() + self.send_header("content-type", "application/json") + self.send_header("content-length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + body = json.dumps({{ + "access_token": access_token, + "token_type": "Bearer", + "expires_in": 300, + }}).encode() + self.send_response(200) + self.send_header("content-type", "application/json") + self.send_header("content-length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, fmt, *args): + return + +HTTPServer(("127.0.0.1", PORT), Handler).serve_forever() +"# + ) +} + +async fn start_container_token_endpoint(sandbox_name: &str, token_port: u16) -> Result<(), String> { + let socket = std::env::var("OPENSHELL_PODMAN_SOCKET") + .map_err(|_| "OPENSHELL_PODMAN_SOCKET must be set by e2e-podman.sh".to_string())?; + let container_name = podman_container_name_for_sandbox(&socket, sandbox_name).await?; + let mut cmd = Command::new("podman"); + cmd.arg("--url") + .arg(format!("unix://{socket}")) + .arg("exec") + .arg("-d") + .arg(&container_name) + .arg("python3") + .arg("-c") + .arg(container_token_endpoint_script()) + .arg(token_port.to_string()); + apply_podman_config_env(&mut cmd); + let output = cmd + .output() + .await + .map_err(|err| format!("spawn podman exec token endpoint: {err}"))?; + if !output.status.success() { + return Err(format!( + "podman exec token endpoint failed: {}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + + for _ in 0..20 { + if container_loopback_port_ready(&container_name, token_port).await { + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(250)).await; + } + Err(format!( + "container token endpoint did not become ready on 127.0.0.1:{token_port}" + )) +} + +async fn podman_container_name_for_sandbox( + socket: &str, + sandbox_name: &str, +) -> Result { + let mut cmd = Command::new("podman"); + cmd.arg("--url") + .arg(format!("unix://{socket}")) + .arg("ps") + .arg("--filter") + .arg(format!("label=openshell.ai/sandbox-name={sandbox_name}")) + .arg("--format") + .arg("{{.Names}}"); + apply_podman_config_env(&mut cmd); + let output = cmd + .output() + .await + .map_err(|err| format!("spawn podman ps for sandbox container: {err}"))?; + if !output.status.success() { + return Err(format!( + "podman ps for sandbox container failed: {}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + let names = String::from_utf8_lossy(&output.stdout) + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(str::to_string) + .collect::>(); + match names.as_slice() { + [name] => Ok(name.clone()), + [] => Err(format!( + "no running Podman container found for sandbox '{sandbox_name}'" + )), + _ => Err(format!( + "multiple running Podman containers found for sandbox '{sandbox_name}': {}", + names.join(", ") + )), + } +} + +fn apply_podman_config_env(cmd: &mut Command) { + if std::env::var_os("OPENSHELL_E2E_CONTAINER_ENGINE_UNSET_XDG_CONFIG_HOME").is_some() { + cmd.env_remove("XDG_CONFIG_HOME"); + } else if let Some(value) = std::env::var_os("OPENSHELL_E2E_CONTAINER_ENGINE_XDG_CONFIG_HOME") { + cmd.env("XDG_CONFIG_HOME", value); + } +} + +async fn container_loopback_port_ready(container_name: &str, token_port: u16) -> bool { + let Ok(socket) = std::env::var("OPENSHELL_PODMAN_SOCKET") else { + return false; + }; + let probe = format!( + "import socket; s=socket.create_connection(('127.0.0.1', {token_port}), 1); s.close()" + ); + let mut cmd = Command::new("podman"); + cmd.arg("--url") + .arg(format!("unix://{socket}")) + .arg("exec") + .arg(container_name) + .arg("python3") + .arg("-c") + .arg(probe); + apply_podman_config_env(&mut cmd); + cmd.stdout(Stdio::null()).stderr(Stdio::null()); + cmd.status() + .await + .map(|status| status.success()) + .unwrap_or(false) +} + +async fn sandbox_exec_curl(sandbox_name: &str, target_port: u16) -> Result { + let url = format!("http://host.openshell.internal:{target_port}/resource"); + for _ in 0..20 { + let output = openshell_cmd() + .args([ + "sandbox", + "exec", + "--name", + sandbox_name, + "--no-tty", + "--", + "curl", + "-fsS", + &url, + ]) + .output() + .await + .map_err(|err| format!("spawn openshell sandbox exec: {err}"))?; + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let combined = format!("{stdout}{stderr}"); + if output.status.success() { + return Ok(combined); + } + tokio::time::sleep(Duration::from_millis(500)).await; + } + Err(format!("curl to protected target did not succeed at {url}")) +} + +#[tokio::test] +async fn podman_provider_token_exchange_injects_bearer_header() { + let gateway_socket = PathBuf::from( + std::env::var("OPENSHELL_E2E_GATEWAY_SPIFFE_SOCKET") + .expect("OPENSHELL_E2E_GATEWAY_SPIFFE_SOCKET must be set by e2e-podman.sh"), + ); + let provider_socket = PathBuf::from( + std::env::var("OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET") + .expect("OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET must be set by e2e-podman.sh"), + ); + + let profile_type = format!("podman-token-exchange-e2e-{}", std::process::id()); + let provider_name = format!("podman-token-exchange-e2e-{}", std::process::id()); + let token_port = find_free_port(); + let target_port = find_free_port(); + let token_endpoint = format!("http://127.0.0.1:{token_port}/token"); + let gateway_subject = format!("spiffe://{TRUST_DOMAIN}/openshell/gateway"); + let supervisor_subject = format!("spiffe://{TRUST_DOMAIN}/openshell/sandbox/e2e"); + + let _gateway_spiffe = start_spiffe_workload_api(&gateway_socket, &gateway_subject).await; + let _provider_spiffe = start_spiffe_workload_api(&provider_socket, &supervisor_subject).await; + let _gateway_token = start_gateway_token_endpoint(token_port).await; + let _target = start_protected_target(target_port).await; + + run_cli(&[ + "settings", + "set", + "--global", + "--key", + "providers_v2_enabled", + "--value", + "true", + "--yes", + ]) + .await + .expect("enable providers v2"); + + run_cli_ignore_error(&["provider", "delete", &provider_name, "--yes"]).await; + run_cli_ignore_error(&["provider", "profile", "delete", &profile_type, "--yes"]).await; + + let profile = write_profile(&profile_type, token_port, target_port); + let profile_path = profile + .path() + .to_str() + .expect("profile path should be UTF-8"); + run_cli(&["provider", "profile", "import", "-f", profile_path]) + .await + .expect("import provider profile"); + run_cli(&[ + "provider", + "create", + "--name", + &provider_name, + "--type", + &profile_type, + "--credential", + &format!("subject_token={USER_SUBJECT_TOKEN}"), + ]) + .await + .expect("create provider"); + + let script = sandbox_script(token_port); + let mut sandbox = SandboxGuard::create_keep_with_args( + &["--provider", &provider_name], + &["sh", "-lc", &script], + "token-server-ready", + ) + .await + .unwrap_or_else(|err| { + panic!( + "sandbox should complete token exchange against {token_endpoint} and protected target port {target_port}:\n{err}" + ) + }); + start_container_token_endpoint(&sandbox.name, token_port) + .await + .expect("start container token endpoint"); + let curl_output = sandbox_exec_curl(&sandbox.name, target_port) + .await + .expect("curl protected target from kept sandbox"); + + run_cli_ignore_error(&["provider", "delete", &provider_name, "--yes"]).await; + run_cli_ignore_error(&["provider", "profile", "delete", &profile_type, "--yes"]).await; + sandbox.cleanup().await; + + assert!( + curl_output.contains("token-exchange-ok"), + "protected target should receive the final exchanged bearer token:\n{}", + curl_output + ); +} diff --git a/e2e/with-podman-gateway.sh b/e2e/with-podman-gateway.sh index cd52e007ab..59ea8423ec 100755 --- a/e2e/with-podman-gateway.sh +++ b/e2e/with-podman-gateway.sh @@ -86,6 +86,15 @@ podman_cmd() { WORKDIR_PARENT="${TMPDIR:-/tmp}" WORKDIR_PARENT="${WORKDIR_PARENT%/}" WORKDIR="$(mktemp -d "${WORKDIR_PARENT}/openshell-e2e-podman.XXXXXX")" +if [ "${OPENSHELL_E2E_SPIFFE_FIXTURE:-0}" = "1" ]; then + mkdir -p "${WORKDIR}/spiffe" + export OPENSHELL_E2E_GATEWAY_SPIFFE_SOCKET="${OPENSHELL_E2E_GATEWAY_SPIFFE_SOCKET:-${WORKDIR}/spiffe/gateway.sock}" + if [ -z "${OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET:-}" ]; then + OPENSHELL_E2E_PROVIDER_SPIFFE_PORT="$(e2e_pick_port)" + export OPENSHELL_E2E_PROVIDER_SPIFFE_LISTEN="0.0.0.0:${OPENSHELL_E2E_PROVIDER_SPIFFE_PORT}" + export OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET="tcp:169.254.1.2:${OPENSHELL_E2E_PROVIDER_SPIFFE_PORT}" + fi +fi GATEWAY_BIN="" CLI_BIN="" GATEWAY_PID="" @@ -456,6 +465,9 @@ cp "${ROOT}/deploy/rpm/gateway.toml.default" "${GATEWAY_CONFIG}" printf 'guest_tls_cert = %s\n' "$(toml_string "${PKI_DIR}/client/tls.crt")" printf 'guest_tls_key = %s\n' "$(toml_string "${PKI_DIR}/client/tls.key")" printf 'enable_bind_mounts = true\n' + if [ -n "${OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET:-}" ]; then + printf 'provider_spiffe_workload_api_socket = %s\n' "$(toml_string "${OPENSHELL_E2E_PROVIDER_SPIFFE_SOCKET}")" + fi # The in-process Podman driver reads `socket_path` from TOML only — the # OPENSHELL_PODMAN_SOCKET env var is honoured by the standalone driver # binary, not the in-process driver used here. Pin the socket to the one @@ -500,6 +512,7 @@ e2e_export_gateway_restart_metadata \ OPENSHELL_SUPERVISOR_IMAGE="${SUPERVISOR_IMAGE}" \ OPENSHELL_NETWORK_NAME="${PODMAN_NETWORK_NAME}" \ +OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET="${OPENSHELL_E2E_GATEWAY_SPIFFE_SOCKET:-}" \ "${GATEWAY_BIN}" "${GATEWAY_ARGS[@]}" >"${GATEWAY_LOG}" 2>&1 & GATEWAY_PID=$! printf '%s\n' "${GATEWAY_PID}" >"${GATEWAY_PID_FILE}" diff --git a/examples/spiffe-token-exchange-demo/README.md b/examples/spiffe-token-exchange-demo/README.md new file mode 100644 index 0000000000..9c6a50b9c2 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/README.md @@ -0,0 +1,231 @@ +# SPIFFE Token Exchange Demo + +This example validates provider dynamic token exchange using SPIFFE JWT-SVIDs. +It runs alongside `examples/spiffe-token-grant-demo` but exercises the +`token_exchange` grant type instead of `client_credentials`. + +The demo deploys three in-cluster workloads: + +| Workload | Purpose | +|---|---| +| `token-exchange-issuer` | Issues a demo user subject token, performs the gateway intermediate token exchange, and performs the supervisor final token exchange | +| `alpha-exchange` | Requires a final bearer token with audience and scope `alpha` | +| `beta-exchange` | Requires a final bearer token with audience and scope `beta` | + +The OpenShell provider profile in `provider-profile.yaml` declares a stored +`subject_token` credential and a runtime `access_token` credential with +`token_grant.grant_type: token_exchange`. + +The profile declares exact Kubernetes service hostnames for `alpha-exchange` +and `beta-exchange`. It intentionally does not set `allowed_ips`, because +cluster service CIDRs vary across Kubernetes installations. + +When a sandbox curls `alpha-exchange` or `beta-exchange`: + +1. The supervisor fetches its SPIFFE JWT-SVID. +2. The supervisor asks the gateway for an intermediate token. +3. The gateway verifies the supervisor SVID, fetches its own gateway JWT-SVID, + and exchanges the stored provider `subject_token` at `token-exchange-issuer`. + The requested intermediate audience is the supervisor SPIFFE ID. +4. The supervisor exchanges the intermediate token at the same token endpoint + for the final alpha/beta access token. +5. The supervisor injects that final token into the outbound HTTP request. + +## Prerequisites + +- A Kubernetes OpenShell dev cluster. +- SPIRE enabled for provider token grants and gateway token exchange. +- Gateway and supervisor access to SPIRE OIDC/JWKS discovery. +- OpenShell configured with the Kubernetes ServiceAccount supervisor bootstrap + path. +- `providers_v2_enabled=true` on the target gateway. +- Local `curl`, `python3`, `openssl`, `nc`, `kubectl`, and `openshell`. +- A registered and logged-in CLI gateway. The script uses `GATEWAY_NAME`, then + `OPENSHELL_GATEWAY`, then the active OpenShell gateway selection. + +For the Helm dev environment, deploy with the SPIRE releases and +`ci/values-spire.yaml` enabled in `deploy/helm/openshell/skaffold.yaml`. + +The demo assumes these SPIFFE ID prefixes: + +| Identity | Prefix | +|---|---| +| Gateway | `spiffe://openshell.local/ns/openshell/sa/` | +| Supervisor | `spiffe://openshell.local/openshell/sandbox/` | + +Override `GATEWAY_TRUST_DOMAIN_PREFIX` or `SUPERVISOR_TRUST_DOMAIN_PREFIX` in +`k8s/workloads.yaml` if your development cluster uses different SPIFFE IDs. + +The demo issuer fetches SPIRE JWKS from the in-cluster OIDC discovery service +to verify JWT-SVID signatures. The issuer pod runs a `spiffe-helper` sidecar +that writes the SPIFFE bundle into a shared volume. The Node issuer uses that +bundle as `SPIRE_JWKS_CA_FILE` when fetching JWKS over HTTPS. + +## Kubeconfig And Mise + +The repository `mise.toml` sets `KUBECONFIG` to the repo-local `kubeconfig` +when your shell activates the OpenShell directory. If you are testing against a +different cluster, run these commands from outside the repository and pass the +target kubeconfig explicitly. + +```bash +export OPENSHELL_REPO=/path/to/OpenShell +export DEMO_KUBECONFIG=/path/to/your/kubeconfig +export OPENSHELL_GATEWAY=local +``` + +## Deploy Workloads + +From a directory outside the repository: + +```bash +ACCESS_TOKEN_SECRET="$(openssl rand -hex 32)" +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default create secret generic openshell-spiffe-token-exchange-demo \ + --from-literal=access-token-secret="$ACCESS_TOKEN_SECRET" \ + --dry-run=client \ + -o yaml | KUBECONFIG="$DEMO_KUBECONFIG" kubectl apply -f - +KUBECONFIG="$DEMO_KUBECONFIG" kubectl apply -k "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/k8s" +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default rollout restart deployment/token-exchange-issuer deployment/alpha-exchange deployment/beta-exchange +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default rollout status deployment/token-exchange-issuer --timeout=180s +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default rollout status deployment/alpha-exchange --timeout=180s +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default rollout status deployment/beta-exchange --timeout=180s +``` + +## Register Provider And Test + +Port-forward the local gateway in one terminal: + +```bash +KUBECONFIG="$DEMO_KUBECONFIG" kubectl port-forward -n openshell svc/openshell 8097:8080 +``` + +Copy the Helm-generated TLS client bundle into the CLI config used for this +demo. This uses the same gateway name as `OPENSHELL_GATEWAY`. + +```bash +mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/openshell/gateways/${OPENSHELL_GATEWAY}/mtls" +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n openshell get secret openshell-client-tls \ + -o jsonpath='{.data.ca\.crt}' | base64 -d > "${XDG_CONFIG_HOME:-$HOME/.config}/openshell/gateways/${OPENSHELL_GATEWAY}/mtls/ca.crt" +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n openshell get secret openshell-client-tls \ + -o jsonpath='{.data.tls\.crt}' | base64 -d > "${XDG_CONFIG_HOME:-$HOME/.config}/openshell/gateways/${OPENSHELL_GATEWAY}/mtls/tls.crt" +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n openshell get secret openshell-client-tls \ + -o jsonpath='{.data.tls\.key}' | base64 -d > "${XDG_CONFIG_HOME:-$HOME/.config}/openshell/gateways/${OPENSHELL_GATEWAY}/mtls/tls.key" +``` + +Port-forward the token exchange issuer in another terminal and fetch a demo +subject token: + +```bash +KUBECONFIG="$DEMO_KUBECONFIG" kubectl port-forward -n default svc/token-exchange-issuer 18080:80 +SUBJECT_TOKEN="$( + curl -fsS http://127.0.0.1:18080/demo-subject-token | + python3 -c 'import json, sys; print(json.load(sys.stdin)["access_token"])' +)" +``` + +Then run: + +```bash +export GATEWAY=https://127.0.0.1:8097 + +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" settings set \ + --global --key providers_v2_enabled --value true --yes + +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" provider profile import \ + -f "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/provider-profile.yaml" + +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" provider create \ + --name spiffe-token-exchange-demo \ + --type spiffe-token-exchange-demo \ + --credential "subject_token=${SUBJECT_TOKEN}" + +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" sandbox create \ + --name spiffe-token-exchange-demo \ + --provider spiffe-token-exchange-demo \ + --keep \ + --no-tty \ + -- echo "sandbox ready" + +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" sandbox exec \ + --name spiffe-token-exchange-demo \ + --no-tty \ + -- curl -sS http://alpha-exchange.default.svc.cluster.local/ + +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" sandbox exec \ + --name spiffe-token-exchange-demo \ + --no-tty \ + -- curl -sS http://beta-exchange.default.svc.cluster.local/ +``` + +Expected output includes the demo user as the token subject and the sandbox +SPIFFE ID as the authorized party/client: + +```text +alpha called with path /: + sub: demo-user + aud: alpha, account + scope: alpha profile email + azp: spiffe://openshell.local/openshell/sandbox/ + client_id: spiffe://openshell.local/openshell/sandbox/ + +beta called with path /: + sub: demo-user + aud: beta, account + scope: beta profile email + azp: spiffe://openshell.local/openshell/sandbox/ + client_id: spiffe://openshell.local/openshell/sandbox/ +``` + +The token issuer logs both token exchange phases: + +```bash +KUBECONFIG="$DEMO_KUBECONFIG" kubectl -n default logs deployment/token-exchange-issuer --tail=40 +``` + +Example log lines: + +```text +issued intermediate token for user=demo-user audience=spiffe://openshell.local/openshell/sandbox/ +issued final token for user=demo-user audience=alpha client=spiffe://openshell.local/openshell/sandbox/ +issued final token for user=demo-user audience=beta client=spiffe://openshell.local/openshell/sandbox/ +``` + +## Automated Demo + +`demo.sh` applies the workloads, fetches a demo subject token, registers the +provider profile, creates a sandbox, curls alpha/beta, and deletes the sandbox +with `openshell` on exit. It leaves the Kubernetes demo workloads in place and +prints diagnostics only when the run fails. + +```bash +cd /tmp +KUBECONFIG="$DEMO_KUBECONFIG" bash "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/demo.sh" +``` + +The script reuses your normal OpenShell CLI config so it can load the stored +OIDC token for `OPENSHELL_GATEWAY`. If you set `ISOLATED_CONFIG=1`, register +and log in to the gateway in that isolated config before running the demo. + +## Podman Demo + +A local Podman variant lives in `podman/`. It reuses the same dummy token +issuer and protected service code, starts local SPIRE and demo service +containers, manually registers one sandbox SPIFFE entry, and runs the same +alpha/beta token exchange checks. + +See `podman/README.md` for the required gateway configuration and script +usage. + +## Cleanup + +Delete the sandbox through OpenShell: + +```bash +openshell --gateway "$OPENSHELL_GATEWAY" --gateway-endpoint "$GATEWAY" sandbox delete spiffe-token-exchange-demo +``` + +Delete the demo workloads with Kubernetes: + +```bash +KUBECONFIG="$DEMO_KUBECONFIG" kubectl delete -k "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/k8s" +``` diff --git a/examples/spiffe-token-exchange-demo/demo.sh b/examples/spiffe-token-exchange-demo/demo.sh new file mode 100755 index 0000000000..e50f989454 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/demo.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROFILE_FILE="${SCRIPT_DIR}/provider-profile.yaml" +K8S_DIR="${SCRIPT_DIR}/k8s" + +SANDBOX_NAME="${SANDBOX_NAME:-spiffe-token-demo}" +PROVIDER_NAME="${PROVIDER_NAME:-spiffe-token-exchange-demo}" +PROFILE_ID="${PROFILE_ID:-spiffe-token-exchange-demo}" +PORT_FORWARD_PORT="${PORT_FORWARD_PORT:-8097}" +TOKEN_ISSUER_PORT="${TOKEN_ISSUER_PORT:-18080}" +GATEWAY_ENDPOINT="${GATEWAY_ENDPOINT:-https://127.0.0.1:${PORT_FORWARD_PORT}}" +KEEP_SANDBOX="${KEEP_SANDBOX:-0}" +ISOLATED_CONFIG="${ISOLATED_CONFIG:-0}" +ACCESS_TOKEN_SECRET="${ACCESS_TOKEN_SECRET:-$(openssl rand -hex 32)}" + +TEMP_CONFIG_HOME="" +if [[ "$ISOLATED_CONFIG" == "1" ]]; then + TEMP_CONFIG_HOME="$(mktemp -d)" + export XDG_CONFIG_HOME="$TEMP_CONFIG_HOME" +fi + +default_gateway_name() { + if [[ -n "${GATEWAY_NAME:-}" ]]; then + printf "%s\n" "$GATEWAY_NAME" + return + fi + if [[ -n "${OPENSHELL_GATEWAY:-}" ]]; then + printf "%s\n" "$OPENSHELL_GATEWAY" + return + fi + + local config_home="${XDG_CONFIG_HOME:-$HOME/.config}" + if [[ -s "${config_home}/openshell/active_gateway" ]]; then + head -n1 "${config_home}/openshell/active_gateway" + return + fi + if [[ -s /etc/openshell/active_gateway ]]; then + head -n1 /etc/openshell/active_gateway + return + fi + + printf "k8s\n" +} + +GATEWAY_NAME="$(default_gateway_name)" + +PF_PID="" +TOKEN_PF_PID="" + +dump_diagnostics() { + set +e + + printf "\n=== diagnostics: openshell sandbox logs ===\n" >&2 + "${OS[@]}" logs "$SANDBOX_NAME" -n 120 --source sandbox >&2 + + printf "\n=== diagnostics: gateway logs ===\n" >&2 + kubectl -n openshell logs -l app.kubernetes.io/name=openshell,app.kubernetes.io/instance=openshell \ + --tail=120 --prefix=true >&2 + + printf "\n=== diagnostics: token exchange issuer logs ===\n" >&2 + kubectl -n default logs -l app=token-exchange-issuer --tail=120 --prefix=true >&2 + + printf "\n=== diagnostics: alpha logs ===\n" >&2 + kubectl -n default logs -l app=alpha-exchange --tail=60 --prefix=true >&2 + + printf "\n=== diagnostics: beta logs ===\n" >&2 + kubectl -n default logs -l app=beta-exchange --tail=60 --prefix=true >&2 + + printf "\n=== diagnostics: gateway port-forward log ===\n" >&2 + sed 's/^/gateway-port-forward> /' /tmp/openshell-spiffe-token-exchange-demo-gateway-port-forward.log >&2 + + printf "\n=== diagnostics: token issuer port-forward log ===\n" >&2 + sed 's/^/issuer-port-forward> /' /tmp/openshell-spiffe-token-exchange-demo-issuer-port-forward.log >&2 +} + +cleanup() { + if [[ "$KEEP_SANDBOX" != "1" ]]; then + openshell --gateway "$GATEWAY_NAME" --gateway-endpoint "$GATEWAY_ENDPOINT" sandbox delete "$SANDBOX_NAME" >/dev/null 2>&1 || true + fi + if [[ -n "$PF_PID" ]]; then + kill "$PF_PID" >/dev/null 2>&1 || true + fi + if [[ -n "$TOKEN_PF_PID" ]]; then + kill "$TOKEN_PF_PID" >/dev/null 2>&1 || true + fi + if [[ -n "$TEMP_CONFIG_HOME" ]]; then + rm -rf "$TEMP_CONFIG_HOME" + fi +} + +on_exit() { + local status="$1" + if [[ "$status" -ne 0 ]]; then + dump_diagnostics || true + fi + cleanup + exit "$status" +} +trap 'on_exit $?' EXIT + +run() { + printf "\n$ %s\n" "$*" + "$@" +} + +wait_for_port() { + local port="$1" + local label="$2" + for _ in $(seq 1 60); do + if nc -z 127.0.0.1 "$port" >/dev/null 2>&1; then + return 0 + fi + sleep 0.25 + done + printf "%s port-forward did not become ready\n" "$label" >&2 + exit 1 +} + +assert_contains() { + local haystack="$1" + local needle="$2" + if [[ "$haystack" != *"$needle"* ]]; then + printf "expected output to contain: %s\n" "$needle" >&2 + printf "actual output:\n%s\n" "$haystack" >&2 + exit 1 + fi +} + +install_gateway_tls_bundle() { + local config_home="${XDG_CONFIG_HOME:-$HOME/.config}" + local tls_dir="${config_home}/openshell/gateways/${GATEWAY_NAME}/mtls" + mkdir -p "$tls_dir" + kubectl -n openshell get secret openshell-client-tls \ + -o jsonpath='{.data.ca\.crt}' | base64 -d >"${tls_dir}/ca.crt" + kubectl -n openshell get secret openshell-client-tls \ + -o jsonpath='{.data.tls\.crt}' | base64 -d >"${tls_dir}/tls.crt" + kubectl -n openshell get secret openshell-client-tls \ + -o jsonpath='{.data.tls\.key}' | base64 -d >"${tls_dir}/tls.key" +} + +subject_token_from_json() { + python3 -c 'import json, sys; print(json.load(sys.stdin)["access_token"])' +} + +sandbox_curl_until() { + local label="$1" + local url="$2" + local expected="$3" + local output="" + + for attempt in $(seq 1 12); do + printf "\n$ openshell sandbox exec %s curl (attempt %s)\n" "$label" "$attempt" + if output=$("${OS[@]}" sandbox exec --name "$SANDBOX_NAME" --no-tty -- curl -sS --max-time 10 "$url" 2>&1); then + printf "%s\n" "$output" + if [[ "$output" == *"$expected"* ]]; then + SANDBOX_CURL_OUTPUT="$output" + return 0 + fi + else + printf "%s\n" "$output" + fi + sleep 2 + done + + printf "timed out waiting for %s to return expected output\n" "$label" >&2 + printf "last output:\n%s\n" "$output" >&2 + exit 1 +} + +OS=(openshell --gateway "$GATEWAY_NAME" --gateway-endpoint "$GATEWAY_ENDPOINT") + +printf "Using OpenShell gateway '%s' at %s\n" "$GATEWAY_NAME" "$GATEWAY_ENDPOINT" + +printf "\n$ kubectl -n default create secret generic openshell-spiffe-token-exchange-demo --from-literal=access-token-secret=*** --dry-run=client -o yaml | kubectl apply -f -\n" +kubectl -n default create secret generic openshell-spiffe-token-exchange-demo \ + --from-literal=access-token-secret="$ACCESS_TOKEN_SECRET" \ + --dry-run=client \ + -o yaml | kubectl apply -f - + +run kubectl apply -k "$K8S_DIR" +run kubectl -n default rollout restart deployment/token-exchange-issuer deployment/alpha-exchange deployment/beta-exchange +run kubectl -n default rollout status deployment/token-exchange-issuer --timeout=180s +run kubectl -n default rollout status deployment/alpha-exchange --timeout=180s +run kubectl -n default rollout status deployment/beta-exchange --timeout=180s + +kubectl -n openshell port-forward svc/openshell "${PORT_FORWARD_PORT}:8080" >/tmp/openshell-spiffe-token-exchange-demo-gateway-port-forward.log 2>&1 & +PF_PID=$! +wait_for_port "$PORT_FORWARD_PORT" "gateway" +install_gateway_tls_bundle + +kubectl -n default port-forward svc/token-exchange-issuer "${TOKEN_ISSUER_PORT}:80" >/tmp/openshell-spiffe-token-exchange-demo-issuer-port-forward.log 2>&1 & +TOKEN_PF_PID=$! +wait_for_port "$TOKEN_ISSUER_PORT" "token issuer" + +SUBJECT_TOKEN="$(curl -fsS "http://127.0.0.1:${TOKEN_ISSUER_PORT}/demo-subject-token" | subject_token_from_json)" + +"${OS[@]}" sandbox delete "$SANDBOX_NAME" >/dev/null 2>&1 || true +"${OS[@]}" provider delete "$PROVIDER_NAME" >/dev/null 2>&1 || true +"${OS[@]}" provider profile delete "$PROFILE_ID" >/dev/null 2>&1 || true + +run "${OS[@]}" settings set --global --key providers_v2_enabled --value true --yes +run "${OS[@]}" provider profile lint -f "$PROFILE_FILE" +run "${OS[@]}" provider profile import -f "$PROFILE_FILE" +run "${OS[@]}" provider create --name "$PROVIDER_NAME" --type "$PROFILE_ID" --credential "subject_token=${SUBJECT_TOKEN}" +run "${OS[@]}" sandbox create --name "$SANDBOX_NAME" --provider "$PROVIDER_NAME" --keep --no-tty -- echo "sandbox ready" + +sandbox_curl_until "alpha" "http://alpha-exchange.default.svc.cluster.local/" "alpha called with path /:" +ALPHA_OUTPUT="$SANDBOX_CURL_OUTPUT" +assert_contains "$ALPHA_OUTPUT" "alpha called with path /:" +assert_contains "$ALPHA_OUTPUT" "sub: demo-user" +assert_contains "$ALPHA_OUTPUT" "aud: alpha, account" +assert_contains "$ALPHA_OUTPUT" "scope: alpha profile email" +assert_contains "$ALPHA_OUTPUT" "azp: spiffe://openshell.local/openshell/sandbox/" + +sandbox_curl_until "beta" "http://beta-exchange.default.svc.cluster.local/" "beta called with path /:" +BETA_OUTPUT="$SANDBOX_CURL_OUTPUT" +assert_contains "$BETA_OUTPUT" "beta called with path /:" +assert_contains "$BETA_OUTPUT" "sub: demo-user" +assert_contains "$BETA_OUTPUT" "aud: beta, account" +assert_contains "$BETA_OUTPUT" "scope: beta profile email" +assert_contains "$BETA_OUTPUT" "azp: spiffe://openshell.local/openshell/sandbox/" + +printf "\nSPIFFE token exchange demo succeeded.\n" diff --git a/examples/spiffe-token-exchange-demo/k8s/kustomization.yaml b/examples/spiffe-token-exchange-demo/k8s/kustomization.yaml new file mode 100644 index 0000000000..d4cdcb80a0 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/k8s/kustomization.yaml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: + - name: openshell-spiffe-token-exchange-demo-scripts + files: + - token-issuer.js + - protected-service.js + - name: openshell-spiffe-token-exchange-demo-helper + files: + - spiffe-helper.conf + +resources: + - workloads.yaml diff --git a/examples/spiffe-token-exchange-demo/k8s/protected-service.js b/examples/spiffe-token-exchange-demo/k8s/protected-service.js new file mode 100644 index 0000000000..263601c705 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/k8s/protected-service.js @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const http = require("http"); +const crypto = require("crypto"); + +const PORT = Number(process.env.PORT || 8080); +const SERVICE_NAME = process.env.SERVICE_NAME || "alpha"; +const EXPECTED_AUDIENCE = process.env.EXPECTED_AUDIENCE || SERVICE_NAME; +const EXPECTED_SCOPE = process.env.EXPECTED_SCOPE || SERVICE_NAME; +const ACCESS_TOKEN_ISSUER = + process.env.ACCESS_TOKEN_ISSUER || "http://token-exchange-issuer.default.svc.cluster.local"; +const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET; + +if (!ACCESS_TOKEN_SECRET) { + throw new Error("ACCESS_TOKEN_SECRET is required"); +} + +function b64urlDecode(value) { + const padded = `${value}${"=".repeat((4 - (value.length % 4)) % 4)}`; + return Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64"); +} + +function b64urlEncode(value) { + return Buffer.from(value) + .toString("base64") + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); +} + +function parseJwt(jwt) { + const parts = jwt.split("."); + if (parts.length !== 3) { + throw new Error("JWT must contain three segments"); + } + return { + payload: JSON.parse(b64urlDecode(parts[1]).toString("utf8")), + signingInput: `${parts[0]}.${parts[1]}`, + signature: parts[2], + }; +} + +function verifyAccessToken(jwt) { + const parsed = parseJwt(jwt); + const expected = b64urlEncode( + crypto.createHmac("sha256", ACCESS_TOKEN_SECRET).update(parsed.signingInput).digest(), + ); + if ( + parsed.signature.length !== expected.length || + !crypto.timingSafeEqual(Buffer.from(parsed.signature), Buffer.from(expected)) + ) { + throw new Error("access token signature validation failed"); + } + + const now = Math.floor(Date.now() / 1000); + if (parsed.payload.exp && parsed.payload.exp <= now) { + throw new Error("access token expired"); + } + if (parsed.payload.iss !== ACCESS_TOKEN_ISSUER) { + throw new Error(`unexpected access token issuer ${parsed.payload.iss}`); + } + if (parsed.payload.demo_token_use !== "final") { + throw new Error("expected final token"); + } + const aud = Array.isArray(parsed.payload.aud) ? parsed.payload.aud : [parsed.payload.aud]; + if (!aud.includes(EXPECTED_AUDIENCE)) { + throw new Error(`access token audience did not include ${EXPECTED_AUDIENCE}`); + } + const scopes = String(parsed.payload.scope || "").split(/\s+/).filter(Boolean); + if (!scopes.includes(EXPECTED_SCOPE)) { + throw new Error(`access token scope did not include ${EXPECTED_SCOPE}`); + } + return parsed.payload; +} + +function text(res, status, body) { + res.writeHead(status, { + "content-type": "text/plain", + "content-length": Buffer.byteLength(body), + }); + res.end(body); +} + +http + .createServer((req, res) => { + try { + if (req.url === "/healthz") { + return text(res, 200, "ok\n"); + } + const auth = req.headers.authorization || ""; + const token = auth.startsWith("Bearer ") ? auth.slice("Bearer ".length) : ""; + if (!token) { + console.warn(`${SERVICE_NAME} rejected request path=${req.url} reason=missing_bearer_token`); + return text(res, 401, `${SERVICE_NAME} missing bearer token\n`); + } + const claims = verifyAccessToken(token); + const aud = Array.isArray(claims.aud) ? claims.aud.join(", ") : claims.aud; + console.log( + `${SERVICE_NAME} accepted request path=${req.url} sub=${claims.sub} aud="${aud}" scope="${claims.scope}" client_id=${claims.client_id}`, + ); + return text( + res, + 200, + `${SERVICE_NAME} called with path ${req.url}:\n` + + ` sub: ${claims.sub}\n` + + ` aud: ${aud}\n` + + ` iss: ${claims.iss}\n` + + ` scope: ${claims.scope}\n` + + ` azp: ${claims.azp}\n` + + ` client_id: ${claims.client_id}\n`, + ); + } catch (error) { + console.warn(`${SERVICE_NAME} rejected request path=${req.url} reason="${error.message}"`); + return text(res, 403, `${SERVICE_NAME} rejected token: ${error.message}\n`); + } + }) + .listen(PORT, "0.0.0.0", () => { + console.log(`${SERVICE_NAME} listening on ${PORT}`); + }); diff --git a/examples/spiffe-token-exchange-demo/k8s/spiffe-helper.conf b/examples/spiffe-token-exchange-demo/k8s/spiffe-helper.conf new file mode 100644 index 0000000000..93987d00c9 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/k8s/spiffe-helper.conf @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +agent_address = "/run/spire/sockets/spire-agent.sock" +cert_dir = "/target" +svid_file_name = "svid.pem" +svid_key_file_name = "key.pem" +svid_bundle_file_name = "bundle.pem" diff --git a/examples/spiffe-token-exchange-demo/k8s/token-issuer.js b/examples/spiffe-token-exchange-demo/k8s/token-issuer.js new file mode 100644 index 0000000000..1a5d0e7527 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/k8s/token-issuer.js @@ -0,0 +1,342 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const http = require("http"); +const https = require("https"); +const crypto = require("crypto"); +const fs = require("fs"); + +const TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"; +const JWT_SPIFFE_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe"; + +const PORT = Number(process.env.PORT || 8080); +const JWKS_URI = + process.env.SPIRE_JWKS_URI || + "https://spire-spiffe-oidc-discovery-provider.spire.svc.cluster.local/keys"; +const SPIRE_ISSUER = + process.env.SPIRE_ISSUER || + "https://spire-spiffe-oidc-discovery-provider.spire.svc.cluster.local"; +const JWT_SVID_AUDIENCE = + process.env.JWT_SVID_AUDIENCE || "http://token-exchange-issuer.default.svc.cluster.local"; +const SUPERVISOR_TRUST_DOMAIN_PREFIX = + process.env.SUPERVISOR_TRUST_DOMAIN_PREFIX || "spiffe://openshell.local/openshell/sandbox/"; +const GATEWAY_TRUST_DOMAIN_PREFIX = + process.env.GATEWAY_TRUST_DOMAIN_PREFIX || "spiffe://openshell.local/ns/openshell/sa/"; +const ACCESS_TOKEN_ISSUER = + process.env.ACCESS_TOKEN_ISSUER || "http://token-exchange-issuer.default.svc.cluster.local"; +const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET; +const DEMO_USER_SUBJECT = process.env.DEMO_USER_SUBJECT || "demo-user"; +const SPIRE_JWKS_CA_FILE = process.env.SPIRE_JWKS_CA_FILE || ""; +const JWT_SVID_VERIFY_ALGORITHMS = { + ES256: { algorithm: "sha256", dsaEncoding: "ieee-p1363" }, + RS256: { algorithm: "RSA-SHA256" }, +}; + +if (!ACCESS_TOKEN_SECRET) { + throw new Error("ACCESS_TOKEN_SECRET is required"); +} + +let cachedJwks; +let cachedJwksAt = 0; + +function b64urlDecode(value) { + const padded = `${value}${"=".repeat((4 - (value.length % 4)) % 4)}`; + return Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64"); +} + +function b64urlEncode(value) { + return Buffer.from(value) + .toString("base64") + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); +} + +function parseJwt(jwt) { + const parts = jwt.split("."); + if (parts.length !== 3) { + throw new Error("JWT must contain three segments"); + } + return { + header: JSON.parse(b64urlDecode(parts[0]).toString("utf8")), + payload: JSON.parse(b64urlDecode(parts[1]).toString("utf8")), + signingInput: `${parts[0]}.${parts[1]}`, + signature: b64urlDecode(parts[2]), + signatureB64: parts[2], + }; +} + +async function jwks() { + const now = Date.now(); + if (cachedJwks && now - cachedJwksAt < 60000) { + return cachedJwks; + } + cachedJwks = await fetchJson(JWKS_URI); + cachedJwksAt = now; + return cachedJwks; +} + +function fetchJson(url) { + return new Promise((resolve, reject) => { + const parsed = new URL(url); + const isHttps = parsed.protocol === "https:"; + const client = isHttps ? https : http; + const options = {}; + if (isHttps && SPIRE_JWKS_CA_FILE) { + options.ca = fs.readFileSync(SPIRE_JWKS_CA_FILE); + } + + const req = client.get(parsed, options, (res) => { + const chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => { + const body = Buffer.concat(chunks).toString("utf8"); + if (res.statusCode < 200 || res.statusCode >= 300) { + reject(new Error(`JWKS fetch failed with HTTP ${res.statusCode}: ${body}`)); + return; + } + try { + resolve(JSON.parse(body)); + } catch (error) { + reject(error); + } + }); + }); + req.on("error", reject); + req.setTimeout(10000, () => req.destroy(new Error("JWKS fetch timed out"))); + }); +} + +function hasAudience(payload, expected) { + const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud]; + return aud.includes(expected); +} + +async function verifyJwtSvid(jwt, subjectPrefix) { + const parsed = parseJwt(jwt); + const verifyAlgorithm = JWT_SVID_VERIFY_ALGORITHMS[parsed.header.alg]; + if (!verifyAlgorithm) { + throw new Error(`unsupported JWT-SVID alg ${parsed.header.alg}`); + } + + const keys = await jwks(); + const jwk = keys.keys.find((key) => key.kid === parsed.header.kid); + if (!jwk) { + throw new Error(`no JWKS key for kid ${parsed.header.kid}`); + } + + const verifier = crypto.createVerify(verifyAlgorithm.algorithm); + verifier.update(parsed.signingInput); + verifier.end(); + const publicKey = crypto.createPublicKey({ key: jwk, format: "jwk" }); + const verifyOptions = { key: publicKey }; + if (verifyAlgorithm.dsaEncoding) { + verifyOptions.dsaEncoding = verifyAlgorithm.dsaEncoding; + } + if (!verifier.verify(verifyOptions, parsed.signature)) { + throw new Error("JWT-SVID signature validation failed"); + } + + const now = Math.floor(Date.now() / 1000); + if (parsed.payload.exp && parsed.payload.exp <= now) { + throw new Error("JWT-SVID expired"); + } + if (parsed.payload.nbf && parsed.payload.nbf > now + 30) { + throw new Error("JWT-SVID not active yet"); + } + if (parsed.payload.iss !== SPIRE_ISSUER) { + throw new Error(`unexpected JWT-SVID issuer ${parsed.payload.iss}`); + } + if (!hasAudience(parsed.payload, JWT_SVID_AUDIENCE)) { + throw new Error(`JWT-SVID audience did not include ${JWT_SVID_AUDIENCE}`); + } + if (!String(parsed.payload.sub || "").startsWith(subjectPrefix)) { + throw new Error(`JWT-SVID subject did not start with ${subjectPrefix}`); + } + return parsed.payload; +} + +function signAccessToken(payload) { + const header = b64urlEncode(JSON.stringify({ alg: "HS256", typ: "JWT" })); + const body = b64urlEncode(JSON.stringify(payload)); + const signingInput = `${header}.${body}`; + const signature = crypto + .createHmac("sha256", ACCESS_TOKEN_SECRET) + .update(signingInput) + .digest(); + return `${signingInput}.${b64urlEncode(signature)}`; +} + +function verifyAccessToken(jwt, tokenUse) { + const parsed = parseJwt(jwt); + const expected = b64urlEncode( + crypto.createHmac("sha256", ACCESS_TOKEN_SECRET).update(parsed.signingInput).digest(), + ); + if ( + parsed.signatureB64.length !== expected.length || + !crypto.timingSafeEqual(Buffer.from(parsed.signatureB64), Buffer.from(expected)) + ) { + throw new Error("token signature validation failed"); + } + + const now = Math.floor(Date.now() / 1000); + if (parsed.payload.exp && parsed.payload.exp <= now) { + throw new Error("token expired"); + } + if (parsed.payload.iss !== ACCESS_TOKEN_ISSUER) { + throw new Error(`unexpected token issuer ${parsed.payload.iss}`); + } + if (parsed.payload.demo_token_use !== tokenUse) { + throw new Error(`expected ${tokenUse} token`); + } + return parsed.payload; +} + +function issueDemoSubjectToken() { + const now = Math.floor(Date.now() / 1000); + return signAccessToken({ + iss: ACCESS_TOKEN_ISSUER, + sub: DEMO_USER_SUBJECT, + aud: ["openshell-gateway", "account"], + scope: "openid profile email", + demo_token_use: "user_subject", + iat: now, + exp: now + 1800, + }); +} + +function json(res, status, body) { + const payload = JSON.stringify(body); + res.writeHead(status, { + "content-type": "application/json", + "content-length": Buffer.byteLength(payload), + }); + res.end(payload); +} + +async function bodyText(req) { + const chunks = []; + for await (const chunk of req) { + chunks.push(chunk); + if (Buffer.concat(chunks).length > 1024 * 1024) { + throw new Error("request body too large"); + } + } + return Buffer.concat(chunks).toString("utf8"); +} + +async function handleTokenExchange(req, res) { + const params = new URLSearchParams(await bodyText(req)); + if (params.get("grant_type") !== TOKEN_EXCHANGE_GRANT_TYPE) { + return json(res, 400, { error: "unsupported_grant_type" }); + } + if (params.get("client_assertion_type") !== JWT_SPIFFE_ASSERTION_TYPE) { + return json(res, 400, { error: "unsupported_client_assertion_type" }); + } + + const jwtSvid = params.get("client_assertion"); + if (!jwtSvid) { + return json(res, 400, { error: "missing_client_assertion" }); + } + const subjectToken = params.get("subject_token"); + if (!subjectToken) { + return json(res, 400, { error: "missing_subject_token" }); + } + + const audience = params.get("audience") || ""; + const requestedScopes = (params.get("scope") || "").split(/\s+/).filter(Boolean); + const now = Math.floor(Date.now() / 1000); + + const userToken = (() => { + try { + return verifyAccessToken(subjectToken, "user_subject"); + } catch (_error) { + return null; + } + })(); + + if (userToken) { + const gatewaySvid = await verifyJwtSvid(jwtSvid, GATEWAY_TRUST_DOMAIN_PREFIX); + if (!audience.startsWith(SUPERVISOR_TRUST_DOMAIN_PREFIX)) { + return json(res, 400, { error: "unsupported_intermediate_audience", audience }); + } + const intermediateToken = signAccessToken({ + iss: ACCESS_TOKEN_ISSUER, + sub: userToken.sub, + aud: [audience], + scope: userToken.scope || "openid profile email", + azp: gatewaySvid.sub, + client_id: gatewaySvid.sub, + demo_token_use: "intermediate", + iat: now, + exp: now + 300, + }); + console.log(`issued intermediate token for user=${userToken.sub} audience=${audience}`); + return json(res, 200, { + access_token: intermediateToken, + token_type: "Bearer", + expires_in: 300, + }); + } + + const supervisorSvid = await verifyJwtSvid(jwtSvid, SUPERVISOR_TRUST_DOMAIN_PREFIX); + const intermediateToken = verifyAccessToken(subjectToken, "intermediate"); + if (!hasAudience(intermediateToken, supervisorSvid.sub)) { + return json(res, 403, { error: "intermediate_token_audience_mismatch" }); + } + if (!["alpha", "beta"].includes(audience)) { + return json(res, 400, { error: "unsupported_audience", audience }); + } + if (!requestedScopes.includes(audience)) { + return json(res, 403, { error: "missing_matching_scope" }); + } + + const accessToken = signAccessToken({ + iss: ACCESS_TOKEN_ISSUER, + sub: intermediateToken.sub, + aud: [audience, "account"], + scope: `${requestedScopes.join(" ")} profile email`, + azp: supervisorSvid.sub, + client_id: supervisorSvid.sub, + demo_token_use: "final", + iat: now, + exp: now + 300, + }); + + console.log( + `issued final token for user=${intermediateToken.sub} audience=${audience} client=${supervisorSvid.sub}`, + ); + return json(res, 200, { + access_token: accessToken, + token_type: "Bearer", + expires_in: 300, + scope: `${requestedScopes.join(" ")} profile email`, + }); +} + +http + .createServer(async (req, res) => { + try { + if (req.url === "/healthz") { + res.writeHead(200, { "content-type": "text/plain" }); + return res.end("ok\n"); + } + if (req.method === "GET" && req.url === "/demo-subject-token") { + return json(res, 200, { + access_token: issueDemoSubjectToken(), + token_type: "Bearer", + expires_in: 1800, + }); + } + if (req.method === "POST" && req.url === "/token") { + return await handleTokenExchange(req, res); + } + return json(res, 404, { error: "not_found" }); + } catch (error) { + console.error(error); + return json(res, 500, { error: "server_error", message: error.message }); + } + }) + .listen(PORT, "0.0.0.0", () => { + console.log(`token exchange issuer listening on ${PORT}`); + }); diff --git a/examples/spiffe-token-exchange-demo/k8s/workloads.yaml b/examples/spiffe-token-exchange-demo/k8s/workloads.yaml new file mode 100644 index 0000000000..2bb1639214 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/k8s/workloads.yaml @@ -0,0 +1,230 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: token-exchange-issuer + namespace: default + labels: + app: token-exchange-issuer +spec: + replicas: 1 + selector: + matchLabels: + app: token-exchange-issuer + template: + metadata: + labels: + app: token-exchange-issuer + spec: + containers: + - name: token-exchange-issuer + image: node:22-alpine + imagePullPolicy: IfNotPresent + command: ["node", "/demo/token-issuer.js"] + ports: + - name: http + containerPort: 8080 + env: + - name: ACCESS_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: openshell-spiffe-token-exchange-demo + key: access-token-secret + - name: ACCESS_TOKEN_ISSUER + value: http://token-exchange-issuer.default.svc.cluster.local + - name: SPIRE_JWKS_URI + value: https://spire-spiffe-oidc-discovery-provider.spire.svc.cluster.local/keys + - name: SPIRE_JWKS_CA_FILE + value: /etc/x509/spiffe-bundle/bundle.pem + - name: SPIRE_ISSUER + value: https://spire-spiffe-oidc-discovery-provider.spire.svc.cluster.local + - name: JWT_SVID_AUDIENCE + value: http://token-exchange-issuer.default.svc.cluster.local + - name: SUPERVISOR_TRUST_DOMAIN_PREFIX + value: spiffe://openshell.local/openshell/sandbox/ + - name: GATEWAY_TRUST_DOMAIN_PREFIX + value: spiffe://openshell.local/ns/openshell/sa/ + - name: DEMO_USER_SUBJECT + value: demo-user + readinessProbe: + httpGet: + path: /healthz + port: http + volumeMounts: + - name: scripts + mountPath: /demo + readOnly: true + - name: spiffe-bundle + mountPath: /etc/x509/spiffe-bundle + readOnly: true + - name: spiffe-helper + image: ghcr.io/spiffe/spiffe-helper:0.11.0 + imagePullPolicy: IfNotPresent + args: ["-config", "/etc/spiffe-helper/spiffe-helper.conf"] + volumeMounts: + - name: spiffe-socket + mountPath: /run/spire/sockets + readOnly: true + - name: spiffe-bundle + mountPath: /target + - name: helper-config + mountPath: /etc/spiffe-helper + readOnly: true + volumes: + - name: scripts + configMap: + name: openshell-spiffe-token-exchange-demo-scripts + - name: helper-config + configMap: + name: openshell-spiffe-token-exchange-demo-helper + - name: spiffe-socket + csi: + driver: csi.spiffe.io + readOnly: true + - name: spiffe-bundle + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: token-exchange-issuer + namespace: default +spec: + selector: + app: token-exchange-issuer + ports: + - name: http + port: 80 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: alpha-exchange + namespace: default + labels: + app: alpha-exchange +spec: + replicas: 1 + selector: + matchLabels: + app: alpha-exchange + template: + metadata: + labels: + app: alpha-exchange + spec: + containers: + - name: alpha-exchange + image: node:22-alpine + imagePullPolicy: IfNotPresent + command: ["node", "/demo/protected-service.js"] + ports: + - name: http + containerPort: 8080 + env: + - name: SERVICE_NAME + value: alpha + - name: EXPECTED_AUDIENCE + value: alpha + - name: EXPECTED_SCOPE + value: alpha + - name: ACCESS_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: openshell-spiffe-token-exchange-demo + key: access-token-secret + - name: ACCESS_TOKEN_ISSUER + value: http://token-exchange-issuer.default.svc.cluster.local + readinessProbe: + httpGet: + path: /healthz + port: http + volumeMounts: + - name: scripts + mountPath: /demo + readOnly: true + volumes: + - name: scripts + configMap: + name: openshell-spiffe-token-exchange-demo-scripts +--- +apiVersion: v1 +kind: Service +metadata: + name: alpha-exchange + namespace: default +spec: + selector: + app: alpha-exchange + ports: + - name: http + port: 80 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: beta-exchange + namespace: default + labels: + app: beta-exchange +spec: + replicas: 1 + selector: + matchLabels: + app: beta-exchange + template: + metadata: + labels: + app: beta-exchange + spec: + containers: + - name: beta-exchange + image: node:22-alpine + imagePullPolicy: IfNotPresent + command: ["node", "/demo/protected-service.js"] + ports: + - name: http + containerPort: 8080 + env: + - name: SERVICE_NAME + value: beta + - name: EXPECTED_AUDIENCE + value: beta + - name: EXPECTED_SCOPE + value: beta + - name: ACCESS_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: openshell-spiffe-token-exchange-demo + key: access-token-secret + - name: ACCESS_TOKEN_ISSUER + value: http://token-exchange-issuer.default.svc.cluster.local + readinessProbe: + httpGet: + path: /healthz + port: http + volumeMounts: + - name: scripts + mountPath: /demo + readOnly: true + volumes: + - name: scripts + configMap: + name: openshell-spiffe-token-exchange-demo-scripts +--- +apiVersion: v1 +kind: Service +metadata: + name: beta-exchange + namespace: default +spec: + selector: + app: beta-exchange + ports: + - name: http + port: 80 + targetPort: http diff --git a/examples/spiffe-token-exchange-demo/podman/README.md b/examples/spiffe-token-exchange-demo/podman/README.md new file mode 100644 index 0000000000..d8fdde4b4b --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/README.md @@ -0,0 +1,316 @@ +# Podman SPIFFE Token Exchange Demo + +This variant runs the SPIFFE token exchange demo with local Podman containers +instead of Kubernetes workloads. + +The first version is intentionally single-sandbox. The script creates one +concrete SPIRE registration entry for the sandbox after OpenShell creates it. +It does not rely on SPIRE templating one entry into many per-sandbox SPIFFE IDs. + +## What Runs + +`demo.sh` starts these local Podman containers on the `openshell` network by +default: + +| Container | Purpose | +|---|---| +| `openshell-spiffe-demo-spire-server` | Local SPIRE server | +| `openshell-spiffe-demo-spire-agent` | Local SPIRE agent with a Workload API socket | +| `openshell-spiffe-demo-spire-oidc` | SPIRE OIDC discovery provider for JWKS | +| `openshell-spiffe-demo-token-issuer` | Dummy IdP/token exchange endpoint | +| `openshell-spiffe-demo-alpha` | Protected alpha service | +| `openshell-spiffe-demo-beta` | Protected beta service | + +The OpenShell gateway is not started by this script. Start it separately with +the Podman driver and SPIFFE provider token grant settings. + +For the most self-contained path, set `START_GATEWAY=1`. The script starts a +gateway container on the same Podman network as the token issuer and protected +services. That makes Podman's DNS alias +`token-exchange-issuer.default.svc.cluster.local` resolve inside the gateway +without host DNS changes. + +## Gateway Requirements + +Start the gateway with: + +```shell +export OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET=/path/to/demo/spire-agent.sock +``` + +Configure the Podman driver with the same socket path: + +```toml +[openshell.drivers.podman] +network_name = "openshell" +provider_spiffe_workload_api_socket = "/path/to/demo/spire-agent.sock" +``` + +When `demo.sh` starts SPIRE itself, it prints the exact socket path to use. +Restart the gateway with that path before the final alpha/beta calls if the +gateway was not already configured. + +The provider profile uses +`http://token-exchange-issuer.default.svc.cluster.local:8080/token` by default. +The token issuer container has that Podman network alias, so sandboxes on the +same Podman network can resolve it. The gateway also performs the intermediate +token exchange, so a host-running gateway must be able to resolve and reach the +same name. For local testing, add a hosts/DNS entry that maps +`token-exchange-issuer.default.svc.cluster.local` to the host address serving +the published `TOKEN_ISSUER_PORT`, or run the gateway in an environment attached +to the same Podman network. + +`START_GATEWAY=1` automates that same-network gateway setup. It mounts the host +Podman socket into the gateway container, writes a temporary gateway config with +`compute_drivers = ["podman"]`, and mounts the SPIRE Workload API socket at the +same absolute host path so the gateway can pass that path to sibling sandbox +containers. + +The script looks for a Podman API socket in the usual rootless and rootful +locations, plus `podman system connection list`. If none exists, it starts a +temporary rootless API service under the script's temporary directory and stops +it during cleanup. The self-started socket is mounted into demo containers with +Podman relabeling. Set `PODMAN_SOCKET=/path/to/podman.sock` or +`PODMAN_SOCKET=unix:///path/to/podman.sock` to use a specific existing socket. +The SPIRE agent and managed gateway containers run with +`--security-opt label=disable` because both must connect to the rootless Podman +API Unix socket. + +SPIRE runs as a non-root user in its upstream images. The script creates +throwaway state and socket directories under its temporary directory with broad +write permissions so rootless Podman UID mappings can create the SQLite +datastore and Workload API sockets. + +## Run + +From anywhere: + +```shell +export OPENSHELL_REPO=/path/to/OpenShell +export GATEWAY_NAME=local +export GATEWAY_ENDPOINT=http://127.0.0.1:8080 + +bash "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/podman/demo.sh" +``` + +Self-contained local path: + +```shell +START_GATEWAY=1 \ +bash "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/podman/demo.sh" +``` + +Common overrides: + +```shell +SANDBOX_NAME=spiffe-podman-demo +PODMAN_NETWORK=openshell +TOKEN_ISSUER_PORT=18080 +MANAGED_GATEWAY_PORT=18082 +MANAGED_GATEWAY_HEALTH_PORT=18083 +GATEWAY_IMAGE=ghcr.io/nvidia/openshell/gateway:latest +SANDBOX_IMAGE=ghcr.io/nvidia/openshell/sandbox:latest +SUPERVISOR_IMAGE=ghcr.io/nvidia/openshell/supervisor:latest +SANDBOX_IMAGE_PULL_POLICY=missing +PODMAN_STOP_TIMEOUT_SECS=3 +KEEP_DEMO=1 +KEEP_SANDBOX=1 +``` + +Keep `SANDBOX_NAME` at 19 characters or fewer for the Podman driver. The +default `spiffe-podman-demo` is 18 characters. + +`PODMAN_STOP_TIMEOUT_SECS` controls how long the managed gateway asks Podman to +wait for sandbox containers to stop before Podman force-kills them during +cleanup. The short demo default avoids waiting on long SIGTERM grace periods. + +When testing branch-local images with `START_GATEWAY=1`, override all three +OpenShell runtime images: + +```shell +START_GATEWAY=1 \ +GATEWAY_IMAGE=localhost/openshell/gateway:branch \ +SANDBOX_IMAGE=localhost/openshell/sandbox:branch \ +SUPERVISOR_IMAGE=localhost/openshell/supervisor:branch \ +SANDBOX_IMAGE_PULL_POLICY=never \ +bash "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/podman/demo.sh" +``` + +`SANDBOX_IMAGE` renders to `[openshell.drivers.podman].default_image`. +`SUPERVISOR_IMAGE` renders to `[openshell.drivers.podman].supervisor_image`. + +Use `START_SPIRE=0` only when you already have SPIRE running and can provide a +host path to a Workload API socket: + +```shell +START_SPIRE=0 \ +SPIRE_AGENT_SOCKET_HOST_PATH=/run/spire/agent.sock \ +bash "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/podman/demo.sh" +``` + +## SPIRE Startup Scripts + +The full demo uses these scripts internally, and you can also run them directly +when you want to manage SPIRE separately from the token exchange flow: + +```shell +SPIRE_STATE_DIR="$(mktemp -d)" \ +SPIRE_ENV_FILE=/tmp/openshell-spire-server.env \ +bash "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/podman/spire/start-server-oidc.sh" + +source /tmp/openshell-spire-server.env + +SPIRE_STATE_DIR="$(mktemp -d)" \ +SPIRE_ENV_FILE=/tmp/openshell-spire-agent.env \ +bash "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/podman/spire/start-agent.sh" + +source /tmp/openshell-spire-agent.env +printf "Workload API socket: %s\n" "$SPIRE_AGENT_SOCKET_HOST_PATH" +``` + +`start-server-oidc.sh` starts the SPIRE server and OIDC discovery provider. +`start-agent.sh` generates a join token from the server container and starts a +SPIRE agent with the Docker-compatible Podman workload attestor. Both scripts +honor the same image, container, network, trust-domain, and SPIRE parent ID +environment variables as `demo.sh`. + +## Gateway Startup Script + +After starting the SPIRE agent, start a Podman-backed OpenShell gateway with: + +```shell +SPIRE_AGENT_ENV_FILE=/tmp/openshell-spire-agent.env \ +GATEWAY_ENV_FILE=/tmp/openshell-spiffe-gateway.env \ +bash "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/podman/start-gateway.sh" +``` + +The gateway listens on `http://127.0.0.1:8888` by default, with health checks on +`http://127.0.0.1:8889`. Override those ports with `GATEWAY_PORT` and +`GATEWAY_HEALTH_PORT`. + +`start-gateway.sh` mounts the SPIRE agent Workload API socket into the gateway +container and writes a temporary gateway config with the Podman driver enabled. +Register the gateway SPIFFE entry as a separate step: + +```shell +GATEWAY_SELECTORS="docker:label:openshell.spiffe-demo:gateway" \ +bash "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/podman/spire/register-gateway.sh" +``` + +The script also honors `GATEWAY_IMAGE`, `SANDBOX_IMAGE`, `SUPERVISOR_IMAGE`, +`SANDBOX_IMAGE_PULL_POLICY`, `PODMAN_NETWORK`, `PODMAN_SOCKET`, and +`PODMAN_STOP_TIMEOUT_SECS`. + +To require OIDC login for user-facing gateway calls, set `GATEWAY_OIDC_ISSUER`. +The script then renders `[openshell.gateway.oidc]` and defaults +`allow_unauthenticated_users` to `false`: + +```shell +SPIRE_AGENT_ENV_FILE=/tmp/openshell-spire-agent.env \ +GATEWAY_OIDC_ISSUER=https://idp.example.com/realms/openshell \ +GATEWAY_OIDC_AUDIENCE=openshell-cli \ +GATEWAY_OIDC_CLIENT_ID=openshell-cli \ +GATEWAY_OIDC_LOGIN_SCOPES="openid profile email" \ +bash "$OPENSHELL_REPO/examples/spiffe-token-exchange-demo/podman/start-gateway.sh" +``` + +The script prints the matching `openshell gateway add ... --oidc-issuer ...` +command after the gateway is ready. You can also run it directly: + +```shell +openshell gateway add http://127.0.0.1:8888 \ + --name podman-spiffe-demo \ + --oidc-issuer https://idp.example.com/realms/openshell \ + --oidc-client-id openshell-cli \ + --oidc-audience openshell-cli \ + --oidc-scopes "openid profile email" +``` + +OIDC-related overrides: + +- `GATEWAY_OIDC_ISSUER` +- `GATEWAY_OIDC_AUDIENCE`, default `openshell-cli` +- `GATEWAY_OIDC_JWKS_TTL_SECS`, default `3600` +- `GATEWAY_OIDC_ROLES_CLAIM`, default `realm_access.roles` +- `GATEWAY_OIDC_ADMIN_ROLE`, default `openshell-admin` +- `GATEWAY_OIDC_USER_ROLE`, default `openshell-user` +- `GATEWAY_OIDC_SCOPES_CLAIM`, default empty +- `GATEWAY_OIDC_CLIENT_ID`, default `openshell-cli` +- `GATEWAY_OIDC_LOGIN_SCOPES`, default empty +- `GATEWAY_ALLOW_UNAUTHENTICATED_USERS`, default `false` when OIDC is enabled + and `true` otherwise + +## SPIRE Registration + +The helpers are: + +```shell +examples/spiffe-token-exchange-demo/podman/spire/register-gateway.sh +examples/spiffe-token-exchange-demo/podman/spire/register-sandbox.sh +``` + +Defaults: + +- SPIRE agent parent ID: + `spiffe://openshell.local/openshell/spire-agent/demo` +- Gateway SPIFFE ID: + `spiffe://openshell.local/openshell/gateway/demo` +- Gateway selectors: + `unix:uid:` plus `unix:path:` when + `openshell-server` is on `PATH` +- Managed gateway selectors: + `docker:label:openshell.spiffe-demo:gateway` +- Sandbox SPIFFE ID: + `spiffe://openshell.local/openshell/sandbox/` +- Sandbox selectors: + `docker:label:openshell.managed:true` and + `docker:label:openshell.ai/sandbox-id:` + +If your gateway binary is not named `openshell-server` or is not on `PATH`, set +`GATEWAY_WORKLOAD_PATH` or provide `GATEWAY_SELECTORS`: + +```shell +GATEWAY_WORKLOAD_PATH=/path/to/openshell-server \ + examples/spiffe-token-exchange-demo/podman/spire/register-gateway.sh +``` + +If your SPIRE setup cannot use Docker-style selectors against Podman, provide +explicit selectors: + +```shell +SANDBOX_SELECTORS="selector:a selector:b" \ + examples/spiffe-token-exchange-demo/podman/spire/register-sandbox.sh "$SANDBOX_ID" +``` + +## Expected Output + +The alpha and beta calls should include the demo user and the sandbox SPIFFE ID: + +```text +alpha called with path /: + sub: demo-user + aud: alpha, account + scope: alpha profile email + azp: spiffe://openshell.local/openshell/sandbox/ + client_id: spiffe://openshell.local/openshell/sandbox/ +``` + +## Cleanup + +The script deletes demo containers and the sandbox on exit unless you set +`KEEP_DEMO=1` or `KEEP_SANDBOX=1`. + +Manual cleanup: + +```shell +openshell --gateway "$GATEWAY_NAME" --gateway-endpoint "$GATEWAY_ENDPOINT" \ + sandbox delete spiffe-podman-demo + +podman rm -f \ + openshell-spiffe-demo-token-issuer \ + openshell-spiffe-demo-alpha \ + openshell-spiffe-demo-beta \ + openshell-spiffe-demo-spire-oidc \ + openshell-spiffe-demo-spire-agent \ + openshell-spiffe-demo-spire-server +``` diff --git a/examples/spiffe-token-exchange-demo/podman/demo.sh b/examples/spiffe-token-exchange-demo/podman/demo.sh new file mode 100755 index 0000000000..0c943c15f6 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/demo.sh @@ -0,0 +1,675 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEMO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +PROFILE_TEMPLATE="${SCRIPT_DIR}/provider-profile.yaml" +TOKEN_ISSUER_JS="${DEMO_ROOT}/k8s/token-issuer.js" +PROTECTED_SERVICE_JS="${DEMO_ROOT}/k8s/protected-service.js" + +SANDBOX_NAME="${SANDBOX_NAME:-spiffe-podman-demo}" +PROVIDER_NAME="${PROVIDER_NAME:-spiffe-token-exchange-demo-podman}" +PROFILE_ID="${PROFILE_ID:-spiffe-token-exchange-demo-podman}" +START_GATEWAY="${START_GATEWAY:-0}" +MANAGED_GATEWAY_PORT="${MANAGED_GATEWAY_PORT:-18082}" +MANAGED_GATEWAY_HEALTH_PORT="${MANAGED_GATEWAY_HEALTH_PORT:-18083}" +if [[ -z "${GATEWAY_ENDPOINT:-}" ]]; then + if [[ "$START_GATEWAY" == "1" ]]; then + GATEWAY_ENDPOINT="http://127.0.0.1:${MANAGED_GATEWAY_PORT}" + else + GATEWAY_ENDPOINT="http://127.0.0.1:8080" + fi +fi +PODMAN_NETWORK="${PODMAN_NETWORK:-openshell}" +TOKEN_ISSUER_PORT="${TOKEN_ISSUER_PORT:-18080}" +OIDC_PORT="${OIDC_PORT:-18081}" +TOKEN_ISSUER_SERVICE_HOST="${TOKEN_ISSUER_SERVICE_HOST:-token-exchange-issuer.default.svc.cluster.local}" +KEEP_SANDBOX="${KEEP_SANDBOX:-0}" +KEEP_DEMO="${KEEP_DEMO:-0}" +START_SPIRE="${START_SPIRE:-1}" +ACCESS_TOKEN_SECRET="${ACCESS_TOKEN_SECRET:-$(openssl rand -hex 32)}" +TRUST_DOMAIN="${TRUST_DOMAIN:-openshell.local}" +SPIRE_AGENT_PARENT_ID="${SPIRE_AGENT_PARENT_ID:-spiffe://${TRUST_DOMAIN}/openshell/spire-agent/demo}" +SPIRE_AGENT_SOCKET_HOST_PATH="${SPIRE_AGENT_SOCKET_HOST_PATH:-}" +SPIRE_SERVER_IMAGE="${SPIRE_SERVER_IMAGE:-ghcr.io/spiffe/spire-server:1.12.4}" +SPIRE_AGENT_IMAGE="${SPIRE_AGENT_IMAGE:-ghcr.io/spiffe/spire-agent:1.12.4}" +SPIRE_OIDC_IMAGE="${SPIRE_OIDC_IMAGE:-ghcr.io/spiffe/oidc-discovery-provider:1.12.4}" +NODE_IMAGE="${NODE_IMAGE:-node:22-alpine}" +SPIRE_SERVER_CONTAINER="${SPIRE_SERVER_CONTAINER:-openshell-spiffe-demo-spire-server}" +SPIRE_AGENT_CONTAINER="${SPIRE_AGENT_CONTAINER:-openshell-spiffe-demo-spire-agent}" +SPIRE_OIDC_CONTAINER="${SPIRE_OIDC_CONTAINER:-openshell-spiffe-demo-spire-oidc}" +GATEWAY_CONTAINER="${GATEWAY_CONTAINER:-openshell-spiffe-demo-gateway}" +GATEWAY_IMAGE="${GATEWAY_IMAGE:-ghcr.io/nvidia/openshell/gateway:latest}" +SANDBOX_IMAGE="${SANDBOX_IMAGE:-}" +SUPERVISOR_IMAGE="${SUPERVISOR_IMAGE:-}" +SANDBOX_IMAGE_PULL_POLICY="${SANDBOX_IMAGE_PULL_POLICY:-missing}" +PODMAN_STOP_TIMEOUT_SECS="${PODMAN_STOP_TIMEOUT_SECS:-3}" +TOKEN_ISSUER_CONTAINER="${TOKEN_ISSUER_CONTAINER:-openshell-spiffe-demo-token-issuer}" +ALPHA_CONTAINER="${ALPHA_CONTAINER:-openshell-spiffe-demo-alpha}" +BETA_CONTAINER="${BETA_CONTAINER:-openshell-spiffe-demo-beta}" + +TMP_DIR="$(mktemp -d)" +RENDERED_PROFILE="${TMP_DIR}/provider-profile.yaml" +MOUNT_DIR="${TMP_DIR}/mounts" +TOKEN_ISSUER_JS_MOUNT="${MOUNT_DIR}/token-issuer.js" +PROTECTED_SERVICE_JS_MOUNT="${MOUNT_DIR}/protected-service.js" +PODMAN_SERVICE_PID="" +PODMAN_SOCKET_DEMO_OWNED="0" + +default_gateway_name() { + if [[ -n "${GATEWAY_NAME:-}" ]]; then + printf "%s\n" "$GATEWAY_NAME" + return + fi + if [[ -n "${OPENSHELL_GATEWAY:-}" ]]; then + printf "%s\n" "$OPENSHELL_GATEWAY" + return + fi + if [[ "$START_GATEWAY" == "1" ]]; then + printf "podman-spiffe-demo\n" + return + fi + + local config_home="${XDG_CONFIG_HOME:-$HOME/.config}" + if [[ -s "${config_home}/openshell/active_gateway" ]]; then + head -n1 "${config_home}/openshell/active_gateway" + return + fi + if [[ -s /etc/openshell/active_gateway ]]; then + head -n1 /etc/openshell/active_gateway + return + fi + + printf "local\n" +} + +GATEWAY_NAME="$(default_gateway_name)" +OS=(openshell --gateway "$GATEWAY_NAME" --gateway-endpoint "$GATEWAY_ENDPOINT") + +run() { + printf "\n$ %s\n" "$*" + "$@" +} + +prepare_demo_mounts() { + mkdir -p "$MOUNT_DIR" + cp "$TOKEN_ISSUER_JS" "$TOKEN_ISSUER_JS_MOUNT" + cp "$PROTECTED_SERVICE_JS" "$PROTECTED_SERVICE_JS_MOUNT" + chmod 0644 \ + "$TOKEN_ISSUER_JS_MOUNT" \ + "$PROTECTED_SERVICE_JS_MOUNT" +} + +require_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + printf "missing required command: %s\n" "$cmd" >&2 + exit 1 + fi +} + +wait_for_port() { + local port="$1" + local label="$2" + for _ in $(seq 1 80); do + if nc -z 127.0.0.1 "$port" >/dev/null 2>&1; then + return 0 + fi + sleep 0.25 + done + printf "%s did not become reachable on 127.0.0.1:%s\n" "$label" "$port" >&2 + exit 1 +} + +subject_token_from_json() { + python3 -c 'import json, sys; print(json.load(sys.stdin)["access_token"])' +} + +assert_contains() { + local haystack="$1" + local needle="$2" + if [[ "$haystack" != *"$needle"* ]]; then + printf "expected output to contain: %s\n" "$needle" >&2 + printf "actual output:\n%s\n" "$haystack" >&2 + exit 1 + fi +} + +require_uint() { + local name="$1" + local value="$2" + if [[ ! "$value" =~ ^[0-9]+$ ]]; then + printf "%s must be a non-negative integer, got: %s\n" "$name" "$value" >&2 + exit 1 + fi +} + +toml_string_escape() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + printf "%s\n" "$value" +} + +normalize_podman_socket_path() { + local socket_path="$1" + if [[ "$socket_path" == unix://* ]]; then + socket_path="${socket_path#unix://}" + fi + printf "%s\n" "$socket_path" +} + +detect_podman_socket() { + if [[ -n "${PODMAN_SOCKET:-}" ]]; then + normalize_podman_socket_path "$PODMAN_SOCKET" + return + fi + if [[ -n "${XDG_RUNTIME_DIR:-}" && -S "${XDG_RUNTIME_DIR}/podman/podman.sock" ]]; then + printf "%s\n" "${XDG_RUNTIME_DIR}/podman/podman.sock" + return + fi + if [[ -S "/run/user/$(id -u)/podman/podman.sock" ]]; then + printf "%s\n" "/run/user/$(id -u)/podman/podman.sock" + return + fi + if [[ -S /run/podman/podman.sock ]]; then + printf "%s\n" /run/podman/podman.sock + return + fi + if [[ -S /var/run/docker.sock ]]; then + printf "%s\n" /var/run/docker.sock + return + fi + local connection_socket + connection_socket="$( + podman system connection list --format '{{.URI}}' 2>/dev/null | + sed -n 's|^unix://||p' | + while IFS= read -r candidate; do + if [[ -S "$candidate" ]]; then + printf "%s\n" "$candidate" + break + fi + done + )" + if [[ -n "$connection_socket" ]]; then + printf "%s\n" "$connection_socket" + return + fi + return 1 +} + +start_podman_service() { + local socket_path="$1" + + mkdir -p "$(dirname "$socket_path")" + printf "\n$ podman system service --time=0 unix://%s\n" "$socket_path" >&2 + podman system service --time=0 "unix://${socket_path}" & + PODMAN_SERVICE_PID="$!" + + for _ in $(seq 1 80); do + if [[ -S "$socket_path" ]]; then + chmod 0666 "$socket_path" || true + return + fi + if ! kill -0 "$PODMAN_SERVICE_PID" >/dev/null 2>&1; then + wait "$PODMAN_SERVICE_PID" || true + PODMAN_SERVICE_PID="" + break + fi + sleep 0.25 + done + + printf "Podman API service did not create %s; set PODMAN_SOCKET to a running API socket\n" "$socket_path" >&2 + exit 1 +} + +ensure_podman_socket() { + local detected_socket + if detected_socket="$(detect_podman_socket)"; then + PODMAN_SOCKET="$detected_socket" + PODMAN_SOCKET_DEMO_OWNED="0" + return + fi + + local socket_dir="${TMP_DIR}/podman" + local socket_path="${socket_dir}/podman.sock" + + printf "\nNo Podman API socket found; starting a temporary rootless Podman API service.\n" >&2 + start_podman_service "$socket_path" + PODMAN_SOCKET="$socket_path" + PODMAN_SOCKET_DEMO_OWNED="1" +} + +podman_socket_volume() { + local source="$1" + local target="$2" + if [[ "$PODMAN_SOCKET_DEMO_OWNED" == "1" ]]; then + printf "%s:%s:z\n" "$source" "$target" + else + printf "%s:%s\n" "$source" "$target" + fi +} + +cleanup_container() { + podman rm -f "$1" >/dev/null 2>&1 || true +} + +dump_diagnostics() { + set +e + + printf "\n=== diagnostics: openshell sandbox logs ===\n" >&2 + "${OS[@]}" logs "$SANDBOX_NAME" -n 120 --source sandbox >&2 + + printf "\n=== diagnostics: podman containers ===\n" >&2 + podman ps -a --filter "name=openshell-spiffe-demo" >&2 + + for container in \ + "$SPIRE_SERVER_CONTAINER" \ + "$SPIRE_AGENT_CONTAINER" \ + "$SPIRE_OIDC_CONTAINER" \ + "$GATEWAY_CONTAINER" \ + "$TOKEN_ISSUER_CONTAINER" \ + "$ALPHA_CONTAINER" \ + "$BETA_CONTAINER"; do + printf "\n=== diagnostics: %s logs ===\n" "$container" >&2 + podman logs "$container" >&2 + done + + printf "\n=== diagnostics: sandbox container labels ===\n" >&2 + podman ps -a \ + --filter "label=openshell.ai/sandbox-name=${SANDBOX_NAME}" \ + --format '{{.ID}} {{.Names}} {{.Labels}}' >&2 +} + +cleanup() { + if [[ "$KEEP_SANDBOX" != "1" ]]; then + "${OS[@]}" sandbox delete "$SANDBOX_NAME" >/dev/null 2>&1 || true + fi + + if [[ "$KEEP_DEMO" != "1" ]]; then + cleanup_container "$TOKEN_ISSUER_CONTAINER" + cleanup_container "$ALPHA_CONTAINER" + cleanup_container "$BETA_CONTAINER" + if [[ "$START_GATEWAY" == "1" ]]; then + cleanup_container "$GATEWAY_CONTAINER" + fi + if [[ "$START_SPIRE" == "1" ]]; then + cleanup_container "$SPIRE_OIDC_CONTAINER" + cleanup_container "$SPIRE_AGENT_CONTAINER" + cleanup_container "$SPIRE_SERVER_CONTAINER" + fi + if [[ -n "$PODMAN_SERVICE_PID" ]]; then + kill "$PODMAN_SERVICE_PID" >/dev/null 2>&1 || true + wait "$PODMAN_SERVICE_PID" >/dev/null 2>&1 || true + PODMAN_SERVICE_PID="" + fi + rm -rf "$TMP_DIR" + else + printf "\nKeeping demo resources. Temporary files: %s\n" "$TMP_DIR" >&2 + if [[ -n "$PODMAN_SERVICE_PID" ]]; then + printf "Temporary Podman API service PID: %s\n" "$PODMAN_SERVICE_PID" >&2 + fi + fi +} + +on_exit() { + local status="$1" + if [[ "$status" -ne 0 ]]; then + dump_diagnostics || true + fi + cleanup + exit "$status" +} +trap 'on_exit $?' EXIT + +ensure_network() { + if ! podman network exists "$PODMAN_NETWORK" >/dev/null 2>&1; then + run podman network create "$PODMAN_NETWORK" + fi +} + +start_spire() { + local podman_socket="$1" + local spire_state_dir="${TMP_DIR}/spire" + local server_env="${TMP_DIR}/spire-server.env" + local agent_env="${TMP_DIR}/spire-agent.env" + + PODMAN_NETWORK="$PODMAN_NETWORK" \ + TRUST_DOMAIN="$TRUST_DOMAIN" \ + SPIRE_AGENT_PARENT_ID="$SPIRE_AGENT_PARENT_ID" \ + SPIRE_SERVER_IMAGE="$SPIRE_SERVER_IMAGE" \ + SPIRE_OIDC_IMAGE="$SPIRE_OIDC_IMAGE" \ + SPIRE_SERVER_CONTAINER="$SPIRE_SERVER_CONTAINER" \ + SPIRE_OIDC_CONTAINER="$SPIRE_OIDC_CONTAINER" \ + SPIRE_STATE_DIR="$spire_state_dir" \ + SPIRE_ENV_FILE="$server_env" \ + OIDC_PORT="$OIDC_PORT" \ + bash "${SCRIPT_DIR}/spire/start-server-oidc.sh" + + # shellcheck disable=SC1090 + source "$server_env" + + PODMAN_NETWORK="$PODMAN_NETWORK" \ + TRUST_DOMAIN="$TRUST_DOMAIN" \ + SPIRE_AGENT_PARENT_ID="$SPIRE_AGENT_PARENT_ID" \ + SPIRE_AGENT_IMAGE="$SPIRE_AGENT_IMAGE" \ + SPIRE_SERVER_CONTAINER="$SPIRE_SERVER_CONTAINER" \ + SPIRE_AGENT_CONTAINER="$SPIRE_AGENT_CONTAINER" \ + SPIRE_STATE_DIR="$SPIRE_STATE_DIR" \ + SPIRE_ENV_FILE="$agent_env" \ + PODMAN_SOCKET="$podman_socket" \ + PODMAN_SOCKET_DEMO_OWNED="$PODMAN_SOCKET_DEMO_OWNED" \ + bash "${SCRIPT_DIR}/spire/start-agent.sh" + + # shellcheck disable=SC1090 + source "$agent_env" +} + +write_managed_gateway_config() { + local podman_socket_in_container="$1" + local gateway_dir="${TMP_DIR}/gateway" + local jwt_dir="${gateway_dir}/jwt" + local config_path="${gateway_dir}/gateway.toml" + local sandbox_image_line="" + local supervisor_image_line="" + + mkdir -p "$jwt_dir" + if [[ ! -s "${jwt_dir}/signing.pem" ]]; then + openssl genpkey -algorithm ed25519 -out "${jwt_dir}/signing.pem" >/dev/null 2>&1 + openssl pkey -in "${jwt_dir}/signing.pem" -pubout -out "${jwt_dir}/public.pem" >/dev/null 2>&1 + openssl rand -hex 8 >"${jwt_dir}/kid" + fi + if [[ -n "$SANDBOX_IMAGE" ]]; then + sandbox_image_line="default_image = \"$(toml_string_escape "$SANDBOX_IMAGE")\"" + fi + if [[ -n "$SUPERVISOR_IMAGE" ]]; then + supervisor_image_line="supervisor_image = \"$(toml_string_escape "$SUPERVISOR_IMAGE")\"" + fi + + cat >"$config_path" </dev/null 2>&1 || + curl -fsS "http://127.0.0.1:${MANAGED_GATEWAY_HEALTH_PORT}/healthz" >/dev/null 2>&1; then + return + fi + sleep 0.5 + done + + printf "managed OpenShell gateway did not become ready at %s\n" "$GATEWAY_ENDPOINT" >&2 + exit 1 +} + +start_demo_services() { + cleanup_container "$TOKEN_ISSUER_CONTAINER" + cleanup_container "$ALPHA_CONTAINER" + cleanup_container "$BETA_CONTAINER" + + run podman run -d \ + --name "$TOKEN_ISSUER_CONTAINER" \ + --network "$PODMAN_NETWORK" \ + --network-alias token-exchange-issuer \ + --network-alias "$TOKEN_ISSUER_SERVICE_HOST" \ + -p "127.0.0.1:${TOKEN_ISSUER_PORT}:8080" \ + -v "${TOKEN_ISSUER_JS_MOUNT}:/demo/token-issuer.js:ro,z" \ + -e "ACCESS_TOKEN_SECRET=${ACCESS_TOKEN_SECRET}" \ + -e "ACCESS_TOKEN_ISSUER=${TOKEN_ISSUER_BASE_URL}" \ + -e "SPIRE_JWKS_URI=http://spire-oidc:8080/keys" \ + -e "SPIRE_ISSUER=http://spire-oidc:8080" \ + -e "JWT_SVID_AUDIENCE=${TOKEN_ISSUER_BASE_URL}" \ + -e "SUPERVISOR_TRUST_DOMAIN_PREFIX=spiffe://${TRUST_DOMAIN}/openshell/sandbox/" \ + -e "GATEWAY_TRUST_DOMAIN_PREFIX=spiffe://${TRUST_DOMAIN}/openshell/gateway/" \ + -e "DEMO_USER_SUBJECT=demo-user" \ + "$NODE_IMAGE" \ + node /demo/token-issuer.js + + run podman run -d \ + --name "$ALPHA_CONTAINER" \ + --network "$PODMAN_NETWORK" \ + --network-alias alpha-exchange \ + --network-alias alpha-exchange.default.svc.cluster.local \ + -v "${PROTECTED_SERVICE_JS_MOUNT}:/demo/protected-service.js:ro,z" \ + -e SERVICE_NAME=alpha \ + -e EXPECTED_AUDIENCE=alpha \ + -e EXPECTED_SCOPE=alpha \ + -e "ACCESS_TOKEN_SECRET=${ACCESS_TOKEN_SECRET}" \ + -e "ACCESS_TOKEN_ISSUER=${TOKEN_ISSUER_BASE_URL}" \ + "$NODE_IMAGE" \ + node /demo/protected-service.js + + run podman run -d \ + --name "$BETA_CONTAINER" \ + --network "$PODMAN_NETWORK" \ + --network-alias beta-exchange \ + --network-alias beta-exchange.default.svc.cluster.local \ + -v "${PROTECTED_SERVICE_JS_MOUNT}:/demo/protected-service.js:ro,z" \ + -e SERVICE_NAME=beta \ + -e EXPECTED_AUDIENCE=beta \ + -e EXPECTED_SCOPE=beta \ + -e "ACCESS_TOKEN_SECRET=${ACCESS_TOKEN_SECRET}" \ + -e "ACCESS_TOKEN_ISSUER=${TOKEN_ISSUER_BASE_URL}" \ + "$NODE_IMAGE" \ + node /demo/protected-service.js + + wait_for_port "$TOKEN_ISSUER_PORT" "token issuer" +} + +render_provider_profile() { + sed "s|__TOKEN_ISSUER_BASE_URL__|${TOKEN_ISSUER_BASE_URL}|g" \ + "$PROFILE_TEMPLATE" >"$RENDERED_PROFILE" +} + +sandbox_id() { + local output + output="$("${OS[@]}" sandbox get "$SANDBOX_NAME")" + awk '/Id:/ && !found { print $2; found=1 }' <<<"$output" +} + +register_gateway_entry() { + if [[ "${REGISTER_GATEWAY_ENTRY:-1}" != "1" ]]; then + return + fi + if [[ "$START_GATEWAY" == "1" && -z "${GATEWAY_SELECTORS:-}" ]]; then + GATEWAY_SELECTORS="docker:label:openshell.spiffe-demo:gateway" + fi + TRUST_DOMAIN="$TRUST_DOMAIN" \ + GATEWAY_SELECTORS="${GATEWAY_SELECTORS:-}" \ + SPIRE_AGENT_PARENT_ID="$SPIRE_AGENT_PARENT_ID" \ + SPIRE_SERVER_CONTAINER="$SPIRE_SERVER_CONTAINER" \ + bash "${SCRIPT_DIR}/spire/register-gateway.sh" +} + +register_sandbox_entry() { + local id="$1" + TRUST_DOMAIN="$TRUST_DOMAIN" \ + SPIRE_AGENT_PARENT_ID="$SPIRE_AGENT_PARENT_ID" \ + SPIRE_SERVER_CONTAINER="$SPIRE_SERVER_CONTAINER" \ + bash "${SCRIPT_DIR}/spire/register-sandbox.sh" "$id" +} + +sandbox_curl_until() { + local label="$1" + local url="$2" + local expected="$3" + local output="" + + for attempt in $(seq 1 18); do + printf "\n$ openshell sandbox exec %s curl (attempt %s)\n" "$label" "$attempt" + if output=$("${OS[@]}" sandbox exec --name "$SANDBOX_NAME" --no-tty -- curl -sS --max-time 10 "$url" 2>&1); then + printf "%s\n" "$output" + if [[ "$output" == *"$expected"* ]]; then + SANDBOX_CURL_OUTPUT="$output" + return 0 + fi + else + printf "%s\n" "$output" + fi + sleep 2 + done + + printf "timed out waiting for %s to return expected output\n" "$label" >&2 + printf "last output:\n%s\n" "$output" >&2 + exit 1 +} + +require_cmd podman +require_cmd openssl +require_cmd openshell +require_cmd curl +require_cmd python3 +require_cmd nc +require_cmd awk +require_cmd sed + +require_uint PODMAN_STOP_TIMEOUT_SECS "$PODMAN_STOP_TIMEOUT_SECS" +prepare_demo_mounts +ensure_network + +ensure_podman_socket +if [[ "$START_SPIRE" == "1" ]]; then + start_spire "$PODMAN_SOCKET" +elif [[ -z "$SPIRE_AGENT_SOCKET_HOST_PATH" ]]; then + printf "START_SPIRE=0 requires SPIRE_AGENT_SOCKET_HOST_PATH\n" >&2 + exit 1 +fi + +if [[ "$START_GATEWAY" == "1" ]]; then + start_gateway "$PODMAN_SOCKET" +fi + +if [[ -z "${TOKEN_ISSUER_BASE_URL:-}" ]]; then + TOKEN_ISSUER_BASE_URL="http://${TOKEN_ISSUER_SERVICE_HOST}:8080" +fi + +printf "\nUsing OpenShell gateway '%s' at %s\n" "$GATEWAY_NAME" "$GATEWAY_ENDPOINT" +printf "Using Podman network '%s'\n" "$PODMAN_NETWORK" +printf "Using token issuer base URL '%s'\n" "$TOKEN_ISSUER_BASE_URL" +printf "SPIRE agent Workload API socket for gateway and Podman driver: %s\n" "$SPIRE_AGENT_SOCKET_HOST_PATH" +if [[ "$START_GATEWAY" == "1" ]]; then + printf "Managed gateway container: %s\n" "$GATEWAY_CONTAINER" + printf "Managed gateway image: %s\n" "$GATEWAY_IMAGE" + if [[ -n "$SANDBOX_IMAGE" ]]; then + printf "Managed sandbox image: %s\n" "$SANDBOX_IMAGE" + fi + if [[ -n "$SUPERVISOR_IMAGE" ]]; then + printf "Managed supervisor image: %s\n" "$SUPERVISOR_IMAGE" + fi + printf "Managed sandbox image pull policy: %s\n\n" "$SANDBOX_IMAGE_PULL_POLICY" + printf "Managed Podman stop timeout: %s seconds\n\n" "$PODMAN_STOP_TIMEOUT_SECS" +else + printf "\nThe gateway must already be running with:\n" + printf " OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET=%s\n" "$SPIRE_AGENT_SOCKET_HOST_PATH" + printf " [openshell.drivers.podman].provider_spiffe_workload_api_socket=%s\n" "$SPIRE_AGENT_SOCKET_HOST_PATH" + printf " [openshell.drivers.podman].network_name=%s\n\n" "$PODMAN_NETWORK" + printf "The gateway process must be able to resolve and reach %s.\n" "$TOKEN_ISSUER_SERVICE_HOST" + printf "For a host-running gateway, add local DNS/hosts routing to the published issuer port %s if needed.\n\n" "$TOKEN_ISSUER_PORT" +fi + +register_gateway_entry +start_demo_services +render_provider_profile + +SUBJECT_TOKEN="$(curl -fsS "http://127.0.0.1:${TOKEN_ISSUER_PORT}/demo-subject-token" | subject_token_from_json)" + +"${OS[@]}" sandbox delete "$SANDBOX_NAME" >/dev/null 2>&1 || true +"${OS[@]}" provider delete "$PROVIDER_NAME" >/dev/null 2>&1 || true +"${OS[@]}" provider profile delete "$PROFILE_ID" >/dev/null 2>&1 || true + +run "${OS[@]}" settings set --global --key providers_v2_enabled --value true --yes +run "${OS[@]}" provider profile lint -f "$RENDERED_PROFILE" +run "${OS[@]}" provider profile import -f "$RENDERED_PROFILE" +run "${OS[@]}" provider create --name "$PROVIDER_NAME" --type "$PROFILE_ID" --credential "subject_token=${SUBJECT_TOKEN}" +run "${OS[@]}" sandbox create --name "$SANDBOX_NAME" --provider "$PROVIDER_NAME" --keep --no-tty -- echo "sandbox ready" + +SANDBOX_ID="$(sandbox_id)" +if [[ -z "$SANDBOX_ID" ]]; then + printf "could not determine sandbox ID from openshell sandbox get\n" >&2 + exit 1 +fi +printf "\nSandbox ID: %s\n" "$SANDBOX_ID" + +register_sandbox_entry "$SANDBOX_ID" + +sandbox_curl_until "alpha" "http://alpha-exchange:8080/" "alpha called with path /:" +ALPHA_OUTPUT="$SANDBOX_CURL_OUTPUT" +assert_contains "$ALPHA_OUTPUT" "sub: demo-user" +assert_contains "$ALPHA_OUTPUT" "aud: alpha, account" +assert_contains "$ALPHA_OUTPUT" "scope: alpha profile email" +assert_contains "$ALPHA_OUTPUT" "azp: spiffe://${TRUST_DOMAIN}/openshell/sandbox/" +assert_contains "$ALPHA_OUTPUT" "client_id: spiffe://${TRUST_DOMAIN}/openshell/sandbox/" + +sandbox_curl_until "beta" "http://beta-exchange:8080/" "beta called with path /:" +BETA_OUTPUT="$SANDBOX_CURL_OUTPUT" +assert_contains "$BETA_OUTPUT" "sub: demo-user" +assert_contains "$BETA_OUTPUT" "aud: beta, account" +assert_contains "$BETA_OUTPUT" "scope: beta profile email" +assert_contains "$BETA_OUTPUT" "azp: spiffe://${TRUST_DOMAIN}/openshell/sandbox/" +assert_contains "$BETA_OUTPUT" "client_id: spiffe://${TRUST_DOMAIN}/openshell/sandbox/" + +printf "\nPodman SPIFFE token exchange demo succeeded.\n" diff --git a/examples/spiffe-token-exchange-demo/podman/provider-profile.yaml b/examples/spiffe-token-exchange-demo/podman/provider-profile.yaml new file mode 100644 index 0000000000..c98a19555b --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/provider-profile.yaml @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Template rendered by podman/demo.sh. +# __TOKEN_ISSUER_BASE_URL__ must be reachable from both the gateway process and +# the sandbox container. For local HTTP, OpenShell profile validation permits +# loopback hosts and Kubernetes-style service DNS names. + +id: spiffe-token-exchange-demo-podman +display_name: SPIFFE token exchange demo (Podman) +description: Dynamic token exchange for local Podman alpha/beta demo services using stored user subject tokens and SPIFFE JWT-SVID authentication +category: other +credentials: + - name: subject_token + description: Demo user subject token stored on the provider for token exchange + required: true + - name: access_token + description: Access token obtained via RFC 8693 token exchange + required: false + auth_style: bearer + header_name: Authorization + token_grant: + grant_type: token_exchange + token_endpoint: __TOKEN_ISSUER_BASE_URL__/token + audience: demo-default + jwt_svid_audience: __TOKEN_ISSUER_BASE_URL__ + client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-spiffe + scopes: [demo] + cache_ttl_seconds: 60 + subject_token: + source: provider_credential + credential: subject_token + subject_token_type: urn:ietf:params:oauth:token-type:access_token + audience_overrides: + - host: alpha-exchange + port: 8080 + audience: alpha + scopes: [alpha] + - host: beta-exchange + port: 8080 + audience: beta + scopes: [beta] +endpoints: + - host: alpha-exchange + port: 8080 + protocol: rest + tls: none + access: read-write + enforcement: enforce + - host: beta-exchange + port: 8080 + protocol: rest + tls: none + access: read-write + enforcement: enforce +binaries: + - /usr/bin/curl + - /usr/local/bin/curl diff --git a/examples/spiffe-token-exchange-demo/podman/spire/agent.conf b/examples/spiffe-token-exchange-demo/podman/spire/agent.conf new file mode 100644 index 0000000000..0dbf8daba7 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/spire/agent.conf @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +agent { + data_dir = "/run/spire/agent/data" + log_level = "INFO" + server_address = "spire-server" + server_port = "8081" + socket_path = "/run/spire/agent/sockets/agent.sock" + trust_domain = "openshell.local" + insecure_bootstrap = true +} + +plugins { + NodeAttestor "join_token" { + plugin_data {} + } + + KeyManager "disk" { + plugin_data { + directory = "/run/spire/agent/data" + } + } + + WorkloadAttestor "unix" { + plugin_data {} + } + + # Podman exposes a Docker-compatible API. The demo mounts the Podman socket + # here so SPIRE can use Docker-style selectors for the sandbox container + # when the local SPIRE version supports them. + WorkloadAttestor "docker" { + plugin_data { + docker_socket_path = "unix:///run/podman/podman.sock" + } + } +} diff --git a/examples/spiffe-token-exchange-demo/podman/spire/common.sh b/examples/spiffe-token-exchange-demo/podman/spire/common.sh new file mode 100755 index 0000000000..24aae8dc93 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/spire/common.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SPIRE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +PODMAN_NETWORK="${PODMAN_NETWORK:-openshell}" +TRUST_DOMAIN="${TRUST_DOMAIN:-openshell.local}" +SPIRE_AGENT_PARENT_ID="${SPIRE_AGENT_PARENT_ID:-spiffe://${TRUST_DOMAIN}/openshell/spire-agent/demo}" +SPIRE_SERVER_IMAGE="${SPIRE_SERVER_IMAGE:-ghcr.io/spiffe/spire-server:1.12.4}" +SPIRE_AGENT_IMAGE="${SPIRE_AGENT_IMAGE:-ghcr.io/spiffe/spire-agent:1.12.4}" +SPIRE_OIDC_IMAGE="${SPIRE_OIDC_IMAGE:-ghcr.io/spiffe/oidc-discovery-provider:1.12.4}" +SPIRE_SERVER_CONTAINER="${SPIRE_SERVER_CONTAINER:-openshell-spiffe-demo-spire-server}" +SPIRE_AGENT_CONTAINER="${SPIRE_AGENT_CONTAINER:-openshell-spiffe-demo-spire-agent}" +SPIRE_OIDC_CONTAINER="${SPIRE_OIDC_CONTAINER:-openshell-spiffe-demo-spire-oidc}" +SPIRE_SERVER_BIN="${SPIRE_SERVER_BIN:-/opt/spire/bin/spire-server}" +OIDC_PORT="${OIDC_PORT:-18081}" +SPIRE_STATE_DIR="${SPIRE_STATE_DIR:-}" +SPIRE_ENV_FILE="${SPIRE_ENV_FILE:-}" +CLEANUP_EXISTING="${CLEANUP_EXISTING:-1}" +PODMAN_SOCKET_DEMO_OWNED="${PODMAN_SOCKET_DEMO_OWNED:-0}" + +run() { + printf "\n$ %s\n" "$*" >&2 + "$@" +} + +require_cmd() { + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + printf "missing required command: %s\n" "$cmd" >&2 + exit 1 + fi +} + +cleanup_container() { + podman rm -f "$1" >/dev/null 2>&1 || true +} + +ensure_network() { + if ! podman network exists "$PODMAN_NETWORK" >/dev/null 2>&1; then + run podman network create "$PODMAN_NETWORK" + fi +} + +normalize_podman_socket_path() { + local socket_path="$1" + if [[ "$socket_path" == unix://* ]]; then + socket_path="${socket_path#unix://}" + fi + printf "%s\n" "$socket_path" +} + +detect_podman_socket() { + if [[ -n "${PODMAN_SOCKET:-}" ]]; then + normalize_podman_socket_path "$PODMAN_SOCKET" + return + fi + if [[ -n "${XDG_RUNTIME_DIR:-}" && -S "${XDG_RUNTIME_DIR}/podman/podman.sock" ]]; then + printf "%s\n" "${XDG_RUNTIME_DIR}/podman/podman.sock" + return + fi + if [[ -S "/run/user/$(id -u)/podman/podman.sock" ]]; then + printf "%s\n" "/run/user/$(id -u)/podman/podman.sock" + return + fi + if [[ -S /run/podman/podman.sock ]]; then + printf "%s\n" /run/podman/podman.sock + return + fi + if [[ -S /var/run/docker.sock ]]; then + printf "%s\n" /var/run/docker.sock + return + fi + local connection_socket + connection_socket="$( + podman system connection list --format '{{.URI}}' 2>/dev/null | + sed -n 's|^unix://||p' | + while IFS= read -r candidate; do + if [[ -S "$candidate" ]]; then + printf "%s\n" "$candidate" + break + fi + done + )" + if [[ -n "$connection_socket" ]]; then + printf "%s\n" "$connection_socket" + return + fi + return 1 +} + +podman_socket_volume() { + local source="$1" + local target="$2" + if [[ "$PODMAN_SOCKET_DEMO_OWNED" == "1" ]]; then + printf "%s:%s:z\n" "$source" "$target" + else + printf "%s:%s\n" "$source" "$target" + fi +} + +quote_env_value() { + printf "%q" "$1" +} + +write_env_line() { + local name="$1" + local value="$2" + if [[ -n "$SPIRE_ENV_FILE" ]]; then + printf "%s=%s\n" "$name" "$(quote_env_value "$value")" >>"$SPIRE_ENV_FILE" + fi +} + +reset_env_file() { + if [[ -n "$SPIRE_ENV_FILE" ]]; then + mkdir -p "$(dirname "$SPIRE_ENV_FILE")" + : >"$SPIRE_ENV_FILE" + fi +} + +copy_config() { + local source="$1" + local dest="$2" + mkdir -p "$(dirname "$dest")" + cp "$source" "$dest" + chmod 0644 "$dest" +} + +wait_for_socket() { + local path="$1" + local label="$2" + for _ in $(seq 1 80); do + if [[ -S "$path" ]]; then + return + fi + sleep 0.25 + done + printf "%s was not created at %s\n" "$label" "$path" >&2 + return 1 +} + +wait_for_http() { + local url="$1" + local label="$2" + for _ in $(seq 1 80); do + if curl -fsS "$url" >/dev/null 2>&1; then + return + fi + sleep 0.25 + done + printf "%s did not become ready at %s\n" "$label" "$url" >&2 + return 1 +} diff --git a/examples/spiffe-token-exchange-demo/podman/spire/oidc-discovery-provider.conf b/examples/spiffe-token-exchange-demo/podman/spire/oidc-discovery-provider.conf new file mode 100644 index 0000000000..9a1093cba9 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/spire/oidc-discovery-provider.conf @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +log_level = "INFO" +domains = ["spire-oidc", "127.0.0.1", "localhost"] +allow_insecure_scheme = true +insecure_addr = "0.0.0.0:8080" +jwt_issuer = "http://spire-oidc:8080" +jwks_uri = "http://spire-oidc:8080/keys" + +server_api { + address = "unix:///run/spire/server/private/api.sock" +} + +health_checks { + bind_port = "8008" + ready_path = "/ready" + live_path = "/live" +} diff --git a/examples/spiffe-token-exchange-demo/podman/spire/register-gateway.sh b/examples/spiffe-token-exchange-demo/podman/spire/register-gateway.sh new file mode 100755 index 0000000000..88cfca4630 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/spire/register-gateway.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +TRUST_DOMAIN="${TRUST_DOMAIN:-openshell.local}" +GATEWAY_SPIFFE_ID="${GATEWAY_SPIFFE_ID:-spiffe://${TRUST_DOMAIN}/openshell/gateway/demo}" +SPIRE_AGENT_PARENT_ID="${SPIRE_AGENT_PARENT_ID:-spiffe://${TRUST_DOMAIN}/openshell/spire-agent/demo}" +SPIRE_SERVER_SOCKET="${SPIRE_SERVER_SOCKET:-/run/spire/server/private/api.sock}" +SPIRE_SERVER_CONTAINER="${SPIRE_SERVER_CONTAINER:-openshell-spiffe-demo-spire-server}" +SPIRE_SERVER_BIN="${SPIRE_SERVER_BIN:-/opt/spire/bin/spire-server}" + +if [[ -n "${GATEWAY_SELECTORS:-}" ]]; then + read -r -a selectors <<<"$GATEWAY_SELECTORS" +else + gateway_path="${GATEWAY_WORKLOAD_PATH:-$(command -v openshell-server || true)}" + if [[ -n "$gateway_path" ]]; then + selectors=("unix:uid:$(id -u)" "unix:path:${gateway_path}") + else + printf "GATEWAY_WORKLOAD_PATH is unset and openshell-server is not on PATH; falling back to unix:uid only\n" >&2 + selectors=("unix:uid:$(id -u)") + fi +fi + +args=( + entry create + -socketPath "$SPIRE_SERVER_SOCKET" + -parentID "$SPIRE_AGENT_PARENT_ID" + -spiffeID "$GATEWAY_SPIFFE_ID" + -jwtSVIDTTL 300 +) + +for selector in "${selectors[@]}"; do + args+=(-selector "$selector") +done + +printf "Registering gateway SPIFFE entry: %s\n" "$GATEWAY_SPIFFE_ID" >&2 +podman exec "$SPIRE_SERVER_CONTAINER" "$SPIRE_SERVER_BIN" "${args[@]}" diff --git a/examples/spiffe-token-exchange-demo/podman/spire/register-sandbox.sh b/examples/spiffe-token-exchange-demo/podman/spire/register-sandbox.sh new file mode 100755 index 0000000000..5213421cfe --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/spire/register-sandbox.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + printf "usage: %s \n" "$0" >&2 + exit 2 +fi + +SANDBOX_ID="$1" +TRUST_DOMAIN="${TRUST_DOMAIN:-openshell.local}" +SANDBOX_SPIFFE_ID="${SANDBOX_SPIFFE_ID:-spiffe://${TRUST_DOMAIN}/openshell/sandbox/${SANDBOX_ID}}" +SPIRE_AGENT_PARENT_ID="${SPIRE_AGENT_PARENT_ID:-spiffe://${TRUST_DOMAIN}/openshell/spire-agent/demo}" +SPIRE_SERVER_SOCKET="${SPIRE_SERVER_SOCKET:-/run/spire/server/private/api.sock}" +SPIRE_SERVER_CONTAINER="${SPIRE_SERVER_CONTAINER:-openshell-spiffe-demo-spire-server}" +SPIRE_SERVER_BIN="${SPIRE_SERVER_BIN:-/opt/spire/bin/spire-server}" + +if [[ -n "${SANDBOX_SELECTORS:-}" ]]; then + read -r -a selectors <<<"$SANDBOX_SELECTORS" +else + selectors=( + "docker:label:openshell.managed:true" + "docker:label:openshell.ai/sandbox-id:${SANDBOX_ID}" + ) +fi + +args=( + entry create + -socketPath "$SPIRE_SERVER_SOCKET" + -parentID "$SPIRE_AGENT_PARENT_ID" + -spiffeID "$SANDBOX_SPIFFE_ID" + -jwtSVIDTTL 300 +) + +for selector in "${selectors[@]}"; do + args+=(-selector "$selector") +done + +printf "Registering sandbox SPIFFE entry: %s\n" "$SANDBOX_SPIFFE_ID" >&2 +podman exec "$SPIRE_SERVER_CONTAINER" "$SPIRE_SERVER_BIN" "${args[@]}" diff --git a/examples/spiffe-token-exchange-demo/podman/spire/server.conf b/examples/spiffe-token-exchange-demo/podman/spire/server.conf new file mode 100644 index 0000000000..59c7f9d154 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/spire/server.conf @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +server { + bind_address = "0.0.0.0" + bind_port = "8081" + socket_path = "/run/spire/server/private/api.sock" + trust_domain = "openshell.local" + data_dir = "/run/spire/server/data" + log_level = "INFO" + jwt_issuer = "http://spire-oidc:8080" + default_x509_svid_ttl = "1h" + default_jwt_svid_ttl = "5m" + + ca_subject { + country = ["US"] + organization = ["OpenShell Demo"] + common_name = "OpenShell Demo SPIRE" + } +} + +plugins { + DataStore "sql" { + plugin_data { + database_type = "sqlite3" + connection_string = "/run/spire/server/data/datastore.sqlite3" + } + } + + NodeAttestor "join_token" { + plugin_data {} + } + + KeyManager "disk" { + plugin_data { + keys_path = "/run/spire/server/data/keys.json" + } + } +} diff --git a/examples/spiffe-token-exchange-demo/podman/spire/start-agent.sh b/examples/spiffe-token-exchange-demo/podman/spire/start-agent.sh new file mode 100755 index 0000000000..a0fab6eddd --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/spire/start-agent.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +require_cmd podman +require_cmd awk +require_cmd sed + +if [[ -z "$SPIRE_STATE_DIR" ]]; then + SPIRE_STATE_DIR="$(mktemp -d)" +fi + +if ! PODMAN_SOCKET="$(detect_podman_socket)"; then + printf "could not find a Podman API socket; set PODMAN_SOCKET\n" >&2 + exit 1 +fi + +reset_env_file +ensure_network + +agent_dir="${SPIRE_STATE_DIR}/agent" +mount_dir="${SPIRE_STATE_DIR}/mounts" +agent_conf_mount="${mount_dir}/agent.conf" +SPIRE_AGENT_SOCKET_HOST_PATH="${SPIRE_AGENT_SOCKET_HOST_PATH:-${agent_dir}/sockets/agent.sock}" + +mkdir -p "${agent_dir}/data" "${agent_dir}/sockets" "$mount_dir" +chmod 0777 "$SPIRE_STATE_DIR" "$agent_dir" "${agent_dir}/data" "${agent_dir}/sockets" +copy_config "${SPIRE_SCRIPT_DIR}/agent.conf" "$agent_conf_mount" + +if [[ "$CLEANUP_EXISTING" == "1" ]]; then + cleanup_container "$SPIRE_AGENT_CONTAINER" +fi + +join_token="$( + podman exec "$SPIRE_SERVER_CONTAINER" "$SPIRE_SERVER_BIN" token generate \ + -socketPath /run/spire/server/private/api.sock \ + -spiffeID "$SPIRE_AGENT_PARENT_ID" | + awk '/Token:/ { print $2; exit }' +)" +if [[ -z "$join_token" ]]; then + printf "failed to generate SPIRE agent join token\n" >&2 + exit 1 +fi + +run podman run -d \ + --name "$SPIRE_AGENT_CONTAINER" \ + --network "$PODMAN_NETWORK" \ + --pid=host \ + --security-opt label=disable \ + -v "${agent_conf_mount}:/run/spire/config/agent.conf:ro,z" \ + -v "${agent_dir}:/run/spire/agent:z" \ + -v "$(podman_socket_volume "$PODMAN_SOCKET" /run/podman/podman.sock)" \ + "$SPIRE_AGENT_IMAGE" \ + -config /run/spire/config/agent.conf -joinToken "$join_token" + +if ! wait_for_socket "$SPIRE_AGENT_SOCKET_HOST_PATH" "SPIRE agent Workload API socket"; then + podman logs "$SPIRE_AGENT_CONTAINER" >&2 || true + exit 1 +fi + +write_env_line SPIRE_STATE_DIR "$SPIRE_STATE_DIR" +write_env_line SPIRE_AGENT_DIR "$agent_dir" +write_env_line SPIRE_AGENT_SOCKET_HOST_PATH "$SPIRE_AGENT_SOCKET_HOST_PATH" +write_env_line PODMAN_SOCKET "$PODMAN_SOCKET" + +printf "SPIRE agent container: %s\n" "$SPIRE_AGENT_CONTAINER" +printf "SPIRE agent parent ID: %s\n" "$SPIRE_AGENT_PARENT_ID" +printf "SPIRE agent Workload API socket: %s\n" "$SPIRE_AGENT_SOCKET_HOST_PATH" +if [[ -n "$SPIRE_ENV_FILE" ]]; then + printf "Wrote environment file: %s\n" "$SPIRE_ENV_FILE" +fi diff --git a/examples/spiffe-token-exchange-demo/podman/spire/start-server-oidc.sh b/examples/spiffe-token-exchange-demo/podman/spire/start-server-oidc.sh new file mode 100755 index 0000000000..321ff9a2ea --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/spire/start-server-oidc.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${SCRIPT_DIR}/common.sh" + +require_cmd podman +require_cmd curl + +if [[ -z "$SPIRE_STATE_DIR" ]]; then + SPIRE_STATE_DIR="$(mktemp -d)" +fi + +reset_env_file +ensure_network + +server_dir="${SPIRE_STATE_DIR}/server" +mount_dir="${SPIRE_STATE_DIR}/mounts" +server_conf_mount="${mount_dir}/server.conf" +oidc_conf_mount="${mount_dir}/oidc-discovery-provider.conf" +server_socket_host_path="${server_dir}/private/api.sock" + +mkdir -p "${server_dir}/data" "${server_dir}/private" "$mount_dir" +chmod 0777 "$SPIRE_STATE_DIR" "$server_dir" "${server_dir}/data" "${server_dir}/private" +copy_config "${SPIRE_SCRIPT_DIR}/server.conf" "$server_conf_mount" +copy_config "${SPIRE_SCRIPT_DIR}/oidc-discovery-provider.conf" "$oidc_conf_mount" + +if [[ "$CLEANUP_EXISTING" == "1" ]]; then + cleanup_container "$SPIRE_OIDC_CONTAINER" + cleanup_container "$SPIRE_SERVER_CONTAINER" +fi + +printf "Starting SPIRE server container: %s\n" "$SPIRE_SERVER_CONTAINER" >&2 +run podman run -d \ + --name "$SPIRE_SERVER_CONTAINER" \ + --network "$PODMAN_NETWORK" \ + --network-alias spire-server \ + -v "${server_conf_mount}:/run/spire/config/server.conf:ro,z" \ + -v "${server_dir}:/run/spire/server:z" \ + "$SPIRE_SERVER_IMAGE" \ + -config /run/spire/config/server.conf + +if ! wait_for_socket "$server_socket_host_path" "SPIRE server API socket"; then + podman logs "$SPIRE_SERVER_CONTAINER" >&2 || true + exit 1 +fi + +printf "Starting SPIRE OIDC discovery provider container: %s\n" "$SPIRE_OIDC_CONTAINER" >&2 +run podman run -d \ + --name "$SPIRE_OIDC_CONTAINER" \ + --network "$PODMAN_NETWORK" \ + --network-alias spire-oidc \ + -p "127.0.0.1:${OIDC_PORT}:8080" \ + -v "${oidc_conf_mount}:/run/spire/config/oidc-discovery-provider.conf:ro,z" \ + -v "${server_dir}/private:/run/spire/server/private:z" \ + "$SPIRE_OIDC_IMAGE" \ + -config /run/spire/config/oidc-discovery-provider.conf + +if ! wait_for_http "http://127.0.0.1:${OIDC_PORT}/keys" "SPIRE OIDC discovery provider"; then + podman logs "$SPIRE_OIDC_CONTAINER" >&2 || true + exit 1 +fi + +write_env_line SPIRE_STATE_DIR "$SPIRE_STATE_DIR" +write_env_line SPIRE_SERVER_DIR "$server_dir" +write_env_line SPIRE_SERVER_SOCKET_HOST_PATH "$server_socket_host_path" +write_env_line SPIRE_OIDC_KEYS_URL "http://127.0.0.1:${OIDC_PORT}/keys" + +printf "SPIRE server container: %s\n" "$SPIRE_SERVER_CONTAINER" +printf "SPIRE server API socket: %s\n" "$server_socket_host_path" +printf "SPIRE OIDC discovery provider container: %s\n" "$SPIRE_OIDC_CONTAINER" +printf "SPIRE OIDC JWKS URL: http://127.0.0.1:%s/keys\n" "$OIDC_PORT" +if [[ -n "$SPIRE_ENV_FILE" ]]; then + printf "Wrote environment file: %s\n" "$SPIRE_ENV_FILE" +fi diff --git a/examples/spiffe-token-exchange-demo/podman/start-gateway.sh b/examples/spiffe-token-exchange-demo/podman/start-gateway.sh new file mode 100755 index 0000000000..fea89a117a --- /dev/null +++ b/examples/spiffe-token-exchange-demo/podman/start-gateway.sh @@ -0,0 +1,233 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=spire/common.sh +source "${SCRIPT_DIR}/spire/common.sh" + +require_cmd podman +require_cmd curl +require_cmd openssl + +if [[ -n "${SPIRE_AGENT_ENV_FILE:-}" ]]; then + # shellcheck disable=SC1090 + source "$SPIRE_AGENT_ENV_FILE" +fi + +GATEWAY_CONTAINER="${GATEWAY_CONTAINER:-openshell-spiffe-demo-gateway}" +GATEWAY_IMAGE="${GATEWAY_IMAGE:-ghcr.io/nvidia/openshell/gateway:latest}" +GATEWAY_ID="${GATEWAY_ID:-podman-spiffe-demo}" +GATEWAY_PORT="${GATEWAY_PORT:-8888}" +GATEWAY_HEALTH_PORT="${GATEWAY_HEALTH_PORT:-8889}" +GATEWAY_STATE_DIR="${GATEWAY_STATE_DIR:-}" +GATEWAY_ENV_FILE="${GATEWAY_ENV_FILE:-}" +SANDBOX_IMAGE="${SANDBOX_IMAGE:-}" +SUPERVISOR_IMAGE="${SUPERVISOR_IMAGE:-}" +SANDBOX_IMAGE_PULL_POLICY="${SANDBOX_IMAGE_PULL_POLICY:-missing}" +PODMAN_STOP_TIMEOUT_SECS="${PODMAN_STOP_TIMEOUT_SECS:-3}" +GATEWAY_OIDC_ISSUER="${GATEWAY_OIDC_ISSUER:-}" +GATEWAY_OIDC_AUDIENCE="${GATEWAY_OIDC_AUDIENCE:-openshell-cli}" +GATEWAY_OIDC_JWKS_TTL_SECS="${GATEWAY_OIDC_JWKS_TTL_SECS:-3600}" +GATEWAY_OIDC_ROLES_CLAIM="${GATEWAY_OIDC_ROLES_CLAIM:-realm_access.roles}" +GATEWAY_OIDC_ADMIN_ROLE="${GATEWAY_OIDC_ADMIN_ROLE:-openshell-admin}" +GATEWAY_OIDC_USER_ROLE="${GATEWAY_OIDC_USER_ROLE:-openshell-user}" +GATEWAY_OIDC_SCOPES_CLAIM="${GATEWAY_OIDC_SCOPES_CLAIM:-}" +GATEWAY_OIDC_CLIENT_ID="${GATEWAY_OIDC_CLIENT_ID:-openshell-cli}" +GATEWAY_OIDC_LOGIN_SCOPES="${GATEWAY_OIDC_LOGIN_SCOPES:-}" +if [[ -z "${GATEWAY_ALLOW_UNAUTHENTICATED_USERS:-}" ]]; then + if [[ -n "$GATEWAY_OIDC_ISSUER" ]]; then + GATEWAY_ALLOW_UNAUTHENTICATED_USERS="false" + else + GATEWAY_ALLOW_UNAUTHENTICATED_USERS="true" + fi +fi + +if [[ -z "${SPIRE_AGENT_SOCKET_HOST_PATH:-}" ]]; then + printf "SPIRE_AGENT_SOCKET_HOST_PATH is required; source start-agent.sh's env file or set SPIRE_AGENT_ENV_FILE\n" >&2 + exit 1 +fi +if [[ ! -S "$SPIRE_AGENT_SOCKET_HOST_PATH" ]]; then + printf "SPIRE agent Workload API socket does not exist: %s\n" "$SPIRE_AGENT_SOCKET_HOST_PATH" >&2 + exit 1 +fi +if ! PODMAN_SOCKET="$(detect_podman_socket)"; then + printf "could not find a Podman API socket; set PODMAN_SOCKET\n" >&2 + exit 1 +fi +if [[ ! "$PODMAN_STOP_TIMEOUT_SECS" =~ ^[0-9]+$ ]]; then + printf "PODMAN_STOP_TIMEOUT_SECS must be a non-negative integer, got: %s\n" "$PODMAN_STOP_TIMEOUT_SECS" >&2 + exit 1 +fi +if [[ ! "$GATEWAY_OIDC_JWKS_TTL_SECS" =~ ^[0-9]+$ ]]; then + printf "GATEWAY_OIDC_JWKS_TTL_SECS must be a non-negative integer, got: %s\n" "$GATEWAY_OIDC_JWKS_TTL_SECS" >&2 + exit 1 +fi +if [[ "$GATEWAY_ALLOW_UNAUTHENTICATED_USERS" != "true" && "$GATEWAY_ALLOW_UNAUTHENTICATED_USERS" != "false" ]]; then + printf "GATEWAY_ALLOW_UNAUTHENTICATED_USERS must be true or false, got: %s\n" "$GATEWAY_ALLOW_UNAUTHENTICATED_USERS" >&2 + exit 1 +fi + +if [[ -z "$GATEWAY_STATE_DIR" ]]; then + GATEWAY_STATE_DIR="$(mktemp -d)" +fi + +toml_string_escape() { + local value="$1" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + printf "%s\n" "$value" +} + +write_gateway_env_line() { + local name="$1" + local value="$2" + if [[ -n "$GATEWAY_ENV_FILE" ]]; then + printf "%s=%s\n" "$name" "$(quote_env_value "$value")" >>"$GATEWAY_ENV_FILE" + fi +} + +reset_gateway_env_file() { + if [[ -n "$GATEWAY_ENV_FILE" ]]; then + mkdir -p "$(dirname "$GATEWAY_ENV_FILE")" + : >"$GATEWAY_ENV_FILE" + fi +} + +write_gateway_config() { + local podman_socket_in_container="$1" + local jwt_dir="${GATEWAY_STATE_DIR}/jwt" + local config_path="${GATEWAY_STATE_DIR}/gateway.toml" + local sandbox_image_line="" + local supervisor_image_line="" + local oidc_block="" + + mkdir -p "$jwt_dir" + if [[ ! -s "${jwt_dir}/signing.pem" ]]; then + openssl genpkey -algorithm ed25519 -out "${jwt_dir}/signing.pem" >/dev/null 2>&1 + openssl pkey -in "${jwt_dir}/signing.pem" -pubout -out "${jwt_dir}/public.pem" >/dev/null 2>&1 + openssl rand -hex 8 >"${jwt_dir}/kid" + fi + if [[ -n "$SANDBOX_IMAGE" ]]; then + sandbox_image_line="default_image = \"$(toml_string_escape "$SANDBOX_IMAGE")\"" + fi + if [[ -n "$SUPERVISOR_IMAGE" ]]; then + supervisor_image_line="supervisor_image = \"$(toml_string_escape "$SUPERVISOR_IMAGE")\"" + fi + if [[ -n "$GATEWAY_OIDC_ISSUER" ]]; then + oidc_block=" +[openshell.gateway.oidc] +issuer = \"$(toml_string_escape "$GATEWAY_OIDC_ISSUER")\" +audience = \"$(toml_string_escape "$GATEWAY_OIDC_AUDIENCE")\" +jwks_ttl_secs = ${GATEWAY_OIDC_JWKS_TTL_SECS} +roles_claim = \"$(toml_string_escape "$GATEWAY_OIDC_ROLES_CLAIM")\" +admin_role = \"$(toml_string_escape "$GATEWAY_OIDC_ADMIN_ROLE")\" +user_role = \"$(toml_string_escape "$GATEWAY_OIDC_USER_ROLE")\" +scopes_claim = \"$(toml_string_escape "$GATEWAY_OIDC_SCOPES_CLAIM")\" +" + fi + + cat >"$config_path" <&2 || true + exit 1 +fi + +write_gateway_env_line GATEWAY_STATE_DIR "$GATEWAY_STATE_DIR" +write_gateway_env_line GATEWAY_CONFIG "$gateway_config" +write_gateway_env_line GATEWAY_CONTAINER "$GATEWAY_CONTAINER" +write_gateway_env_line GATEWAY_ENDPOINT "http://127.0.0.1:${GATEWAY_PORT}" +write_gateway_env_line GATEWAY_HEALTH_ENDPOINT "http://127.0.0.1:${GATEWAY_HEALTH_PORT}" +write_gateway_env_line GATEWAY_OIDC_ISSUER "$GATEWAY_OIDC_ISSUER" +write_gateway_env_line GATEWAY_OIDC_AUDIENCE "$GATEWAY_OIDC_AUDIENCE" +write_gateway_env_line GATEWAY_OIDC_CLIENT_ID "$GATEWAY_OIDC_CLIENT_ID" +write_gateway_env_line GATEWAY_OIDC_LOGIN_SCOPES "$GATEWAY_OIDC_LOGIN_SCOPES" + +printf "OpenShell gateway container: %s\n" "$GATEWAY_CONTAINER" +printf "OpenShell gateway endpoint: http://127.0.0.1:%s\n" "$GATEWAY_PORT" +printf "OpenShell gateway health endpoint: http://127.0.0.1:%s\n" "$GATEWAY_HEALTH_PORT" +printf "OpenShell gateway config: %s\n" "$gateway_config" +printf "SPIRE agent Workload API socket: %s\n" "$SPIRE_AGENT_SOCKET_HOST_PATH" +if [[ -n "$GATEWAY_OIDC_ISSUER" ]]; then + printf "OpenShell gateway OIDC issuer: %s\n" "$GATEWAY_OIDC_ISSUER" + printf "Register/login with:\n" + printf " openshell gateway add http://127.0.0.1:%s --name %s --oidc-issuer %s --oidc-client-id %s --oidc-audience %s" \ + "$GATEWAY_PORT" "$GATEWAY_ID" "$GATEWAY_OIDC_ISSUER" "$GATEWAY_OIDC_CLIENT_ID" "$GATEWAY_OIDC_AUDIENCE" + if [[ -n "$GATEWAY_OIDC_LOGIN_SCOPES" ]]; then + printf " --oidc-scopes %s" "$GATEWAY_OIDC_LOGIN_SCOPES" + fi + printf "\n" +fi +if [[ -n "$GATEWAY_ENV_FILE" ]]; then + printf "Wrote environment file: %s\n" "$GATEWAY_ENV_FILE" +fi diff --git a/examples/spiffe-token-exchange-demo/provider-profile.yaml b/examples/spiffe-token-exchange-demo/provider-profile.yaml new file mode 100644 index 0000000000..0c751ed007 --- /dev/null +++ b/examples/spiffe-token-exchange-demo/provider-profile.yaml @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +id: spiffe-token-exchange-demo +display_name: SPIFFE token exchange demo +description: Dynamic token exchange for alpha/beta demo services using stored user subject tokens and SPIFFE JWT-SVID authentication +category: other +credentials: + - name: subject_token + description: Demo user subject token stored on the provider for token exchange + required: true + - name: access_token + description: Access token obtained via RFC 8693 token exchange + required: false + auth_style: bearer + header_name: Authorization + token_grant: + grant_type: token_exchange + token_endpoint: http://token-exchange-issuer.default.svc.cluster.local/token + audience: demo-default + jwt_svid_audience: http://token-exchange-issuer.default.svc.cluster.local + client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-spiffe + scopes: [demo] + cache_ttl_seconds: 60 + subject_token: + source: provider_credential + credential: subject_token + subject_token_type: urn:ietf:params:oauth:token-type:access_token + audience_overrides: + - host: alpha-exchange.default.svc.cluster.local + port: 80 + audience: alpha + scopes: [alpha] + - host: beta-exchange.default.svc.cluster.local + port: 80 + audience: beta + scopes: [beta] +endpoints: + - host: alpha-exchange.default.svc.cluster.local + port: 80 + protocol: rest + tls: none + access: read-write + enforcement: enforce + - host: beta-exchange.default.svc.cluster.local + port: 80 + protocol: rest + tls: none + access: read-write + enforcement: enforce +binaries: + - /usr/bin/curl + - /usr/local/bin/curl diff --git a/proto/openshell.proto b/proto/openshell.proto index 49f6581e7c..829253efef 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -408,6 +408,15 @@ service OpenShell { }; } + // Exchange a stored provider subject token for an intermediate token scoped + // to the calling supervisor's SPIFFE identity. + rpc ExchangeProviderSubjectToken(ExchangeProviderSubjectTokenRequest) + returns (ExchangeProviderSubjectTokenResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "sandbox" + }; + } + // Fetch recent sandbox logs (one-shot). rpc GetSandboxLogs(GetSandboxLogsRequest) returns (GetSandboxLogsResponse) { option (openshell.options.v1.authorization) = { @@ -1431,6 +1440,25 @@ message ProviderCredentialTokenGrantAudienceOverride { // Provider credential token grant configuration. // When present, the credential is obtained dynamically via OAuth2 grant when needed. +enum ProviderCredentialTokenGrantType { + PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED = 0; + PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS = 1; + PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE = 2; +} + +message ProviderCredentialTokenGrantSubjectToken { + // Source for the token exchange subject token. Phase one supports + // "provider_credential". + string source = 1; + + // Provider credential key that stores the subject token. + string credential = 2; + + // OAuth2 subject_token_type. If omitted, OpenShell uses + // urn:ietf:params:oauth:token-type:access_token. + string subject_token_type = 3; +} + message ProviderCredentialTokenGrant { // OAuth2 token endpoint URL (e.g., https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token) string token_endpoint = 1; @@ -1455,6 +1483,17 @@ message ProviderCredentialTokenGrant { // Optional: OAuth2 client_assertion_type value. If omitted, OpenShell uses // urn:ietf:params:oauth:client-assertion-type:jwt-bearer. string client_assertion_type = 7; + + // Grant type. If omitted/unspecified, OpenShell treats this as client_credentials + // for backwards compatibility. + ProviderCredentialTokenGrantType grant_type = 8; + + // Subject token metadata for token_exchange grants. + ProviderCredentialTokenGrantSubjectToken subject_token = 9; + + // OAuth2 requested_token_type. If omitted for token_exchange, OpenShell uses + // urn:ietf:params:oauth:token-type:access_token. + string requested_token_type = 10; } // Provider credential declaration. @@ -1767,6 +1806,27 @@ message GetSandboxProviderEnvironmentResponse { repeated string non_secret_environment_keys = 6; } +message ExchangeProviderSubjectTokenRequest { + // The sandbox ID. Must match the authenticated sandbox principal. + string sandbox_id = 1; + + // Attached provider record holding the configured subject token credential. + string provider = 2; + + // Provider profile credential that declares the token_exchange grant. + string credential_key = 3; + + // Supervisor JWT-SVID. The gateway verifies this and uses its `sub` claim + // as the requested audience for the intermediate token. + string supervisor_jwt_svid = 4 [(openshell.options.v1.secret) = true]; +} + +message ExchangeProviderSubjectTokenResponse { + string access_token = 1 [(openshell.options.v1.secret) = true]; + int64 expires_in = 2; + string token_type = 3; +} + // --------------------------------------------------------------------------- // Policy update messages // --------------------------------------------------------------------------- diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index a854c751a2..a0cc858ad2 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -90,6 +90,57 @@ func (SandboxPhase) EnumDescriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{0} } +// Provider credential token grant configuration. +// When present, the credential is obtained dynamically via OAuth2 grant when needed. +type ProviderCredentialTokenGrantType int32 + +const ( + ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED ProviderCredentialTokenGrantType = 0 + ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS ProviderCredentialTokenGrantType = 1 + ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE ProviderCredentialTokenGrantType = 2 +) + +// Enum value maps for ProviderCredentialTokenGrantType. +var ( + ProviderCredentialTokenGrantType_name = map[int32]string{ + 0: "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED", + 1: "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS", + 2: "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE", + } + ProviderCredentialTokenGrantType_value = map[string]int32{ + "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED": 0, + "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS": 1, + "PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE": 2, + } +) + +func (x ProviderCredentialTokenGrantType) Enum() *ProviderCredentialTokenGrantType { + p := new(ProviderCredentialTokenGrantType) + *p = x + return p +} + +func (x ProviderCredentialTokenGrantType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProviderCredentialTokenGrantType) Descriptor() protoreflect.EnumDescriptor { + return file_openshell_proto_enumTypes[1].Descriptor() +} + +func (ProviderCredentialTokenGrantType) Type() protoreflect.EnumType { + return &file_openshell_proto_enumTypes[1] +} + +func (x ProviderCredentialTokenGrantType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProviderCredentialTokenGrantType.Descriptor instead. +func (ProviderCredentialTokenGrantType) EnumDescriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{1} +} + type ProviderCredentialRefreshStrategy int32 const ( @@ -135,11 +186,11 @@ func (x ProviderCredentialRefreshStrategy) String() string { } func (ProviderCredentialRefreshStrategy) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[1].Descriptor() + return file_openshell_proto_enumTypes[2].Descriptor() } func (ProviderCredentialRefreshStrategy) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[1] + return &file_openshell_proto_enumTypes[2] } func (x ProviderCredentialRefreshStrategy) Number() protoreflect.EnumNumber { @@ -148,7 +199,7 @@ func (x ProviderCredentialRefreshStrategy) Number() protoreflect.EnumNumber { // Deprecated: Use ProviderCredentialRefreshStrategy.Descriptor instead. func (ProviderCredentialRefreshStrategy) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{1} + return file_openshell_proto_rawDescGZIP(), []int{2} } // Stable provider profile categories used by clients for grouping and filtering. @@ -200,11 +251,11 @@ func (x ProviderProfileCategory) String() string { } func (ProviderProfileCategory) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[2].Descriptor() + return file_openshell_proto_enumTypes[3].Descriptor() } func (ProviderProfileCategory) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[2] + return &file_openshell_proto_enumTypes[3] } func (x ProviderProfileCategory) Number() protoreflect.EnumNumber { @@ -213,7 +264,7 @@ func (x ProviderProfileCategory) Number() protoreflect.EnumNumber { // Deprecated: Use ProviderProfileCategory.Descriptor instead. func (ProviderProfileCategory) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{2} + return file_openshell_proto_rawDescGZIP(), []int{3} } // Policy load status. @@ -260,11 +311,11 @@ func (x PolicyStatus) String() string { } func (PolicyStatus) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[3].Descriptor() + return file_openshell_proto_enumTypes[4].Descriptor() } func (PolicyStatus) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[3] + return &file_openshell_proto_enumTypes[4] } func (x PolicyStatus) Number() protoreflect.EnumNumber { @@ -273,7 +324,7 @@ func (x PolicyStatus) Number() protoreflect.EnumNumber { // Deprecated: Use PolicyStatus.Descriptor instead. func (PolicyStatus) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{3} + return file_openshell_proto_rawDescGZIP(), []int{4} } // Service status enum. @@ -313,11 +364,11 @@ func (x ServiceStatus) String() string { } func (ServiceStatus) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[4].Descriptor() + return file_openshell_proto_enumTypes[5].Descriptor() } func (ServiceStatus) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[4] + return &file_openshell_proto_enumTypes[5] } func (x ServiceStatus) Number() protoreflect.EnumNumber { @@ -326,7 +377,7 @@ func (x ServiceStatus) Number() protoreflect.EnumNumber { // Deprecated: Use ServiceStatus.Descriptor instead. func (ServiceStatus) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{4} + return file_openshell_proto_rawDescGZIP(), []int{5} } // Workspace-scoped role for members. @@ -363,11 +414,11 @@ func (x WorkspaceRole) String() string { } func (WorkspaceRole) Descriptor() protoreflect.EnumDescriptor { - return file_openshell_proto_enumTypes[5].Descriptor() + return file_openshell_proto_enumTypes[6].Descriptor() } func (WorkspaceRole) Type() protoreflect.EnumType { - return &file_openshell_proto_enumTypes[5] + return &file_openshell_proto_enumTypes[6] } func (x WorkspaceRole) Number() protoreflect.EnumNumber { @@ -376,7 +427,7 @@ func (x WorkspaceRole) Number() protoreflect.EnumNumber { // Deprecated: Use WorkspaceRole.Descriptor instead. func (WorkspaceRole) EnumDescriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{5} + return file_openshell_proto_rawDescGZIP(), []int{6} } // IssueSandboxToken request. Empty body; identity is established by the @@ -5099,8 +5150,71 @@ func (x *ProviderCredentialTokenGrantAudienceOverride) GetScopes() []string { return nil } -// Provider credential token grant configuration. -// When present, the credential is obtained dynamically via OAuth2 grant when needed. +type ProviderCredentialTokenGrantSubjectToken struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Source for the token exchange subject token. Phase one supports + // "provider_credential". + Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + // Provider credential key that stores the subject token. + Credential string `protobuf:"bytes,2,opt,name=credential,proto3" json:"credential,omitempty"` + // OAuth2 subject_token_type. If omitted, OpenShell uses + // urn:ietf:params:oauth:token-type:access_token. + SubjectTokenType string `protobuf:"bytes,3,opt,name=subject_token_type,json=subjectTokenType,proto3" json:"subject_token_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderCredentialTokenGrantSubjectToken) Reset() { + *x = ProviderCredentialTokenGrantSubjectToken{} + mi := &file_openshell_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderCredentialTokenGrantSubjectToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderCredentialTokenGrantSubjectToken) ProtoMessage() {} + +func (x *ProviderCredentialTokenGrantSubjectToken) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[71] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProviderCredentialTokenGrantSubjectToken.ProtoReflect.Descriptor instead. +func (*ProviderCredentialTokenGrantSubjectToken) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{71} +} + +func (x *ProviderCredentialTokenGrantSubjectToken) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *ProviderCredentialTokenGrantSubjectToken) GetCredential() string { + if x != nil { + return x.Credential + } + return "" +} + +func (x *ProviderCredentialTokenGrantSubjectToken) GetSubjectTokenType() string { + if x != nil { + return x.SubjectTokenType + } + return "" +} + type ProviderCredentialTokenGrant struct { state protoimpl.MessageState `protogen:"open.v1"` // OAuth2 token endpoint URL (e.g., https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token) @@ -5120,13 +5234,21 @@ type ProviderCredentialTokenGrant struct { // Optional: OAuth2 client_assertion_type value. If omitted, OpenShell uses // urn:ietf:params:oauth:client-assertion-type:jwt-bearer. ClientAssertionType string `protobuf:"bytes,7,opt,name=client_assertion_type,json=clientAssertionType,proto3" json:"client_assertion_type,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Grant type. If omitted/unspecified, OpenShell treats this as client_credentials + // for backwards compatibility. + GrantType ProviderCredentialTokenGrantType `protobuf:"varint,8,opt,name=grant_type,json=grantType,proto3,enum=openshell.v1.ProviderCredentialTokenGrantType" json:"grant_type,omitempty"` + // Subject token metadata for token_exchange grants. + SubjectToken *ProviderCredentialTokenGrantSubjectToken `protobuf:"bytes,9,opt,name=subject_token,json=subjectToken,proto3" json:"subject_token,omitempty"` + // OAuth2 requested_token_type. If omitted for token_exchange, OpenShell uses + // urn:ietf:params:oauth:token-type:access_token. + RequestedTokenType string `protobuf:"bytes,10,opt,name=requested_token_type,json=requestedTokenType,proto3" json:"requested_token_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ProviderCredentialTokenGrant) Reset() { *x = ProviderCredentialTokenGrant{} - mi := &file_openshell_proto_msgTypes[71] + mi := &file_openshell_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5138,7 +5260,7 @@ func (x *ProviderCredentialTokenGrant) String() string { func (*ProviderCredentialTokenGrant) ProtoMessage() {} func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[71] + mi := &file_openshell_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5151,7 +5273,7 @@ func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialTokenGrant.ProtoReflect.Descriptor instead. func (*ProviderCredentialTokenGrant) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{71} + return file_openshell_proto_rawDescGZIP(), []int{72} } func (x *ProviderCredentialTokenGrant) GetTokenEndpoint() string { @@ -5203,6 +5325,27 @@ func (x *ProviderCredentialTokenGrant) GetClientAssertionType() string { return "" } +func (x *ProviderCredentialTokenGrant) GetGrantType() ProviderCredentialTokenGrantType { + if x != nil { + return x.GrantType + } + return ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED +} + +func (x *ProviderCredentialTokenGrant) GetSubjectToken() *ProviderCredentialTokenGrantSubjectToken { + if x != nil { + return x.SubjectToken + } + return nil +} + +func (x *ProviderCredentialTokenGrant) GetRequestedTokenType() string { + if x != nil { + return x.RequestedTokenType + } + return "" +} + // Provider credential declaration. type ProviderProfileCredential struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -5222,7 +5365,7 @@ type ProviderProfileCredential struct { func (x *ProviderProfileCredential) Reset() { *x = ProviderProfileCredential{} - mi := &file_openshell_proto_msgTypes[72] + mi := &file_openshell_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5234,7 +5377,7 @@ func (x *ProviderProfileCredential) String() string { func (*ProviderProfileCredential) ProtoMessage() {} func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[72] + mi := &file_openshell_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5247,7 +5390,7 @@ func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileCredential.ProtoReflect.Descriptor instead. func (*ProviderProfileCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{72} + return file_openshell_proto_rawDescGZIP(), []int{73} } func (x *ProviderProfileCredential) GetName() string { @@ -5332,7 +5475,7 @@ type ProviderCredentialRefreshMaterial struct { func (x *ProviderCredentialRefreshMaterial) Reset() { *x = ProviderCredentialRefreshMaterial{} - mi := &file_openshell_proto_msgTypes[73] + mi := &file_openshell_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5344,7 +5487,7 @@ func (x *ProviderCredentialRefreshMaterial) String() string { func (*ProviderCredentialRefreshMaterial) ProtoMessage() {} func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[73] + mi := &file_openshell_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5357,7 +5500,7 @@ func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message // Deprecated: Use ProviderCredentialRefreshMaterial.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshMaterial) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{73} + return file_openshell_proto_rawDescGZIP(), []int{74} } func (x *ProviderCredentialRefreshMaterial) GetName() string { @@ -5402,7 +5545,7 @@ type ProviderCredentialRefreshOutput struct { func (x *ProviderCredentialRefreshOutput) Reset() { *x = ProviderCredentialRefreshOutput{} - mi := &file_openshell_proto_msgTypes[74] + mi := &file_openshell_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5414,7 +5557,7 @@ func (x *ProviderCredentialRefreshOutput) String() string { func (*ProviderCredentialRefreshOutput) ProtoMessage() {} func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[74] + mi := &file_openshell_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5427,7 +5570,7 @@ func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefreshOutput.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshOutput) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{74} + return file_openshell_proto_rawDescGZIP(), []int{75} } func (x *ProviderCredentialRefreshOutput) GetOutput() string { @@ -5459,7 +5602,7 @@ type ProviderCredentialRefresh struct { func (x *ProviderCredentialRefresh) Reset() { *x = ProviderCredentialRefresh{} - mi := &file_openshell_proto_msgTypes[75] + mi := &file_openshell_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5471,7 +5614,7 @@ func (x *ProviderCredentialRefresh) String() string { func (*ProviderCredentialRefresh) ProtoMessage() {} func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[75] + mi := &file_openshell_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5484,7 +5627,7 @@ func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefresh.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefresh) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{75} + return file_openshell_proto_rawDescGZIP(), []int{76} } func (x *ProviderCredentialRefresh) GetStrategy() ProviderCredentialRefreshStrategy { @@ -5553,7 +5696,7 @@ type ProviderCredentialRefreshStatus struct { func (x *ProviderCredentialRefreshStatus) Reset() { *x = ProviderCredentialRefreshStatus{} - mi := &file_openshell_proto_msgTypes[76] + mi := &file_openshell_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5565,7 +5708,7 @@ func (x *ProviderCredentialRefreshStatus) String() string { func (*ProviderCredentialRefreshStatus) ProtoMessage() {} func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[76] + mi := &file_openshell_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5578,7 +5721,7 @@ func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderCredentialRefreshStatus.ProtoReflect.Descriptor instead. func (*ProviderCredentialRefreshStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{76} + return file_openshell_proto_rawDescGZIP(), []int{77} } func (x *ProviderCredentialRefreshStatus) GetProviderName() string { @@ -5655,7 +5798,7 @@ type ProviderProfileDiscovery struct { func (x *ProviderProfileDiscovery) Reset() { *x = ProviderProfileDiscovery{} - mi := &file_openshell_proto_msgTypes[77] + mi := &file_openshell_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5667,7 +5810,7 @@ func (x *ProviderProfileDiscovery) String() string { func (*ProviderProfileDiscovery) ProtoMessage() {} func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[77] + mi := &file_openshell_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5680,7 +5823,7 @@ func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileDiscovery.ProtoReflect.Descriptor instead. func (*ProviderProfileDiscovery) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{77} + return file_openshell_proto_rawDescGZIP(), []int{78} } func (x *ProviderProfileDiscovery) GetCredentials() []string { @@ -5719,7 +5862,7 @@ type StoredProviderCredentialRefreshState struct { func (x *StoredProviderCredentialRefreshState) Reset() { *x = StoredProviderCredentialRefreshState{} - mi := &file_openshell_proto_msgTypes[78] + mi := &file_openshell_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5731,7 +5874,7 @@ func (x *StoredProviderCredentialRefreshState) String() string { func (*StoredProviderCredentialRefreshState) ProtoMessage() {} func (x *StoredProviderCredentialRefreshState) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[78] + mi := &file_openshell_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5744,7 +5887,7 @@ func (x *StoredProviderCredentialRefreshState) ProtoReflect() protoreflect.Messa // Deprecated: Use StoredProviderCredentialRefreshState.ProtoReflect.Descriptor instead. func (*StoredProviderCredentialRefreshState) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{78} + return file_openshell_proto_rawDescGZIP(), []int{79} } func (x *StoredProviderCredentialRefreshState) GetMetadata() *datamodelv1.ObjectMeta { @@ -5878,7 +6021,7 @@ type GetProviderRefreshStatusRequest struct { func (x *GetProviderRefreshStatusRequest) Reset() { *x = GetProviderRefreshStatusRequest{} - mi := &file_openshell_proto_msgTypes[79] + mi := &file_openshell_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5890,7 +6033,7 @@ func (x *GetProviderRefreshStatusRequest) String() string { func (*GetProviderRefreshStatusRequest) ProtoMessage() {} func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[79] + mi := &file_openshell_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5903,7 +6046,7 @@ func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusRequest.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{79} + return file_openshell_proto_rawDescGZIP(), []int{80} } func (x *GetProviderRefreshStatusRequest) GetProvider() string { @@ -5936,7 +6079,7 @@ type GetProviderRefreshStatusResponse struct { func (x *GetProviderRefreshStatusResponse) Reset() { *x = GetProviderRefreshStatusResponse{} - mi := &file_openshell_proto_msgTypes[80] + mi := &file_openshell_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5948,7 +6091,7 @@ func (x *GetProviderRefreshStatusResponse) String() string { func (*GetProviderRefreshStatusResponse) ProtoMessage() {} func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[80] + mi := &file_openshell_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5961,7 +6104,7 @@ func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusResponse.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{80} + return file_openshell_proto_rawDescGZIP(), []int{81} } func (x *GetProviderRefreshStatusResponse) GetCredentials() []*ProviderCredentialRefreshStatus { @@ -5987,7 +6130,7 @@ type ConfigureProviderRefreshRequest struct { func (x *ConfigureProviderRefreshRequest) Reset() { *x = ConfigureProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5999,7 +6142,7 @@ func (x *ConfigureProviderRefreshRequest) String() string { func (*ConfigureProviderRefreshRequest) ProtoMessage() {} func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6012,7 +6155,7 @@ func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{81} + return file_openshell_proto_rawDescGZIP(), []int{82} } func (x *ConfigureProviderRefreshRequest) GetProvider() string { @@ -6073,7 +6216,7 @@ type ConfigureProviderRefreshResponse struct { func (x *ConfigureProviderRefreshResponse) Reset() { *x = ConfigureProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6085,7 +6228,7 @@ func (x *ConfigureProviderRefreshResponse) String() string { func (*ConfigureProviderRefreshResponse) ProtoMessage() {} func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6098,7 +6241,7 @@ func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{82} + return file_openshell_proto_rawDescGZIP(), []int{83} } func (x *ConfigureProviderRefreshResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -6120,7 +6263,7 @@ type RotateProviderCredentialRequest struct { func (x *RotateProviderCredentialRequest) Reset() { *x = RotateProviderCredentialRequest{} - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6132,7 +6275,7 @@ func (x *RotateProviderCredentialRequest) String() string { func (*RotateProviderCredentialRequest) ProtoMessage() {} func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6145,7 +6288,7 @@ func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialRequest.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{83} + return file_openshell_proto_rawDescGZIP(), []int{84} } func (x *RotateProviderCredentialRequest) GetProvider() string { @@ -6178,7 +6321,7 @@ type RotateProviderCredentialResponse struct { func (x *RotateProviderCredentialResponse) Reset() { *x = RotateProviderCredentialResponse{} - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6190,7 +6333,7 @@ func (x *RotateProviderCredentialResponse) String() string { func (*RotateProviderCredentialResponse) ProtoMessage() {} func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6203,7 +6346,7 @@ func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialResponse.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{84} + return file_openshell_proto_rawDescGZIP(), []int{85} } func (x *RotateProviderCredentialResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -6225,7 +6368,7 @@ type DeleteProviderRefreshRequest struct { func (x *DeleteProviderRefreshRequest) Reset() { *x = DeleteProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6237,7 +6380,7 @@ func (x *DeleteProviderRefreshRequest) String() string { func (*DeleteProviderRefreshRequest) ProtoMessage() {} func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6250,7 +6393,7 @@ func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{85} + return file_openshell_proto_rawDescGZIP(), []int{86} } func (x *DeleteProviderRefreshRequest) GetProvider() string { @@ -6283,7 +6426,7 @@ type DeleteProviderRefreshResponse struct { func (x *DeleteProviderRefreshResponse) Reset() { *x = DeleteProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6295,7 +6438,7 @@ func (x *DeleteProviderRefreshResponse) String() string { func (*DeleteProviderRefreshResponse) ProtoMessage() {} func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6308,7 +6451,7 @@ func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{86} + return file_openshell_proto_rawDescGZIP(), []int{87} } func (x *DeleteProviderRefreshResponse) GetDeleted() bool { @@ -6348,7 +6491,7 @@ type ProviderProfile struct { func (x *ProviderProfile) Reset() { *x = ProviderProfile{} - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6360,7 +6503,7 @@ func (x *ProviderProfile) String() string { func (*ProviderProfile) ProtoMessage() {} func (x *ProviderProfile) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6373,7 +6516,7 @@ func (x *ProviderProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfile.ProtoReflect.Descriptor instead. func (*ProviderProfile) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{87} + return file_openshell_proto_rawDescGZIP(), []int{88} } func (x *ProviderProfile) GetId() string { @@ -6478,7 +6621,7 @@ type StoredProviderProfile struct { func (x *StoredProviderProfile) Reset() { *x = StoredProviderProfile{} - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6490,7 +6633,7 @@ func (x *StoredProviderProfile) String() string { func (*StoredProviderProfile) ProtoMessage() {} func (x *StoredProviderProfile) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6503,7 +6646,7 @@ func (x *StoredProviderProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredProviderProfile.ProtoReflect.Descriptor instead. func (*StoredProviderProfile) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{88} + return file_openshell_proto_rawDescGZIP(), []int{89} } func (x *StoredProviderProfile) GetMetadata() *datamodelv1.ObjectMeta { @@ -6530,7 +6673,7 @@ type ProviderProfileResponse struct { func (x *ProviderProfileResponse) Reset() { *x = ProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6542,7 +6685,7 @@ func (x *ProviderProfileResponse) String() string { func (*ProviderProfileResponse) ProtoMessage() {} func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6555,7 +6698,7 @@ func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileResponse.ProtoReflect.Descriptor instead. func (*ProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{89} + return file_openshell_proto_rawDescGZIP(), []int{90} } func (x *ProviderProfileResponse) GetProfile() *ProviderProfile { @@ -6575,7 +6718,7 @@ type ListProviderProfilesResponse struct { func (x *ListProviderProfilesResponse) Reset() { *x = ListProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6587,7 +6730,7 @@ func (x *ListProviderProfilesResponse) String() string { func (*ListProviderProfilesResponse) ProtoMessage() {} func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6600,7 +6743,7 @@ func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ListProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{90} + return file_openshell_proto_rawDescGZIP(), []int{91} } func (x *ListProviderProfilesResponse) GetProfiles() []*ProviderProfile { @@ -6623,7 +6766,7 @@ type ImportProviderProfilesRequest struct { func (x *ImportProviderProfilesRequest) Reset() { *x = ImportProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6635,7 +6778,7 @@ func (x *ImportProviderProfilesRequest) String() string { func (*ImportProviderProfilesRequest) ProtoMessage() {} func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6648,7 +6791,7 @@ func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{91} + return file_openshell_proto_rawDescGZIP(), []int{92} } func (x *ImportProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { @@ -6677,7 +6820,7 @@ type ImportProviderProfilesResponse struct { func (x *ImportProviderProfilesResponse) Reset() { *x = ImportProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6689,7 +6832,7 @@ func (x *ImportProviderProfilesResponse) String() string { func (*ImportProviderProfilesResponse) ProtoMessage() {} func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6702,7 +6845,7 @@ func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{92} + return file_openshell_proto_rawDescGZIP(), []int{93} } func (x *ImportProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -6746,7 +6889,7 @@ type UpdateProviderProfilesRequest struct { func (x *UpdateProviderProfilesRequest) Reset() { *x = UpdateProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6758,7 +6901,7 @@ func (x *UpdateProviderProfilesRequest) String() string { func (*UpdateProviderProfilesRequest) ProtoMessage() {} func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6771,7 +6914,7 @@ func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{93} + return file_openshell_proto_rawDescGZIP(), []int{94} } func (x *UpdateProviderProfilesRequest) GetProfile() *ProviderProfileImportItem { @@ -6814,7 +6957,7 @@ type UpdateProviderProfilesResponse struct { func (x *UpdateProviderProfilesResponse) Reset() { *x = UpdateProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6826,7 +6969,7 @@ func (x *UpdateProviderProfilesResponse) String() string { func (*UpdateProviderProfilesResponse) ProtoMessage() {} func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6839,7 +6982,7 @@ func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{94} + return file_openshell_proto_rawDescGZIP(), []int{95} } func (x *UpdateProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -6876,7 +7019,7 @@ type LintProviderProfilesRequest struct { func (x *LintProviderProfilesRequest) Reset() { *x = LintProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6888,7 +7031,7 @@ func (x *LintProviderProfilesRequest) String() string { func (*LintProviderProfilesRequest) ProtoMessage() {} func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6901,7 +7044,7 @@ func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*LintProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{95} + return file_openshell_proto_rawDescGZIP(), []int{96} } func (x *LintProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { @@ -6929,7 +7072,7 @@ type LintProviderProfilesResponse struct { func (x *LintProviderProfilesResponse) Reset() { *x = LintProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6941,7 +7084,7 @@ func (x *LintProviderProfilesResponse) String() string { func (*LintProviderProfilesResponse) ProtoMessage() {} func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6954,7 +7097,7 @@ func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*LintProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{96} + return file_openshell_proto_rawDescGZIP(), []int{97} } func (x *LintProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -6981,7 +7124,7 @@ type DeleteProviderResponse struct { func (x *DeleteProviderResponse) Reset() { *x = DeleteProviderResponse{} - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6993,7 +7136,7 @@ func (x *DeleteProviderResponse) String() string { func (*DeleteProviderResponse) ProtoMessage() {} func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7006,7 +7149,7 @@ func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{97} + return file_openshell_proto_rawDescGZIP(), []int{98} } func (x *DeleteProviderResponse) GetDeleted() bool { @@ -7029,7 +7172,7 @@ type DeleteProviderProfileRequest struct { func (x *DeleteProviderProfileRequest) Reset() { *x = DeleteProviderProfileRequest{} - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7041,7 +7184,7 @@ func (x *DeleteProviderProfileRequest) String() string { func (*DeleteProviderProfileRequest) ProtoMessage() {} func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7054,7 +7197,7 @@ func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{98} + return file_openshell_proto_rawDescGZIP(), []int{99} } func (x *DeleteProviderProfileRequest) GetId() string { @@ -7081,7 +7224,7 @@ type DeleteProviderProfileResponse struct { func (x *DeleteProviderProfileResponse) Reset() { *x = DeleteProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7093,7 +7236,7 @@ func (x *DeleteProviderProfileResponse) String() string { func (*DeleteProviderProfileResponse) ProtoMessage() {} func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7106,7 +7249,7 @@ func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{99} + return file_openshell_proto_rawDescGZIP(), []int{100} } func (x *DeleteProviderProfileResponse) GetDeleted() bool { @@ -7131,7 +7274,7 @@ type GetSandboxProviderEnvironmentRequest struct { func (x *GetSandboxProviderEnvironmentRequest) Reset() { *x = GetSandboxProviderEnvironmentRequest{} - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7143,7 +7286,7 @@ func (x *GetSandboxProviderEnvironmentRequest) String() string { func (*GetSandboxProviderEnvironmentRequest) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7156,7 +7299,7 @@ func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use GetSandboxProviderEnvironmentRequest.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{100} + return file_openshell_proto_rawDescGZIP(), []int{101} } func (x *GetSandboxProviderEnvironmentRequest) GetSandboxId() string { @@ -7185,7 +7328,7 @@ type StaticCredentialEndpointBinding struct { func (x *StaticCredentialEndpointBinding) Reset() { *x = StaticCredentialEndpointBinding{} - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7197,7 +7340,7 @@ func (x *StaticCredentialEndpointBinding) String() string { func (*StaticCredentialEndpointBinding) ProtoMessage() {} func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7210,7 +7353,7 @@ func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredentialEndpointBinding.ProtoReflect.Descriptor instead. func (*StaticCredentialEndpointBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{101} + return file_openshell_proto_rawDescGZIP(), []int{102} } func (x *StaticCredentialEndpointBinding) GetHost() string { @@ -7248,7 +7391,7 @@ type StaticCredentialBinding struct { func (x *StaticCredentialBinding) Reset() { *x = StaticCredentialBinding{} - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7260,7 +7403,7 @@ func (x *StaticCredentialBinding) String() string { func (*StaticCredentialBinding) ProtoMessage() {} func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7273,7 +7416,7 @@ func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredentialBinding.ProtoReflect.Descriptor instead. func (*StaticCredentialBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{102} + return file_openshell_proto_rawDescGZIP(), []int{103} } func (x *StaticCredentialBinding) GetEndpoints() []*StaticCredentialEndpointBinding { @@ -7317,7 +7460,7 @@ type GetSandboxProviderEnvironmentResponse struct { func (x *GetSandboxProviderEnvironmentResponse) Reset() { *x = GetSandboxProviderEnvironmentResponse{} - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7329,7 +7472,7 @@ func (x *GetSandboxProviderEnvironmentResponse) String() string { func (*GetSandboxProviderEnvironmentResponse) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7342,7 +7485,7 @@ func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Mess // Deprecated: Use GetSandboxProviderEnvironmentResponse.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{103} + return file_openshell_proto_rawDescGZIP(), []int{104} } func (x *GetSandboxProviderEnvironmentResponse) GetEnvironment() map[string]string { @@ -7387,6 +7530,139 @@ func (x *GetSandboxProviderEnvironmentResponse) GetNonSecretEnvironmentKeys() [] return nil } +type ExchangeProviderSubjectTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The sandbox ID. Must match the authenticated sandbox principal. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Attached provider record holding the configured subject token credential. + Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + // Provider profile credential that declares the token_exchange grant. + CredentialKey string `protobuf:"bytes,3,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + // Supervisor JWT-SVID. The gateway verifies this and uses its `sub` claim + // as the requested audience for the intermediate token. + SupervisorJwtSvid string `protobuf:"bytes,4,opt,name=supervisor_jwt_svid,json=supervisorJwtSvid,proto3" json:"supervisor_jwt_svid,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExchangeProviderSubjectTokenRequest) Reset() { + *x = ExchangeProviderSubjectTokenRequest{} + mi := &file_openshell_proto_msgTypes[105] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExchangeProviderSubjectTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExchangeProviderSubjectTokenRequest) ProtoMessage() {} + +func (x *ExchangeProviderSubjectTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[105] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExchangeProviderSubjectTokenRequest.ProtoReflect.Descriptor instead. +func (*ExchangeProviderSubjectTokenRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{105} +} + +func (x *ExchangeProviderSubjectTokenRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *ExchangeProviderSubjectTokenRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *ExchangeProviderSubjectTokenRequest) GetCredentialKey() string { + if x != nil { + return x.CredentialKey + } + return "" +} + +func (x *ExchangeProviderSubjectTokenRequest) GetSupervisorJwtSvid() string { + if x != nil { + return x.SupervisorJwtSvid + } + return "" +} + +type ExchangeProviderSubjectTokenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + ExpiresIn int64 `protobuf:"varint,2,opt,name=expires_in,json=expiresIn,proto3" json:"expires_in,omitempty"` + TokenType string `protobuf:"bytes,3,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExchangeProviderSubjectTokenResponse) Reset() { + *x = ExchangeProviderSubjectTokenResponse{} + mi := &file_openshell_proto_msgTypes[106] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExchangeProviderSubjectTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExchangeProviderSubjectTokenResponse) ProtoMessage() {} + +func (x *ExchangeProviderSubjectTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[106] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExchangeProviderSubjectTokenResponse.ProtoReflect.Descriptor instead. +func (*ExchangeProviderSubjectTokenResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{106} +} + +func (x *ExchangeProviderSubjectTokenResponse) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *ExchangeProviderSubjectTokenResponse) GetExpiresIn() int64 { + if x != nil { + return x.ExpiresIn + } + return 0 +} + +func (x *ExchangeProviderSubjectTokenResponse) GetTokenType() string { + if x != nil { + return x.TokenType + } + return "" +} + // Update sandbox policy request. type UpdateConfigRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -7434,7 +7710,7 @@ type UpdateConfigRequest struct { func (x *UpdateConfigRequest) Reset() { *x = UpdateConfigRequest{} - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7446,7 +7722,7 @@ func (x *UpdateConfigRequest) String() string { func (*UpdateConfigRequest) ProtoMessage() {} func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7459,7 +7735,7 @@ func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigRequest.ProtoReflect.Descriptor instead. func (*UpdateConfigRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{104} + return file_openshell_proto_rawDescGZIP(), []int{107} } func (x *UpdateConfigRequest) GetName() string { @@ -7549,7 +7825,7 @@ type PolicyMergeOperation struct { func (x *PolicyMergeOperation) Reset() { *x = PolicyMergeOperation{} - mi := &file_openshell_proto_msgTypes[105] + mi := &file_openshell_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7561,7 +7837,7 @@ func (x *PolicyMergeOperation) String() string { func (*PolicyMergeOperation) ProtoMessage() {} func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[105] + mi := &file_openshell_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7574,7 +7850,7 @@ func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyMergeOperation.ProtoReflect.Descriptor instead. func (*PolicyMergeOperation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{105} + return file_openshell_proto_rawDescGZIP(), []int{108} } func (x *PolicyMergeOperation) GetOperation() isPolicyMergeOperation_Operation { @@ -7688,7 +7964,7 @@ type AddNetworkRule struct { func (x *AddNetworkRule) Reset() { *x = AddNetworkRule{} - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7700,7 +7976,7 @@ func (x *AddNetworkRule) String() string { func (*AddNetworkRule) ProtoMessage() {} func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7713,7 +7989,7 @@ func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use AddNetworkRule.ProtoReflect.Descriptor instead. func (*AddNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{106} + return file_openshell_proto_rawDescGZIP(), []int{109} } func (x *AddNetworkRule) GetRuleName() string { @@ -7741,7 +8017,7 @@ type RemoveNetworkEndpoint struct { func (x *RemoveNetworkEndpoint) Reset() { *x = RemoveNetworkEndpoint{} - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7753,7 +8029,7 @@ func (x *RemoveNetworkEndpoint) String() string { func (*RemoveNetworkEndpoint) ProtoMessage() {} func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7766,7 +8042,7 @@ func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkEndpoint.ProtoReflect.Descriptor instead. func (*RemoveNetworkEndpoint) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{107} + return file_openshell_proto_rawDescGZIP(), []int{110} } func (x *RemoveNetworkEndpoint) GetRuleName() string { @@ -7799,7 +8075,7 @@ type RemoveNetworkRule struct { func (x *RemoveNetworkRule) Reset() { *x = RemoveNetworkRule{} - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7811,7 +8087,7 @@ func (x *RemoveNetworkRule) String() string { func (*RemoveNetworkRule) ProtoMessage() {} func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7824,7 +8100,7 @@ func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkRule.ProtoReflect.Descriptor instead. func (*RemoveNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{108} + return file_openshell_proto_rawDescGZIP(), []int{111} } func (x *RemoveNetworkRule) GetRuleName() string { @@ -7845,7 +8121,7 @@ type AddDenyRules struct { func (x *AddDenyRules) Reset() { *x = AddDenyRules{} - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7857,7 +8133,7 @@ func (x *AddDenyRules) String() string { func (*AddDenyRules) ProtoMessage() {} func (x *AddDenyRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7870,7 +8146,7 @@ func (x *AddDenyRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddDenyRules.ProtoReflect.Descriptor instead. func (*AddDenyRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{109} + return file_openshell_proto_rawDescGZIP(), []int{112} } func (x *AddDenyRules) GetHost() string { @@ -7905,7 +8181,7 @@ type AddAllowRules struct { func (x *AddAllowRules) Reset() { *x = AddAllowRules{} - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7917,7 +8193,7 @@ func (x *AddAllowRules) String() string { func (*AddAllowRules) ProtoMessage() {} func (x *AddAllowRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7930,7 +8206,7 @@ func (x *AddAllowRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddAllowRules.ProtoReflect.Descriptor instead. func (*AddAllowRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{110} + return file_openshell_proto_rawDescGZIP(), []int{113} } func (x *AddAllowRules) GetHost() string { @@ -7964,7 +8240,7 @@ type RemoveNetworkBinary struct { func (x *RemoveNetworkBinary) Reset() { *x = RemoveNetworkBinary{} - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7976,7 +8252,7 @@ func (x *RemoveNetworkBinary) String() string { func (*RemoveNetworkBinary) ProtoMessage() {} func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7989,7 +8265,7 @@ func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkBinary.ProtoReflect.Descriptor instead. func (*RemoveNetworkBinary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{111} + return file_openshell_proto_rawDescGZIP(), []int{114} } func (x *RemoveNetworkBinary) GetRuleName() string { @@ -8025,7 +8301,7 @@ type UpdateConfigResponse struct { func (x *UpdateConfigResponse) Reset() { *x = UpdateConfigResponse{} - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8037,7 +8313,7 @@ func (x *UpdateConfigResponse) String() string { func (*UpdateConfigResponse) ProtoMessage() {} func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8050,7 +8326,7 @@ func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigResponse.ProtoReflect.Descriptor instead. func (*UpdateConfigResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{112} + return file_openshell_proto_rawDescGZIP(), []int{115} } func (x *UpdateConfigResponse) GetVersion() uint32 { @@ -8105,7 +8381,7 @@ type GetSandboxPolicyStatusRequest struct { func (x *GetSandboxPolicyStatusRequest) Reset() { *x = GetSandboxPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8117,7 +8393,7 @@ func (x *GetSandboxPolicyStatusRequest) String() string { func (*GetSandboxPolicyStatusRequest) ProtoMessage() {} func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8130,7 +8406,7 @@ func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{113} + return file_openshell_proto_rawDescGZIP(), []int{116} } func (x *GetSandboxPolicyStatusRequest) GetName() string { @@ -8174,7 +8450,7 @@ type GetSandboxPolicyStatusResponse struct { func (x *GetSandboxPolicyStatusResponse) Reset() { *x = GetSandboxPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8186,7 +8462,7 @@ func (x *GetSandboxPolicyStatusResponse) String() string { func (*GetSandboxPolicyStatusResponse) ProtoMessage() {} func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8199,7 +8475,7 @@ func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{114} + return file_openshell_proto_rawDescGZIP(), []int{117} } func (x *GetSandboxPolicyStatusResponse) GetRevision() *SandboxPolicyRevision { @@ -8233,7 +8509,7 @@ type ListSandboxPoliciesRequest struct { func (x *ListSandboxPoliciesRequest) Reset() { *x = ListSandboxPoliciesRequest{} - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8245,7 +8521,7 @@ func (x *ListSandboxPoliciesRequest) String() string { func (*ListSandboxPoliciesRequest) ProtoMessage() {} func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8258,7 +8534,7 @@ func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesRequest.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{115} + return file_openshell_proto_rawDescGZIP(), []int{118} } func (x *ListSandboxPoliciesRequest) GetName() string { @@ -8306,7 +8582,7 @@ type ListSandboxPoliciesResponse struct { func (x *ListSandboxPoliciesResponse) Reset() { *x = ListSandboxPoliciesResponse{} - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8318,7 +8594,7 @@ func (x *ListSandboxPoliciesResponse) String() string { func (*ListSandboxPoliciesResponse) ProtoMessage() {} func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8331,7 +8607,7 @@ func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesResponse.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{116} + return file_openshell_proto_rawDescGZIP(), []int{119} } func (x *ListSandboxPoliciesResponse) GetRevisions() []*SandboxPolicyRevision { @@ -8358,7 +8634,7 @@ type ReportPolicyStatusRequest struct { func (x *ReportPolicyStatusRequest) Reset() { *x = ReportPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8370,7 +8646,7 @@ func (x *ReportPolicyStatusRequest) String() string { func (*ReportPolicyStatusRequest) ProtoMessage() {} func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8383,7 +8659,7 @@ func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{117} + return file_openshell_proto_rawDescGZIP(), []int{120} } func (x *ReportPolicyStatusRequest) GetSandboxId() string { @@ -8423,7 +8699,7 @@ type ReportPolicyStatusResponse struct { func (x *ReportPolicyStatusResponse) Reset() { *x = ReportPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8435,7 +8711,7 @@ func (x *ReportPolicyStatusResponse) String() string { func (*ReportPolicyStatusResponse) ProtoMessage() {} func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8448,7 +8724,7 @@ func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{118} + return file_openshell_proto_rawDescGZIP(), []int{121} } // A versioned policy revision with metadata. @@ -8476,7 +8752,7 @@ type SandboxPolicyRevision struct { func (x *SandboxPolicyRevision) Reset() { *x = SandboxPolicyRevision{} - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8488,7 +8764,7 @@ func (x *SandboxPolicyRevision) String() string { func (*SandboxPolicyRevision) ProtoMessage() {} func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8501,7 +8777,7 @@ func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxPolicyRevision.ProtoReflect.Descriptor instead. func (*SandboxPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{119} + return file_openshell_proto_rawDescGZIP(), []int{122} } func (x *SandboxPolicyRevision) GetVersion() uint32 { @@ -8581,7 +8857,7 @@ type GetSandboxLogsRequest struct { func (x *GetSandboxLogsRequest) Reset() { *x = GetSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8593,7 +8869,7 @@ func (x *GetSandboxLogsRequest) String() string { func (*GetSandboxLogsRequest) ProtoMessage() {} func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8606,7 +8882,7 @@ func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*GetSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{120} + return file_openshell_proto_rawDescGZIP(), []int{123} } func (x *GetSandboxLogsRequest) GetSandboxId() string { @@ -8664,7 +8940,7 @@ type PushSandboxLogsRequest struct { func (x *PushSandboxLogsRequest) Reset() { *x = PushSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8676,7 +8952,7 @@ func (x *PushSandboxLogsRequest) String() string { func (*PushSandboxLogsRequest) ProtoMessage() {} func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8689,7 +8965,7 @@ func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*PushSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{121} + return file_openshell_proto_rawDescGZIP(), []int{124} } func (x *PushSandboxLogsRequest) GetSandboxId() string { @@ -8715,7 +8991,7 @@ type PushSandboxLogsResponse struct { func (x *PushSandboxLogsResponse) Reset() { *x = PushSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8727,7 +9003,7 @@ func (x *PushSandboxLogsResponse) String() string { func (*PushSandboxLogsResponse) ProtoMessage() {} func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8740,7 +9016,7 @@ func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*PushSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{122} + return file_openshell_proto_rawDescGZIP(), []int{125} } // Get sandbox logs response. @@ -8756,7 +9032,7 @@ type GetSandboxLogsResponse struct { func (x *GetSandboxLogsResponse) Reset() { *x = GetSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8768,7 +9044,7 @@ func (x *GetSandboxLogsResponse) String() string { func (*GetSandboxLogsResponse) ProtoMessage() {} func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8781,7 +9057,7 @@ func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*GetSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{123} + return file_openshell_proto_rawDescGZIP(), []int{126} } func (x *GetSandboxLogsResponse) GetLogs() []*SandboxLogLine { @@ -8814,7 +9090,7 @@ type SupervisorMessage struct { func (x *SupervisorMessage) Reset() { *x = SupervisorMessage{} - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8826,7 +9102,7 @@ func (x *SupervisorMessage) String() string { func (*SupervisorMessage) ProtoMessage() {} func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8839,7 +9115,7 @@ func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorMessage.ProtoReflect.Descriptor instead. func (*SupervisorMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{124} + return file_openshell_proto_rawDescGZIP(), []int{127} } func (x *SupervisorMessage) GetPayload() isSupervisorMessage_Payload { @@ -8930,7 +9206,7 @@ type GatewayMessage struct { func (x *GatewayMessage) Reset() { *x = GatewayMessage{} - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8942,7 +9218,7 @@ func (x *GatewayMessage) String() string { func (*GatewayMessage) ProtoMessage() {} func (x *GatewayMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8955,7 +9231,7 @@ func (x *GatewayMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayMessage.ProtoReflect.Descriptor instead. func (*GatewayMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{125} + return file_openshell_proto_rawDescGZIP(), []int{128} } func (x *GatewayMessage) GetPayload() isGatewayMessage_Payload { @@ -9057,7 +9333,7 @@ type SupervisorHello struct { func (x *SupervisorHello) Reset() { *x = SupervisorHello{} - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9069,7 +9345,7 @@ func (x *SupervisorHello) String() string { func (*SupervisorHello) ProtoMessage() {} func (x *SupervisorHello) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9082,7 +9358,7 @@ func (x *SupervisorHello) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHello.ProtoReflect.Descriptor instead. func (*SupervisorHello) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{126} + return file_openshell_proto_rawDescGZIP(), []int{129} } func (x *SupervisorHello) GetSandboxId() string { @@ -9112,7 +9388,7 @@ type SessionAccepted struct { func (x *SessionAccepted) Reset() { *x = SessionAccepted{} - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9124,7 +9400,7 @@ func (x *SessionAccepted) String() string { func (*SessionAccepted) ProtoMessage() {} func (x *SessionAccepted) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9137,7 +9413,7 @@ func (x *SessionAccepted) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionAccepted.ProtoReflect.Descriptor instead. func (*SessionAccepted) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{127} + return file_openshell_proto_rawDescGZIP(), []int{130} } func (x *SessionAccepted) GetSessionId() string { @@ -9165,7 +9441,7 @@ type SessionRejected struct { func (x *SessionRejected) Reset() { *x = SessionRejected{} - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9177,7 +9453,7 @@ func (x *SessionRejected) String() string { func (*SessionRejected) ProtoMessage() {} func (x *SessionRejected) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9190,7 +9466,7 @@ func (x *SessionRejected) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRejected.ProtoReflect.Descriptor instead. func (*SessionRejected) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{128} + return file_openshell_proto_rawDescGZIP(), []int{131} } func (x *SessionRejected) GetReason() string { @@ -9209,7 +9485,7 @@ type SupervisorHeartbeat struct { func (x *SupervisorHeartbeat) Reset() { *x = SupervisorHeartbeat{} - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9221,7 +9497,7 @@ func (x *SupervisorHeartbeat) String() string { func (*SupervisorHeartbeat) ProtoMessage() {} func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9234,7 +9510,7 @@ func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHeartbeat.ProtoReflect.Descriptor instead. func (*SupervisorHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{129} + return file_openshell_proto_rawDescGZIP(), []int{132} } // Gateway heartbeat. @@ -9246,7 +9522,7 @@ type GatewayHeartbeat struct { func (x *GatewayHeartbeat) Reset() { *x = GatewayHeartbeat{} - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9258,7 +9534,7 @@ func (x *GatewayHeartbeat) String() string { func (*GatewayHeartbeat) ProtoMessage() {} func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9271,7 +9547,7 @@ func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayHeartbeat.ProtoReflect.Descriptor instead. func (*GatewayHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{130} + return file_openshell_proto_rawDescGZIP(), []int{133} } // Gateway requests the supervisor to open a relay channel. @@ -9300,7 +9576,7 @@ type RelayOpen struct { func (x *RelayOpen) Reset() { *x = RelayOpen{} - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9312,7 +9588,7 @@ func (x *RelayOpen) String() string { func (*RelayOpen) ProtoMessage() {} func (x *RelayOpen) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9325,7 +9601,7 @@ func (x *RelayOpen) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpen.ProtoReflect.Descriptor instead. func (*RelayOpen) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{131} + return file_openshell_proto_rawDescGZIP(), []int{134} } func (x *RelayOpen) GetChannelId() string { @@ -9392,7 +9668,7 @@ type SshRelayTarget struct { func (x *SshRelayTarget) Reset() { *x = SshRelayTarget{} - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9404,7 +9680,7 @@ func (x *SshRelayTarget) String() string { func (*SshRelayTarget) ProtoMessage() {} func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9417,7 +9693,7 @@ func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use SshRelayTarget.ProtoReflect.Descriptor instead. func (*SshRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{132} + return file_openshell_proto_rawDescGZIP(), []int{135} } // TCP target dialed by the supervisor from inside the sandbox. @@ -9433,7 +9709,7 @@ type TcpRelayTarget struct { func (x *TcpRelayTarget) Reset() { *x = TcpRelayTarget{} - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[136] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9445,7 +9721,7 @@ func (x *TcpRelayTarget) String() string { func (*TcpRelayTarget) ProtoMessage() {} func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[136] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9458,7 +9734,7 @@ func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpRelayTarget.ProtoReflect.Descriptor instead. func (*TcpRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{133} + return file_openshell_proto_rawDescGZIP(), []int{136} } func (x *TcpRelayTarget) GetHost() string { @@ -9486,7 +9762,7 @@ type RelayInit struct { func (x *RelayInit) Reset() { *x = RelayInit{} - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[137] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9498,7 +9774,7 @@ func (x *RelayInit) String() string { func (*RelayInit) ProtoMessage() {} func (x *RelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[137] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9511,7 +9787,7 @@ func (x *RelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayInit.ProtoReflect.Descriptor instead. func (*RelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{134} + return file_openshell_proto_rawDescGZIP(), []int{137} } func (x *RelayInit) GetChannelId() string { @@ -9538,7 +9814,7 @@ type RelayFrame struct { func (x *RelayFrame) Reset() { *x = RelayFrame{} - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[138] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9550,7 +9826,7 @@ func (x *RelayFrame) String() string { func (*RelayFrame) ProtoMessage() {} func (x *RelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[138] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9563,7 +9839,7 @@ func (x *RelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayFrame.ProtoReflect.Descriptor instead. func (*RelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{135} + return file_openshell_proto_rawDescGZIP(), []int{138} } func (x *RelayFrame) GetPayload() isRelayFrame_Payload { @@ -9622,7 +9898,7 @@ type RelayOpenResult struct { func (x *RelayOpenResult) Reset() { *x = RelayOpenResult{} - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[139] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9634,7 +9910,7 @@ func (x *RelayOpenResult) String() string { func (*RelayOpenResult) ProtoMessage() {} func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[139] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9647,7 +9923,7 @@ func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpenResult.ProtoReflect.Descriptor instead. func (*RelayOpenResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{136} + return file_openshell_proto_rawDescGZIP(), []int{139} } func (x *RelayOpenResult) GetChannelId() string { @@ -9684,7 +9960,7 @@ type RelayClose struct { func (x *RelayClose) Reset() { *x = RelayClose{} - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[140] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9696,7 +9972,7 @@ func (x *RelayClose) String() string { func (*RelayClose) ProtoMessage() {} func (x *RelayClose) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[140] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9709,7 +9985,7 @@ func (x *RelayClose) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayClose.ProtoReflect.Descriptor instead. func (*RelayClose) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{137} + return file_openshell_proto_rawDescGZIP(), []int{140} } func (x *RelayClose) GetChannelId() string { @@ -9743,7 +10019,7 @@ type L7RequestSample struct { func (x *L7RequestSample) Reset() { *x = L7RequestSample{} - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9755,7 +10031,7 @@ func (x *L7RequestSample) String() string { func (*L7RequestSample) ProtoMessage() {} func (x *L7RequestSample) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9768,7 +10044,7 @@ func (x *L7RequestSample) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RequestSample.ProtoReflect.Descriptor instead. func (*L7RequestSample) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{138} + return file_openshell_proto_rawDescGZIP(), []int{141} } func (x *L7RequestSample) GetMethod() string { @@ -9842,7 +10118,7 @@ type DenialSummary struct { func (x *DenialSummary) Reset() { *x = DenialSummary{} - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9854,7 +10130,7 @@ func (x *DenialSummary) String() string { func (*DenialSummary) ProtoMessage() {} func (x *DenialSummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9867,7 +10143,7 @@ func (x *DenialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialSummary.ProtoReflect.Descriptor instead. func (*DenialSummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{139} + return file_openshell_proto_rawDescGZIP(), []int{142} } func (x *DenialSummary) GetSandboxId() string { @@ -10002,7 +10278,7 @@ type DenialGroupCount struct { func (x *DenialGroupCount) Reset() { *x = DenialGroupCount{} - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[143] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10014,7 +10290,7 @@ func (x *DenialGroupCount) String() string { func (*DenialGroupCount) ProtoMessage() {} func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[143] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10027,7 +10303,7 @@ func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialGroupCount.ProtoReflect.Descriptor instead. func (*DenialGroupCount) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{140} + return file_openshell_proto_rawDescGZIP(), []int{143} } func (x *DenialGroupCount) GetDenyGroup() string { @@ -10060,7 +10336,7 @@ type NetworkActivitySummary struct { func (x *NetworkActivitySummary) Reset() { *x = NetworkActivitySummary{} - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10072,7 +10348,7 @@ func (x *NetworkActivitySummary) String() string { func (*NetworkActivitySummary) ProtoMessage() {} func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10085,7 +10361,7 @@ func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkActivitySummary.ProtoReflect.Descriptor instead. func (*NetworkActivitySummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{141} + return file_openshell_proto_rawDescGZIP(), []int{144} } func (x *NetworkActivitySummary) GetNetworkActivityCount() uint32 { @@ -10159,7 +10435,7 @@ type PolicyChunk struct { func (x *PolicyChunk) Reset() { *x = PolicyChunk{} - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10171,7 +10447,7 @@ func (x *PolicyChunk) String() string { func (*PolicyChunk) ProtoMessage() {} func (x *PolicyChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10184,7 +10460,7 @@ func (x *PolicyChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyChunk.ProtoReflect.Descriptor instead. func (*PolicyChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{142} + return file_openshell_proto_rawDescGZIP(), []int{145} } func (x *PolicyChunk) GetId() string { @@ -10330,7 +10606,7 @@ type DraftPolicyUpdate struct { func (x *DraftPolicyUpdate) Reset() { *x = DraftPolicyUpdate{} - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10342,7 +10618,7 @@ func (x *DraftPolicyUpdate) String() string { func (*DraftPolicyUpdate) ProtoMessage() {} func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10355,7 +10631,7 @@ func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftPolicyUpdate.ProtoReflect.Descriptor instead. func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{143} + return file_openshell_proto_rawDescGZIP(), []int{146} } func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { @@ -10413,7 +10689,7 @@ type SubmitPolicyAnalysisRequest struct { func (x *SubmitPolicyAnalysisRequest) Reset() { *x = SubmitPolicyAnalysisRequest{} - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[147] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10425,7 +10701,7 @@ func (x *SubmitPolicyAnalysisRequest) String() string { func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[147] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10438,7 +10714,7 @@ func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{144} + return file_openshell_proto_rawDescGZIP(), []int{147} } func (x *SubmitPolicyAnalysisRequest) GetSummaries() []*DenialSummary { @@ -10501,7 +10777,7 @@ type SubmitPolicyAnalysisResponse struct { func (x *SubmitPolicyAnalysisResponse) Reset() { *x = SubmitPolicyAnalysisResponse{} - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[148] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10513,7 +10789,7 @@ func (x *SubmitPolicyAnalysisResponse) String() string { func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[148] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10526,7 +10802,7 @@ func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{145} + return file_openshell_proto_rawDescGZIP(), []int{148} } func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { @@ -10572,7 +10848,7 @@ type GetDraftPolicyRequest struct { func (x *GetDraftPolicyRequest) Reset() { *x = GetDraftPolicyRequest{} - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10584,7 +10860,7 @@ func (x *GetDraftPolicyRequest) String() string { func (*GetDraftPolicyRequest) ProtoMessage() {} func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10597,7 +10873,7 @@ func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyRequest.ProtoReflect.Descriptor instead. func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{146} + return file_openshell_proto_rawDescGZIP(), []int{149} } func (x *GetDraftPolicyRequest) GetName() string { @@ -10637,7 +10913,7 @@ type GetDraftPolicyResponse struct { func (x *GetDraftPolicyResponse) Reset() { *x = GetDraftPolicyResponse{} - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[150] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10649,7 +10925,7 @@ func (x *GetDraftPolicyResponse) String() string { func (*GetDraftPolicyResponse) ProtoMessage() {} func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[150] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10662,7 +10938,7 @@ func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyResponse.ProtoReflect.Descriptor instead. func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{147} + return file_openshell_proto_rawDescGZIP(), []int{150} } func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { @@ -10708,7 +10984,7 @@ type ApproveDraftChunkRequest struct { func (x *ApproveDraftChunkRequest) Reset() { *x = ApproveDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[151] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10720,7 +10996,7 @@ func (x *ApproveDraftChunkRequest) String() string { func (*ApproveDraftChunkRequest) ProtoMessage() {} func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[151] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10733,7 +11009,7 @@ func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{148} + return file_openshell_proto_rawDescGZIP(), []int{151} } func (x *ApproveDraftChunkRequest) GetName() string { @@ -10769,7 +11045,7 @@ type ApproveDraftChunkResponse struct { func (x *ApproveDraftChunkResponse) Reset() { *x = ApproveDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[152] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10781,7 +11057,7 @@ func (x *ApproveDraftChunkResponse) String() string { func (*ApproveDraftChunkResponse) ProtoMessage() {} func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[152] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10794,7 +11070,7 @@ func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{149} + return file_openshell_proto_rawDescGZIP(), []int{152} } func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { @@ -10828,7 +11104,7 @@ type RejectDraftChunkRequest struct { func (x *RejectDraftChunkRequest) Reset() { *x = RejectDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[153] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10840,7 +11116,7 @@ func (x *RejectDraftChunkRequest) String() string { func (*RejectDraftChunkRequest) ProtoMessage() {} func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[153] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10853,7 +11129,7 @@ func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkRequest.ProtoReflect.Descriptor instead. func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{150} + return file_openshell_proto_rawDescGZIP(), []int{153} } func (x *RejectDraftChunkRequest) GetName() string { @@ -10892,7 +11168,7 @@ type RejectDraftChunkResponse struct { func (x *RejectDraftChunkResponse) Reset() { *x = RejectDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[154] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10904,7 +11180,7 @@ func (x *RejectDraftChunkResponse) String() string { func (*RejectDraftChunkResponse) ProtoMessage() {} func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[154] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10917,7 +11193,7 @@ func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkResponse.ProtoReflect.Descriptor instead. func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{151} + return file_openshell_proto_rawDescGZIP(), []int{154} } // Approve all pending chunks. @@ -10935,7 +11211,7 @@ type ApproveAllDraftChunksRequest struct { func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[155] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10947,7 +11223,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[155] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10960,7 +11236,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{152} + return file_openshell_proto_rawDescGZIP(), []int{155} } func (x *ApproveAllDraftChunksRequest) GetName() string { @@ -11000,7 +11276,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[156] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11012,7 +11288,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[156] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11025,7 +11301,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{153} + return file_openshell_proto_rawDescGZIP(), []int{156} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -11073,7 +11349,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[157] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11085,7 +11361,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[157] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11098,7 +11374,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{154} + return file_openshell_proto_rawDescGZIP(), []int{157} } func (x *EditDraftChunkRequest) GetName() string { @@ -11137,7 +11413,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11149,7 +11425,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11162,7 +11438,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{155} + return file_openshell_proto_rawDescGZIP(), []int{158} } // Reverse an approval (remove merged rule from active policy). @@ -11180,7 +11456,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11192,7 +11468,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11205,7 +11481,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{156} + return file_openshell_proto_rawDescGZIP(), []int{159} } func (x *UndoDraftChunkRequest) GetName() string { @@ -11241,7 +11517,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11253,7 +11529,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11266,7 +11542,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{157} + return file_openshell_proto_rawDescGZIP(), []int{160} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -11296,7 +11572,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11308,7 +11584,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11321,7 +11597,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{158} + return file_openshell_proto_rawDescGZIP(), []int{161} } func (x *ClearDraftChunksRequest) GetName() string { @@ -11348,7 +11624,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11360,7 +11636,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11373,7 +11649,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{159} + return file_openshell_proto_rawDescGZIP(), []int{162} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -11396,7 +11672,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11408,7 +11684,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11421,7 +11697,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{160} + return file_openshell_proto_rawDescGZIP(), []int{163} } func (x *GetDraftHistoryRequest) GetName() string { @@ -11455,7 +11731,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11467,7 +11743,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11480,7 +11756,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{161} + return file_openshell_proto_rawDescGZIP(), []int{164} } func (x *DraftHistoryEntry) GetTimestampMs() int64 { @@ -11521,7 +11797,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11533,7 +11809,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11546,7 +11822,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{162} + return file_openshell_proto_rawDescGZIP(), []int{165} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -11575,7 +11851,7 @@ type PolicyRevisionPayload struct { func (x *PolicyRevisionPayload) Reset() { *x = PolicyRevisionPayload{} - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11587,7 +11863,7 @@ func (x *PolicyRevisionPayload) String() string { func (*PolicyRevisionPayload) ProtoMessage() {} func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11600,7 +11876,7 @@ func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyRevisionPayload.ProtoReflect.Descriptor instead. func (*PolicyRevisionPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{163} + return file_openshell_proto_rawDescGZIP(), []int{166} } func (x *PolicyRevisionPayload) GetPolicy() *sandboxv1.SandboxPolicy { @@ -11673,7 +11949,7 @@ type DraftChunkPayload struct { func (x *DraftChunkPayload) Reset() { *x = DraftChunkPayload{} - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11685,7 +11961,7 @@ func (x *DraftChunkPayload) String() string { func (*DraftChunkPayload) ProtoMessage() {} func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11698,7 +11974,7 @@ func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkPayload.ProtoReflect.Descriptor instead. func (*DraftChunkPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{164} + return file_openshell_proto_rawDescGZIP(), []int{167} } func (x *DraftChunkPayload) GetRuleName() string { @@ -11804,7 +12080,7 @@ type StoredPolicyRevision struct { func (x *StoredPolicyRevision) Reset() { *x = StoredPolicyRevision{} - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11816,7 +12092,7 @@ func (x *StoredPolicyRevision) String() string { func (*StoredPolicyRevision) ProtoMessage() {} func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11829,7 +12105,7 @@ func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredPolicyRevision.ProtoReflect.Descriptor instead. func (*StoredPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{165} + return file_openshell_proto_rawDescGZIP(), []int{168} } func (x *StoredPolicyRevision) GetId() string { @@ -11932,7 +12208,7 @@ type StoredDraftChunk struct { func (x *StoredDraftChunk) Reset() { *x = StoredDraftChunk{} - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11944,7 +12220,7 @@ func (x *StoredDraftChunk) String() string { func (*StoredDraftChunk) ProtoMessage() {} func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11957,7 +12233,7 @@ func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredDraftChunk.ProtoReflect.Descriptor instead. func (*StoredDraftChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{166} + return file_openshell_proto_rawDescGZIP(), []int{169} } func (x *StoredDraftChunk) GetId() string { @@ -12106,7 +12382,7 @@ type CreateWorkspaceRequest struct { func (x *CreateWorkspaceRequest) Reset() { *x = CreateWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12118,7 +12394,7 @@ func (x *CreateWorkspaceRequest) String() string { func (*CreateWorkspaceRequest) ProtoMessage() {} func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12131,7 +12407,7 @@ func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceRequest.ProtoReflect.Descriptor instead. func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{167} + return file_openshell_proto_rawDescGZIP(), []int{170} } func (x *CreateWorkspaceRequest) GetName() string { @@ -12158,7 +12434,7 @@ type CreateWorkspaceResponse struct { func (x *CreateWorkspaceResponse) Reset() { *x = CreateWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12170,7 +12446,7 @@ func (x *CreateWorkspaceResponse) String() string { func (*CreateWorkspaceResponse) ProtoMessage() {} func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12183,7 +12459,7 @@ func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceResponse.ProtoReflect.Descriptor instead. func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{168} + return file_openshell_proto_rawDescGZIP(), []int{171} } func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -12204,7 +12480,7 @@ type GetWorkspaceRequest struct { func (x *GetWorkspaceRequest) Reset() { *x = GetWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12216,7 +12492,7 @@ func (x *GetWorkspaceRequest) String() string { func (*GetWorkspaceRequest) ProtoMessage() {} func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[172] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12229,7 +12505,7 @@ func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceRequest.ProtoReflect.Descriptor instead. func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{169} + return file_openshell_proto_rawDescGZIP(), []int{172} } func (x *GetWorkspaceRequest) GetName() string { @@ -12249,7 +12525,7 @@ type GetWorkspaceResponse struct { func (x *GetWorkspaceResponse) Reset() { *x = GetWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12261,7 +12537,7 @@ func (x *GetWorkspaceResponse) String() string { func (*GetWorkspaceResponse) ProtoMessage() {} func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[173] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12274,7 +12550,7 @@ func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceResponse.ProtoReflect.Descriptor instead. func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{170} + return file_openshell_proto_rawDescGZIP(), []int{173} } func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -12297,7 +12573,7 @@ type ListWorkspacesRequest struct { func (x *ListWorkspacesRequest) Reset() { *x = ListWorkspacesRequest{} - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12309,7 +12585,7 @@ func (x *ListWorkspacesRequest) String() string { func (*ListWorkspacesRequest) ProtoMessage() {} func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[174] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12322,7 +12598,7 @@ func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesRequest.ProtoReflect.Descriptor instead. func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{171} + return file_openshell_proto_rawDescGZIP(), []int{174} } func (x *ListWorkspacesRequest) GetLimit() uint32 { @@ -12356,7 +12632,7 @@ type ListWorkspacesResponse struct { func (x *ListWorkspacesResponse) Reset() { *x = ListWorkspacesResponse{} - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12368,7 +12644,7 @@ func (x *ListWorkspacesResponse) String() string { func (*ListWorkspacesResponse) ProtoMessage() {} func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[175] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12381,7 +12657,7 @@ func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesResponse.ProtoReflect.Descriptor instead. func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{172} + return file_openshell_proto_rawDescGZIP(), []int{175} } func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { @@ -12402,7 +12678,7 @@ type DeleteWorkspaceRequest struct { func (x *DeleteWorkspaceRequest) Reset() { *x = DeleteWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12414,7 +12690,7 @@ func (x *DeleteWorkspaceRequest) String() string { func (*DeleteWorkspaceRequest) ProtoMessage() {} func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[176] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12427,7 +12703,7 @@ func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{173} + return file_openshell_proto_rawDescGZIP(), []int{176} } func (x *DeleteWorkspaceRequest) GetName() string { @@ -12447,7 +12723,7 @@ type DeleteWorkspaceResponse struct { func (x *DeleteWorkspaceResponse) Reset() { *x = DeleteWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12459,7 +12735,7 @@ func (x *DeleteWorkspaceResponse) String() string { func (*DeleteWorkspaceResponse) ProtoMessage() {} func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[177] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12472,7 +12748,7 @@ func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{174} + return file_openshell_proto_rawDescGZIP(), []int{177} } func (x *DeleteWorkspaceResponse) GetDeleted() bool { @@ -12496,7 +12772,7 @@ type WorkspaceMember struct { func (x *WorkspaceMember) Reset() { *x = WorkspaceMember{} - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12508,7 +12784,7 @@ func (x *WorkspaceMember) String() string { func (*WorkspaceMember) ProtoMessage() {} func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[178] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12521,7 +12797,7 @@ func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceMember.ProtoReflect.Descriptor instead. func (*WorkspaceMember) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{175} + return file_openshell_proto_rawDescGZIP(), []int{178} } func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { @@ -12560,7 +12836,7 @@ type AddWorkspaceMemberRequest struct { func (x *AddWorkspaceMemberRequest) Reset() { *x = AddWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12572,7 +12848,7 @@ func (x *AddWorkspaceMemberRequest) String() string { func (*AddWorkspaceMemberRequest) ProtoMessage() {} func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[179] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12585,7 +12861,7 @@ func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{176} + return file_openshell_proto_rawDescGZIP(), []int{179} } func (x *AddWorkspaceMemberRequest) GetWorkspace() string { @@ -12619,7 +12895,7 @@ type AddWorkspaceMemberResponse struct { func (x *AddWorkspaceMemberResponse) Reset() { *x = AddWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12631,7 +12907,7 @@ func (x *AddWorkspaceMemberResponse) String() string { func (*AddWorkspaceMemberResponse) ProtoMessage() {} func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[180] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12644,7 +12920,7 @@ func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{177} + return file_openshell_proto_rawDescGZIP(), []int{180} } func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { @@ -12667,7 +12943,7 @@ type RemoveWorkspaceMemberRequest struct { func (x *RemoveWorkspaceMemberRequest) Reset() { *x = RemoveWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12679,7 +12955,7 @@ func (x *RemoveWorkspaceMemberRequest) String() string { func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[181] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12692,7 +12968,7 @@ func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{178} + return file_openshell_proto_rawDescGZIP(), []int{181} } func (x *RemoveWorkspaceMemberRequest) GetWorkspace() string { @@ -12719,7 +12995,7 @@ type RemoveWorkspaceMemberResponse struct { func (x *RemoveWorkspaceMemberResponse) Reset() { *x = RemoveWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12731,7 +13007,7 @@ func (x *RemoveWorkspaceMemberResponse) String() string { func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[182] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12744,7 +13020,7 @@ func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{179} + return file_openshell_proto_rawDescGZIP(), []int{182} } func (x *RemoveWorkspaceMemberResponse) GetRemoved() bool { @@ -12767,7 +13043,7 @@ type ListWorkspaceMembersRequest struct { func (x *ListWorkspaceMembersRequest) Reset() { *x = ListWorkspaceMembersRequest{} - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12779,7 +13055,7 @@ func (x *ListWorkspaceMembersRequest) String() string { func (*ListWorkspaceMembersRequest) ProtoMessage() {} func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[183] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12792,7 +13068,7 @@ func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{180} + return file_openshell_proto_rawDescGZIP(), []int{183} } func (x *ListWorkspaceMembersRequest) GetWorkspace() string { @@ -12826,7 +13102,7 @@ type ListWorkspaceMembersResponse struct { func (x *ListWorkspaceMembersResponse) Reset() { *x = ListWorkspaceMembersResponse{} - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12838,7 +13114,7 @@ func (x *ListWorkspaceMembersResponse) String() string { func (*ListWorkspaceMembersResponse) ProtoMessage() {} func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[184] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12851,7 +13127,7 @@ func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{181} + return file_openshell_proto_rawDescGZIP(), []int{184} } func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { @@ -13209,7 +13485,13 @@ const file_openshell_proto_rawDesc = "" + "\x04port\x18\x02 \x01(\rR\x04port\x12\x12\n" + "\x04path\x18\x03 \x01(\tR\x04path\x12\x1a\n" + "\baudience\x18\x04 \x01(\tR\baudience\x12\x16\n" + - "\x06scopes\x18\x05 \x03(\tR\x06scopes\"\xf0\x02\n" + + "\x06scopes\x18\x05 \x03(\tR\x06scopes\"\x90\x01\n" + + "(ProviderCredentialTokenGrantSubjectToken\x12\x16\n" + + "\x06source\x18\x01 \x01(\tR\x06source\x12\x1e\n" + + "\n" + + "credential\x18\x02 \x01(\tR\n" + + "credential\x12,\n" + + "\x12subject_token_type\x18\x03 \x01(\tR\x10subjectTokenType\"\xce\x04\n" + "\x1cProviderCredentialTokenGrant\x12%\n" + "\x0etoken_endpoint\x18\x01 \x01(\tR\rtokenEndpoint\x12\x1a\n" + "\baudience\x18\x02 \x01(\tR\baudience\x12*\n" + @@ -13217,7 +13499,12 @@ const file_openshell_proto_rawDesc = "" + "\x06scopes\x18\x03 \x03(\tR\x06scopes\x12*\n" + "\x11cache_ttl_seconds\x18\x04 \x01(\x03R\x0fcacheTtlSeconds\x12i\n" + "\x12audience_overrides\x18\x05 \x03(\v2:.openshell.v1.ProviderCredentialTokenGrantAudienceOverrideR\x11audienceOverrides\x122\n" + - "\x15client_assertion_type\x18\a \x01(\tR\x13clientAssertionType\"\x9e\x03\n" + + "\x15client_assertion_type\x18\a \x01(\tR\x13clientAssertionType\x12M\n" + + "\n" + + "grant_type\x18\b \x01(\x0e2..openshell.v1.ProviderCredentialTokenGrantTypeR\tgrantType\x12[\n" + + "\rsubject_token\x18\t \x01(\v26.openshell.v1.ProviderCredentialTokenGrantSubjectTokenR\fsubjectToken\x120\n" + + "\x14requested_token_type\x18\n" + + " \x01(\tR\x12requestedTokenType\"\x9e\x03\n" + "\x19ProviderProfileCredential\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + @@ -13408,7 +13695,19 @@ const file_openshell_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\v2'.openshell.v1.ProviderProfileCredentialR\x05value:\x028\x01\x1ar\n" + "\x1dStaticCredentialBindingsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12;\n" + - "\x05value\x18\x02 \x01(\v2%.openshell.v1.StaticCredentialBindingR\x05value:\x028\x01\"\xce\x04\n" + + "\x05value\x18\x02 \x01(\v2%.openshell.v1.StaticCredentialBindingR\x05value:\x028\x01\"\xbd\x01\n" + + "#ExchangeProviderSubjectTokenRequest\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x1a\n" + + "\bprovider\x18\x02 \x01(\tR\bprovider\x12%\n" + + "\x0ecredential_key\x18\x03 \x01(\tR\rcredentialKey\x124\n" + + "\x13supervisor_jwt_svid\x18\x04 \x01(\tB\x04\x88\xb5\x18\x01R\x11supervisorJwtSvid\"\x8d\x01\n" + + "$ExchangeProviderSubjectTokenResponse\x12'\n" + + "\faccess_token\x18\x01 \x01(\tB\x04\x88\xb5\x18\x01R\vaccessToken\x12\x1d\n" + + "\n" + + "expires_in\x18\x02 \x01(\x03R\texpiresIn\x12\x1d\n" + + "\n" + + "token_type\x18\x03 \x01(\tR\ttokenType\"\xce\x04\n" + "\x13UpdateConfigRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12;\n" + "\x06policy\x18\x02 \x01(\v2#.openshell.sandbox.v1.SandboxPolicyR\x06policy\x12\x1f\n" + @@ -13853,7 +14152,11 @@ const file_openshell_proto_rawDesc = "" + "\x13SANDBOX_PHASE_READY\x10\x02\x12\x17\n" + "\x13SANDBOX_PHASE_ERROR\x10\x03\x12\x1a\n" + "\x16SANDBOX_PHASE_DELETING\x10\x04\x12\x19\n" + - "\x15SANDBOX_PHASE_UNKNOWN\x10\x05*\xc3\x03\n" + + "\x15SANDBOX_PHASE_UNKNOWN\x10\x05*\xce\x01\n" + + " ProviderCredentialTokenGrantType\x124\n" + + "0PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED\x10\x00\x12;\n" + + "7PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS\x10\x01\x127\n" + + "3PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE\x10\x02*\xc3\x03\n" + "!ProviderCredentialRefreshStrategy\x124\n" + "0PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED\x10\x00\x12/\n" + "+PROVIDER_CREDENTIAL_REFRESH_STRATEGY_STATIC\x10\x01\x121\n" + @@ -13885,7 +14188,7 @@ const file_openshell_proto_rawDesc = "" + "\rWorkspaceRole\x12\x1e\n" + "\x1aWORKSPACE_ROLE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13WORKSPACE_ROLE_USER\x10\x01\x12\x18\n" + - "\x14WORKSPACE_ROLE_ADMIN\x10\x022\xacB\n" + + "\x14WORKSPACE_ROLE_ADMIN\x10\x022\xc3C\n" + "\tOpenShell\x12Z\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\"\x15\x82\xb5\x18\x11\n" + "\x0funauthenticated\x12i\n" + @@ -13971,6 +14274,8 @@ const file_openshell_proto_rawDesc = "" + "\x12ReportPolicyStatus\x12'.openshell.v1.ReportPolicyStatusRequest\x1a(.openshell.v1.ReportPolicyStatusResponse\"\r\x82\xb5\x18\t\n" + "\asandbox\x12\x97\x01\n" + "\x1dGetSandboxProviderEnvironment\x122.openshell.v1.GetSandboxProviderEnvironmentRequest\x1a3.openshell.v1.GetSandboxProviderEnvironmentResponse\"\r\x82\xb5\x18\t\n" + + "\asandbox\x12\x94\x01\n" + + "\x1cExchangeProviderSubjectToken\x121.openshell.v1.ExchangeProviderSubjectTokenRequest\x1a2.openshell.v1.ExchangeProviderSubjectTokenResponse\"\r\x82\xb5\x18\t\n" + "\asandbox\x12}\n" + "\x0eGetSandboxLogs\x12#.openshell.v1.GetSandboxLogsRequest\x1a$.openshell.v1.GetSandboxLogsResponse\" \x82\xb5\x18\x1c\n" + "\x06bearer\x12\x04user\"\fsandbox:read\x12o\n" + @@ -14031,521 +14336,529 @@ func file_openshell_proto_rawDescGZIP() []byte { return file_openshell_proto_rawDescData } -var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 206) +var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 7) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 209) var file_openshell_proto_goTypes = []any{ (SandboxPhase)(0), // 0: openshell.v1.SandboxPhase - (ProviderCredentialRefreshStrategy)(0), // 1: openshell.v1.ProviderCredentialRefreshStrategy - (ProviderProfileCategory)(0), // 2: openshell.v1.ProviderProfileCategory - (PolicyStatus)(0), // 3: openshell.v1.PolicyStatus - (ServiceStatus)(0), // 4: openshell.v1.ServiceStatus - (WorkspaceRole)(0), // 5: openshell.v1.WorkspaceRole - (*IssueSandboxTokenRequest)(nil), // 6: openshell.v1.IssueSandboxTokenRequest - (*IssueSandboxTokenResponse)(nil), // 7: openshell.v1.IssueSandboxTokenResponse - (*RefreshSandboxTokenRequest)(nil), // 8: openshell.v1.RefreshSandboxTokenRequest - (*RefreshSandboxTokenResponse)(nil), // 9: openshell.v1.RefreshSandboxTokenResponse - (*HealthRequest)(nil), // 10: openshell.v1.HealthRequest - (*HealthResponse)(nil), // 11: openshell.v1.HealthResponse - (*GetCurrentUserRequest)(nil), // 12: openshell.v1.GetCurrentUserRequest - (*GetCurrentUserResponse)(nil), // 13: openshell.v1.GetCurrentUserResponse - (*GetGatewayInfoRequest)(nil), // 14: openshell.v1.GetGatewayInfoRequest - (*GetGatewayInfoResponse)(nil), // 15: openshell.v1.GetGatewayInfoResponse - (*ComputeDriverInfo)(nil), // 16: openshell.v1.ComputeDriverInfo - (*ComputeDriverCapabilities)(nil), // 17: openshell.v1.ComputeDriverCapabilities - (*Sandbox)(nil), // 18: openshell.v1.Sandbox - (*SandboxSpec)(nil), // 19: openshell.v1.SandboxSpec - (*ResourceRequirements)(nil), // 20: openshell.v1.ResourceRequirements - (*GpuResourceRequirements)(nil), // 21: openshell.v1.GpuResourceRequirements - (*SandboxTemplate)(nil), // 22: openshell.v1.SandboxTemplate - (*SandboxStatus)(nil), // 23: openshell.v1.SandboxStatus - (*SandboxCondition)(nil), // 24: openshell.v1.SandboxCondition - (*PlatformEvent)(nil), // 25: openshell.v1.PlatformEvent - (*CreateSandboxRequest)(nil), // 26: openshell.v1.CreateSandboxRequest - (*GetSandboxRequest)(nil), // 27: openshell.v1.GetSandboxRequest - (*ListSandboxesRequest)(nil), // 28: openshell.v1.ListSandboxesRequest - (*ListSandboxProvidersRequest)(nil), // 29: openshell.v1.ListSandboxProvidersRequest - (*AttachSandboxProviderRequest)(nil), // 30: openshell.v1.AttachSandboxProviderRequest - (*DetachSandboxProviderRequest)(nil), // 31: openshell.v1.DetachSandboxProviderRequest - (*DeleteSandboxRequest)(nil), // 32: openshell.v1.DeleteSandboxRequest - (*SandboxResponse)(nil), // 33: openshell.v1.SandboxResponse - (*ListSandboxesResponse)(nil), // 34: openshell.v1.ListSandboxesResponse - (*ListSandboxProvidersResponse)(nil), // 35: openshell.v1.ListSandboxProvidersResponse - (*AttachSandboxProviderResponse)(nil), // 36: openshell.v1.AttachSandboxProviderResponse - (*DetachSandboxProviderResponse)(nil), // 37: openshell.v1.DetachSandboxProviderResponse - (*DeleteSandboxResponse)(nil), // 38: openshell.v1.DeleteSandboxResponse - (*CreateSshSessionRequest)(nil), // 39: openshell.v1.CreateSshSessionRequest - (*CreateSshSessionResponse)(nil), // 40: openshell.v1.CreateSshSessionResponse - (*ExposeServiceRequest)(nil), // 41: openshell.v1.ExposeServiceRequest - (*GetServiceRequest)(nil), // 42: openshell.v1.GetServiceRequest - (*ListServicesRequest)(nil), // 43: openshell.v1.ListServicesRequest - (*ListServicesResponse)(nil), // 44: openshell.v1.ListServicesResponse - (*DeleteServiceRequest)(nil), // 45: openshell.v1.DeleteServiceRequest - (*DeleteServiceResponse)(nil), // 46: openshell.v1.DeleteServiceResponse - (*ServiceEndpoint)(nil), // 47: openshell.v1.ServiceEndpoint - (*ServiceEndpointResponse)(nil), // 48: openshell.v1.ServiceEndpointResponse - (*RevokeSshSessionRequest)(nil), // 49: openshell.v1.RevokeSshSessionRequest - (*RevokeSshSessionResponse)(nil), // 50: openshell.v1.RevokeSshSessionResponse - (*ExecSandboxRequest)(nil), // 51: openshell.v1.ExecSandboxRequest - (*ExecSandboxStdout)(nil), // 52: openshell.v1.ExecSandboxStdout - (*ExecSandboxStderr)(nil), // 53: openshell.v1.ExecSandboxStderr - (*ExecSandboxExit)(nil), // 54: openshell.v1.ExecSandboxExit - (*ExecSandboxEvent)(nil), // 55: openshell.v1.ExecSandboxEvent - (*TcpForwardInit)(nil), // 56: openshell.v1.TcpForwardInit - (*TcpForwardFrame)(nil), // 57: openshell.v1.TcpForwardFrame - (*ExecSandboxInput)(nil), // 58: openshell.v1.ExecSandboxInput - (*ExecSandboxWindowResize)(nil), // 59: openshell.v1.ExecSandboxWindowResize - (*SshSession)(nil), // 60: openshell.v1.SshSession - (*WatchSandboxRequest)(nil), // 61: openshell.v1.WatchSandboxRequest - (*SandboxStreamEvent)(nil), // 62: openshell.v1.SandboxStreamEvent - (*SandboxLogLine)(nil), // 63: openshell.v1.SandboxLogLine - (*SandboxStreamWarning)(nil), // 64: openshell.v1.SandboxStreamWarning - (*CreateProviderRequest)(nil), // 65: openshell.v1.CreateProviderRequest - (*GetProviderRequest)(nil), // 66: openshell.v1.GetProviderRequest - (*ListProvidersRequest)(nil), // 67: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 68: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 69: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 70: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 71: openshell.v1.ListProvidersResponse - (*ListProviderProfilesRequest)(nil), // 72: openshell.v1.ListProviderProfilesRequest - (*GetProviderProfileRequest)(nil), // 73: openshell.v1.GetProviderProfileRequest - (*ProviderProfileImportItem)(nil), // 74: openshell.v1.ProviderProfileImportItem - (*ProviderProfileDiagnostic)(nil), // 75: openshell.v1.ProviderProfileDiagnostic - (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 76: openshell.v1.ProviderCredentialTokenGrantAudienceOverride - (*ProviderCredentialTokenGrant)(nil), // 77: openshell.v1.ProviderCredentialTokenGrant - (*ProviderProfileCredential)(nil), // 78: openshell.v1.ProviderProfileCredential - (*ProviderCredentialRefreshMaterial)(nil), // 79: openshell.v1.ProviderCredentialRefreshMaterial - (*ProviderCredentialRefreshOutput)(nil), // 80: openshell.v1.ProviderCredentialRefreshOutput - (*ProviderCredentialRefresh)(nil), // 81: openshell.v1.ProviderCredentialRefresh - (*ProviderCredentialRefreshStatus)(nil), // 82: openshell.v1.ProviderCredentialRefreshStatus - (*ProviderProfileDiscovery)(nil), // 83: openshell.v1.ProviderProfileDiscovery - (*StoredProviderCredentialRefreshState)(nil), // 84: openshell.v1.StoredProviderCredentialRefreshState - (*GetProviderRefreshStatusRequest)(nil), // 85: openshell.v1.GetProviderRefreshStatusRequest - (*GetProviderRefreshStatusResponse)(nil), // 86: openshell.v1.GetProviderRefreshStatusResponse - (*ConfigureProviderRefreshRequest)(nil), // 87: openshell.v1.ConfigureProviderRefreshRequest - (*ConfigureProviderRefreshResponse)(nil), // 88: openshell.v1.ConfigureProviderRefreshResponse - (*RotateProviderCredentialRequest)(nil), // 89: openshell.v1.RotateProviderCredentialRequest - (*RotateProviderCredentialResponse)(nil), // 90: openshell.v1.RotateProviderCredentialResponse - (*DeleteProviderRefreshRequest)(nil), // 91: openshell.v1.DeleteProviderRefreshRequest - (*DeleteProviderRefreshResponse)(nil), // 92: openshell.v1.DeleteProviderRefreshResponse - (*ProviderProfile)(nil), // 93: openshell.v1.ProviderProfile - (*StoredProviderProfile)(nil), // 94: openshell.v1.StoredProviderProfile - (*ProviderProfileResponse)(nil), // 95: openshell.v1.ProviderProfileResponse - (*ListProviderProfilesResponse)(nil), // 96: openshell.v1.ListProviderProfilesResponse - (*ImportProviderProfilesRequest)(nil), // 97: openshell.v1.ImportProviderProfilesRequest - (*ImportProviderProfilesResponse)(nil), // 98: openshell.v1.ImportProviderProfilesResponse - (*UpdateProviderProfilesRequest)(nil), // 99: openshell.v1.UpdateProviderProfilesRequest - (*UpdateProviderProfilesResponse)(nil), // 100: openshell.v1.UpdateProviderProfilesResponse - (*LintProviderProfilesRequest)(nil), // 101: openshell.v1.LintProviderProfilesRequest - (*LintProviderProfilesResponse)(nil), // 102: openshell.v1.LintProviderProfilesResponse - (*DeleteProviderResponse)(nil), // 103: openshell.v1.DeleteProviderResponse - (*DeleteProviderProfileRequest)(nil), // 104: openshell.v1.DeleteProviderProfileRequest - (*DeleteProviderProfileResponse)(nil), // 105: openshell.v1.DeleteProviderProfileResponse - (*GetSandboxProviderEnvironmentRequest)(nil), // 106: openshell.v1.GetSandboxProviderEnvironmentRequest - (*StaticCredentialEndpointBinding)(nil), // 107: openshell.v1.StaticCredentialEndpointBinding - (*StaticCredentialBinding)(nil), // 108: openshell.v1.StaticCredentialBinding - (*GetSandboxProviderEnvironmentResponse)(nil), // 109: openshell.v1.GetSandboxProviderEnvironmentResponse - (*UpdateConfigRequest)(nil), // 110: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 111: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 112: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 113: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 114: openshell.v1.RemoveNetworkRule - (*AddDenyRules)(nil), // 115: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 116: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 117: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 118: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 119: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 120: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 121: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 122: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 123: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 124: openshell.v1.ReportPolicyStatusResponse - (*SandboxPolicyRevision)(nil), // 125: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 126: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 127: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 128: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 129: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 130: openshell.v1.SupervisorMessage - (*GatewayMessage)(nil), // 131: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 132: openshell.v1.SupervisorHello - (*SessionAccepted)(nil), // 133: openshell.v1.SessionAccepted - (*SessionRejected)(nil), // 134: openshell.v1.SessionRejected - (*SupervisorHeartbeat)(nil), // 135: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 136: openshell.v1.GatewayHeartbeat - (*RelayOpen)(nil), // 137: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 138: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 139: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 140: openshell.v1.RelayInit - (*RelayFrame)(nil), // 141: openshell.v1.RelayFrame - (*RelayOpenResult)(nil), // 142: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 143: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 144: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 145: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 146: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 147: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 148: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 149: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 150: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 151: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 152: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 153: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 154: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 155: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 156: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 157: openshell.v1.RejectDraftChunkResponse - (*ApproveAllDraftChunksRequest)(nil), // 158: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 159: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 160: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 161: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 162: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 163: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 164: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 165: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 166: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 167: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 168: openshell.v1.GetDraftHistoryResponse - (*PolicyRevisionPayload)(nil), // 169: openshell.v1.PolicyRevisionPayload - (*DraftChunkPayload)(nil), // 170: openshell.v1.DraftChunkPayload - (*StoredPolicyRevision)(nil), // 171: openshell.v1.StoredPolicyRevision - (*StoredDraftChunk)(nil), // 172: openshell.v1.StoredDraftChunk - (*CreateWorkspaceRequest)(nil), // 173: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 174: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 175: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 176: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 177: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 178: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 179: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 180: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 181: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 182: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 183: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 184: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 185: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 186: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 187: openshell.v1.ListWorkspaceMembersResponse - nil, // 188: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 189: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 190: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 191: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 192: openshell.v1.PlatformEvent.MetadataEntry - nil, // 193: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 194: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 195: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 196: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 197: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - nil, // 198: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - nil, // 199: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - nil, // 200: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 201: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 202: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 203: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - nil, // 204: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 205: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 206: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 207: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 208: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 209: openshell.v1.PolicyRevisionPayload.ProvenanceEntry - nil, // 210: openshell.v1.StoredPolicyRevision.ProvenanceEntry - nil, // 211: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*datamodelv1.ObjectMeta)(nil), // 212: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 213: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 214: google.protobuf.Struct - (*datamodelv1.Provider)(nil), // 215: openshell.datamodel.v1.Provider - (*sandboxv1.NetworkEndpoint)(nil), // 216: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 217: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 218: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 219: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 220: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 221: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 222: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 223: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 224: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 225: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 226: openshell.sandbox.v1.GetGatewayConfigResponse + (ProviderCredentialTokenGrantType)(0), // 1: openshell.v1.ProviderCredentialTokenGrantType + (ProviderCredentialRefreshStrategy)(0), // 2: openshell.v1.ProviderCredentialRefreshStrategy + (ProviderProfileCategory)(0), // 3: openshell.v1.ProviderProfileCategory + (PolicyStatus)(0), // 4: openshell.v1.PolicyStatus + (ServiceStatus)(0), // 5: openshell.v1.ServiceStatus + (WorkspaceRole)(0), // 6: openshell.v1.WorkspaceRole + (*IssueSandboxTokenRequest)(nil), // 7: openshell.v1.IssueSandboxTokenRequest + (*IssueSandboxTokenResponse)(nil), // 8: openshell.v1.IssueSandboxTokenResponse + (*RefreshSandboxTokenRequest)(nil), // 9: openshell.v1.RefreshSandboxTokenRequest + (*RefreshSandboxTokenResponse)(nil), // 10: openshell.v1.RefreshSandboxTokenResponse + (*HealthRequest)(nil), // 11: openshell.v1.HealthRequest + (*HealthResponse)(nil), // 12: openshell.v1.HealthResponse + (*GetCurrentUserRequest)(nil), // 13: openshell.v1.GetCurrentUserRequest + (*GetCurrentUserResponse)(nil), // 14: openshell.v1.GetCurrentUserResponse + (*GetGatewayInfoRequest)(nil), // 15: openshell.v1.GetGatewayInfoRequest + (*GetGatewayInfoResponse)(nil), // 16: openshell.v1.GetGatewayInfoResponse + (*ComputeDriverInfo)(nil), // 17: openshell.v1.ComputeDriverInfo + (*ComputeDriverCapabilities)(nil), // 18: openshell.v1.ComputeDriverCapabilities + (*Sandbox)(nil), // 19: openshell.v1.Sandbox + (*SandboxSpec)(nil), // 20: openshell.v1.SandboxSpec + (*ResourceRequirements)(nil), // 21: openshell.v1.ResourceRequirements + (*GpuResourceRequirements)(nil), // 22: openshell.v1.GpuResourceRequirements + (*SandboxTemplate)(nil), // 23: openshell.v1.SandboxTemplate + (*SandboxStatus)(nil), // 24: openshell.v1.SandboxStatus + (*SandboxCondition)(nil), // 25: openshell.v1.SandboxCondition + (*PlatformEvent)(nil), // 26: openshell.v1.PlatformEvent + (*CreateSandboxRequest)(nil), // 27: openshell.v1.CreateSandboxRequest + (*GetSandboxRequest)(nil), // 28: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 29: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 30: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 31: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 32: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 33: openshell.v1.DeleteSandboxRequest + (*SandboxResponse)(nil), // 34: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 35: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 36: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 37: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 38: openshell.v1.DetachSandboxProviderResponse + (*DeleteSandboxResponse)(nil), // 39: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 40: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 41: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 42: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 43: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 44: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 45: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 46: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 47: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 48: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 49: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 50: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 51: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 52: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 53: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 54: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 55: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 56: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 57: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 58: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 59: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 60: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 61: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 62: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 63: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 64: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 65: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 66: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 67: openshell.v1.GetProviderRequest + (*ListProvidersRequest)(nil), // 68: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 69: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 70: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 71: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 72: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 73: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 74: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 75: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 76: openshell.v1.ProviderProfileDiagnostic + (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 77: openshell.v1.ProviderCredentialTokenGrantAudienceOverride + (*ProviderCredentialTokenGrantSubjectToken)(nil), // 78: openshell.v1.ProviderCredentialTokenGrantSubjectToken + (*ProviderCredentialTokenGrant)(nil), // 79: openshell.v1.ProviderCredentialTokenGrant + (*ProviderProfileCredential)(nil), // 80: openshell.v1.ProviderProfileCredential + (*ProviderCredentialRefreshMaterial)(nil), // 81: openshell.v1.ProviderCredentialRefreshMaterial + (*ProviderCredentialRefreshOutput)(nil), // 82: openshell.v1.ProviderCredentialRefreshOutput + (*ProviderCredentialRefresh)(nil), // 83: openshell.v1.ProviderCredentialRefresh + (*ProviderCredentialRefreshStatus)(nil), // 84: openshell.v1.ProviderCredentialRefreshStatus + (*ProviderProfileDiscovery)(nil), // 85: openshell.v1.ProviderProfileDiscovery + (*StoredProviderCredentialRefreshState)(nil), // 86: openshell.v1.StoredProviderCredentialRefreshState + (*GetProviderRefreshStatusRequest)(nil), // 87: openshell.v1.GetProviderRefreshStatusRequest + (*GetProviderRefreshStatusResponse)(nil), // 88: openshell.v1.GetProviderRefreshStatusResponse + (*ConfigureProviderRefreshRequest)(nil), // 89: openshell.v1.ConfigureProviderRefreshRequest + (*ConfigureProviderRefreshResponse)(nil), // 90: openshell.v1.ConfigureProviderRefreshResponse + (*RotateProviderCredentialRequest)(nil), // 91: openshell.v1.RotateProviderCredentialRequest + (*RotateProviderCredentialResponse)(nil), // 92: openshell.v1.RotateProviderCredentialResponse + (*DeleteProviderRefreshRequest)(nil), // 93: openshell.v1.DeleteProviderRefreshRequest + (*DeleteProviderRefreshResponse)(nil), // 94: openshell.v1.DeleteProviderRefreshResponse + (*ProviderProfile)(nil), // 95: openshell.v1.ProviderProfile + (*StoredProviderProfile)(nil), // 96: openshell.v1.StoredProviderProfile + (*ProviderProfileResponse)(nil), // 97: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 98: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 99: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 100: openshell.v1.ImportProviderProfilesResponse + (*UpdateProviderProfilesRequest)(nil), // 101: openshell.v1.UpdateProviderProfilesRequest + (*UpdateProviderProfilesResponse)(nil), // 102: openshell.v1.UpdateProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 103: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 104: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 105: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 106: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 107: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 108: openshell.v1.GetSandboxProviderEnvironmentRequest + (*StaticCredentialEndpointBinding)(nil), // 109: openshell.v1.StaticCredentialEndpointBinding + (*StaticCredentialBinding)(nil), // 110: openshell.v1.StaticCredentialBinding + (*GetSandboxProviderEnvironmentResponse)(nil), // 111: openshell.v1.GetSandboxProviderEnvironmentResponse + (*ExchangeProviderSubjectTokenRequest)(nil), // 112: openshell.v1.ExchangeProviderSubjectTokenRequest + (*ExchangeProviderSubjectTokenResponse)(nil), // 113: openshell.v1.ExchangeProviderSubjectTokenResponse + (*UpdateConfigRequest)(nil), // 114: openshell.v1.UpdateConfigRequest + (*PolicyMergeOperation)(nil), // 115: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 116: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 117: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 118: openshell.v1.RemoveNetworkRule + (*AddDenyRules)(nil), // 119: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 120: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 121: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 122: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 123: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 124: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 125: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 126: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 127: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 128: openshell.v1.ReportPolicyStatusResponse + (*SandboxPolicyRevision)(nil), // 129: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 130: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 131: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 132: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 133: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 134: openshell.v1.SupervisorMessage + (*GatewayMessage)(nil), // 135: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 136: openshell.v1.SupervisorHello + (*SessionAccepted)(nil), // 137: openshell.v1.SessionAccepted + (*SessionRejected)(nil), // 138: openshell.v1.SessionRejected + (*SupervisorHeartbeat)(nil), // 139: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 140: openshell.v1.GatewayHeartbeat + (*RelayOpen)(nil), // 141: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 142: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 143: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 144: openshell.v1.RelayInit + (*RelayFrame)(nil), // 145: openshell.v1.RelayFrame + (*RelayOpenResult)(nil), // 146: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 147: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 148: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 149: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 150: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 151: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 152: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 153: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 154: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 155: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 156: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 157: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 158: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 159: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 160: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 161: openshell.v1.RejectDraftChunkResponse + (*ApproveAllDraftChunksRequest)(nil), // 162: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 163: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 164: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 165: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 166: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 167: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 168: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 169: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 170: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 171: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 172: openshell.v1.GetDraftHistoryResponse + (*PolicyRevisionPayload)(nil), // 173: openshell.v1.PolicyRevisionPayload + (*DraftChunkPayload)(nil), // 174: openshell.v1.DraftChunkPayload + (*StoredPolicyRevision)(nil), // 175: openshell.v1.StoredPolicyRevision + (*StoredDraftChunk)(nil), // 176: openshell.v1.StoredDraftChunk + (*CreateWorkspaceRequest)(nil), // 177: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 178: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 179: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 180: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 181: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 182: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 183: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 184: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 185: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 186: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 187: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 188: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 189: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 190: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 191: openshell.v1.ListWorkspaceMembersResponse + nil, // 192: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 193: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 194: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 195: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 196: openshell.v1.PlatformEvent.MetadataEntry + nil, // 197: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 198: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 199: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 200: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 201: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + nil, // 202: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + nil, // 203: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + nil, // 204: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 205: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 206: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 207: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + nil, // 208: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 209: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 210: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 211: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 212: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 213: openshell.v1.PolicyRevisionPayload.ProvenanceEntry + nil, // 214: openshell.v1.StoredPolicyRevision.ProvenanceEntry + nil, // 215: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*datamodelv1.ObjectMeta)(nil), // 216: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 217: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 218: google.protobuf.Struct + (*datamodelv1.Provider)(nil), // 219: openshell.datamodel.v1.Provider + (*sandboxv1.NetworkEndpoint)(nil), // 220: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 221: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 222: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 223: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 224: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 225: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 226: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 227: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 228: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 229: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 230: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 4, // 0: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus - 4, // 1: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus - 16, // 2: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo - 17, // 3: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 212, // 4: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 19, // 5: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 23, // 6: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 188, // 7: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 22, // 8: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 213, // 9: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 20, // 10: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements - 21, // 11: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 189, // 12: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 190, // 13: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 191, // 14: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 214, // 15: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 214, // 16: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 24, // 17: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 5, // 0: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus + 5, // 1: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus + 17, // 2: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo + 18, // 3: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities + 216, // 4: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 20, // 5: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec + 24, // 6: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 192, // 7: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 23, // 8: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 217, // 9: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 21, // 10: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements + 22, // 11: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements + 193, // 12: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 194, // 13: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 195, // 14: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 218, // 15: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 218, // 16: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 25, // 17: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition 0, // 18: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 192, // 19: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 19, // 20: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 193, // 21: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 194, // 22: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 18, // 23: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 18, // 24: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 215, // 25: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 18, // 26: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 18, // 27: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 48, // 28: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 212, // 29: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 47, // 30: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 195, // 31: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 52, // 32: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 53, // 33: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 54, // 34: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 138, // 35: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 139, // 36: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 56, // 37: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 51, // 38: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 59, // 39: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 212, // 40: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 18, // 41: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 63, // 42: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 25, // 43: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 64, // 44: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 149, // 45: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 196, // 46: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 215, // 47: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 215, // 48: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 197, // 49: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 215, // 50: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 215, // 51: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 93, // 52: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 76, // 53: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 81, // 54: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 77, // 55: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 1, // 56: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 79, // 57: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 80, // 58: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 1, // 59: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 212, // 60: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 1, // 61: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 198, // 62: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - 199, // 63: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - 82, // 64: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 1, // 65: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 200, // 66: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 82, // 67: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 82, // 68: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 2, // 69: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 78, // 70: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 216, // 71: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 217, // 72: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 83, // 73: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 201, // 74: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 212, // 75: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 93, // 76: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile - 93, // 77: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 93, // 78: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 74, // 79: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 75, // 80: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 93, // 81: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 74, // 82: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 75, // 83: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 93, // 84: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 74, // 85: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 75, // 86: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 107, // 87: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 202, // 88: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 203, // 89: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 204, // 90: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 205, // 91: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 213, // 92: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 218, // 93: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 111, // 94: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 206, // 95: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 112, // 96: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 113, // 97: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 114, // 98: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 115, // 99: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 116, // 100: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 117, // 101: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 219, // 102: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 220, // 103: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 221, // 104: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 207, // 105: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 125, // 106: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 125, // 107: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 3, // 108: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 3, // 109: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 213, // 110: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 208, // 111: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 63, // 112: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 63, // 113: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 132, // 114: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 135, // 115: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 142, // 116: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 143, // 117: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 133, // 118: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 134, // 119: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 136, // 120: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 137, // 121: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 143, // 122: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 138, // 123: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 139, // 124: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 140, // 125: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 144, // 126: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 146, // 127: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 219, // 128: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 145, // 129: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 148, // 130: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 147, // 131: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 148, // 132: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 219, // 133: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 167, // 134: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 213, // 135: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 209, // 136: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry - 219, // 137: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 210, // 138: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry - 211, // 139: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 222, // 140: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 222, // 141: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 222, // 142: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 212, // 143: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 5, // 144: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 5, // 145: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 181, // 146: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 181, // 147: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 78, // 148: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 108, // 149: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 10, // 150: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 12, // 151: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 14, // 152: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 26, // 153: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 27, // 154: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 28, // 155: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 29, // 156: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 30, // 157: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 31, // 158: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 32, // 159: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 39, // 160: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 41, // 161: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 42, // 162: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 43, // 163: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 45, // 164: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 49, // 165: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 51, // 166: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 57, // 167: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 58, // 168: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 65, // 169: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 66, // 170: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 67, // 171: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 72, // 172: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 73, // 173: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 97, // 174: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 99, // 175: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 101, // 176: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 68, // 177: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 85, // 178: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 87, // 179: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 89, // 180: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 91, // 181: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 69, // 182: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 104, // 183: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 223, // 184: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 224, // 185: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 110, // 186: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 119, // 187: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 121, // 188: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 123, // 189: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 106, // 190: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 126, // 191: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 127, // 192: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 130, // 193: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 141, // 194: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 61, // 195: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 150, // 196: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 152, // 197: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 154, // 198: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 156, // 199: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 158, // 200: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 160, // 201: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 162, // 202: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 164, // 203: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 166, // 204: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 6, // 205: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 8, // 206: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 173, // 207: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 175, // 208: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 177, // 209: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 179, // 210: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 182, // 211: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 184, // 212: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 186, // 213: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 11, // 214: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 13, // 215: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 15, // 216: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 33, // 217: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 33, // 218: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 34, // 219: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 35, // 220: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 36, // 221: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 37, // 222: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 38, // 223: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 40, // 224: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 48, // 225: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 48, // 226: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 44, // 227: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 46, // 228: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 50, // 229: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 55, // 230: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 57, // 231: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 55, // 232: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 70, // 233: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 70, // 234: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 71, // 235: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 96, // 236: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 95, // 237: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 98, // 238: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 100, // 239: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 102, // 240: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 70, // 241: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 86, // 242: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 88, // 243: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 90, // 244: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 92, // 245: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 103, // 246: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 105, // 247: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 225, // 248: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 226, // 249: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 118, // 250: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 120, // 251: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 122, // 252: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 124, // 253: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 109, // 254: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 129, // 255: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 128, // 256: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 131, // 257: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 141, // 258: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 62, // 259: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 151, // 260: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 153, // 261: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 155, // 262: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 157, // 263: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 159, // 264: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 161, // 265: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 163, // 266: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 165, // 267: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 168, // 268: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 7, // 269: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 9, // 270: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 174, // 271: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 176, // 272: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 178, // 273: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 180, // 274: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 183, // 275: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 185, // 276: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 187, // 277: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 214, // [214:278] is the sub-list for method output_type - 150, // [150:214] is the sub-list for method input_type - 150, // [150:150] is the sub-list for extension type_name - 150, // [150:150] is the sub-list for extension extendee - 0, // [0:150] is the sub-list for field type_name + 196, // 19: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 20, // 20: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 197, // 21: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 198, // 22: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 19, // 23: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 19, // 24: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 219, // 25: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 19, // 26: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 19, // 27: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 49, // 28: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 216, // 29: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 48, // 30: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 199, // 31: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 53, // 32: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 54, // 33: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 55, // 34: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 142, // 35: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 143, // 36: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 57, // 37: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 52, // 38: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 60, // 39: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 216, // 40: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 19, // 41: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 64, // 42: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 26, // 43: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 65, // 44: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 153, // 45: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 200, // 46: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 219, // 47: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 219, // 48: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 201, // 49: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + 219, // 50: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 219, // 51: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 95, // 52: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 77, // 53: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 1, // 54: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 78, // 55: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 83, // 56: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 79, // 57: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 2, // 58: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 81, // 59: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 82, // 60: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 2, // 61: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 216, // 62: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 2, // 63: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 202, // 64: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + 203, // 65: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + 84, // 66: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 2, // 67: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 204, // 68: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 84, // 69: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 84, // 70: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 3, // 71: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 80, // 72: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 220, // 73: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 221, // 74: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 85, // 75: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 205, // 76: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 216, // 77: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 95, // 78: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile + 95, // 79: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 95, // 80: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 75, // 81: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 76, // 82: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 95, // 83: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 75, // 84: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 76, // 85: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 95, // 86: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 75, // 87: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 76, // 88: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 109, // 89: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 206, // 90: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 207, // 91: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + 208, // 92: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 209, // 93: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 217, // 94: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 222, // 95: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 115, // 96: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 210, // 97: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 116, // 98: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 117, // 99: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 118, // 100: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 119, // 101: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 120, // 102: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 121, // 103: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 223, // 104: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 224, // 105: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 225, // 106: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 211, // 107: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 129, // 108: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 129, // 109: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 4, // 110: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 4, // 111: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 217, // 112: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 212, // 113: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 64, // 114: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 64, // 115: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 136, // 116: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 139, // 117: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 146, // 118: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 147, // 119: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 137, // 120: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 138, // 121: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 140, // 122: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 141, // 123: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 147, // 124: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 142, // 125: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 143, // 126: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 144, // 127: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 148, // 128: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 150, // 129: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 223, // 130: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 149, // 131: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 152, // 132: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 151, // 133: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 152, // 134: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 223, // 135: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 171, // 136: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 217, // 137: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 213, // 138: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry + 223, // 139: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 214, // 140: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry + 215, // 141: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 226, // 142: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 226, // 143: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 226, // 144: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 216, // 145: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 6, // 146: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 6, // 147: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 185, // 148: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 185, // 149: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 80, // 150: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 110, // 151: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 11, // 152: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 13, // 153: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 15, // 154: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 27, // 155: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 28, // 156: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 29, // 157: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 30, // 158: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 31, // 159: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 32, // 160: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 33, // 161: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 40, // 162: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 42, // 163: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 43, // 164: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 44, // 165: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 46, // 166: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 50, // 167: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 52, // 168: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 58, // 169: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 59, // 170: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 66, // 171: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 67, // 172: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 68, // 173: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 73, // 174: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 74, // 175: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 99, // 176: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 101, // 177: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 103, // 178: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 69, // 179: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 87, // 180: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 89, // 181: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 91, // 182: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 93, // 183: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 70, // 184: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 106, // 185: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 227, // 186: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 228, // 187: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 114, // 188: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 123, // 189: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 125, // 190: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 127, // 191: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 108, // 192: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 112, // 193: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 130, // 194: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 131, // 195: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 134, // 196: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 145, // 197: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 62, // 198: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 154, // 199: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 156, // 200: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 158, // 201: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 160, // 202: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 162, // 203: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 164, // 204: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 166, // 205: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 168, // 206: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 170, // 207: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 7, // 208: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 9, // 209: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 177, // 210: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 179, // 211: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 181, // 212: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 183, // 213: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 186, // 214: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 188, // 215: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 190, // 216: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 12, // 217: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 14, // 218: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 16, // 219: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 34, // 220: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 34, // 221: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 35, // 222: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 36, // 223: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 37, // 224: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 38, // 225: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 39, // 226: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 41, // 227: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 49, // 228: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 49, // 229: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 45, // 230: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 47, // 231: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 51, // 232: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 56, // 233: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 58, // 234: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 56, // 235: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 71, // 236: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 71, // 237: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 72, // 238: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 98, // 239: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 97, // 240: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 100, // 241: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 102, // 242: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 104, // 243: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 71, // 244: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 88, // 245: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 90, // 246: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 92, // 247: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 94, // 248: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 105, // 249: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 107, // 250: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 229, // 251: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 230, // 252: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 122, // 253: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 124, // 254: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 126, // 255: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 128, // 256: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 111, // 257: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 113, // 258: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 133, // 259: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 132, // 260: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 135, // 261: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 145, // 262: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 63, // 263: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 155, // 264: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 157, // 265: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 159, // 266: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 161, // 267: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 163, // 268: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 165, // 269: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 167, // 270: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 169, // 271: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 172, // 272: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 8, // 273: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 10, // 274: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 178, // 275: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 180, // 276: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 182, // 277: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 184, // 278: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 187, // 279: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 189, // 280: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 191, // 281: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 217, // [217:282] is the sub-list for method output_type + 152, // [152:217] is the sub-list for method input_type + 152, // [152:152] is the sub-list for extension type_name + 152, // [152:152] is the sub-list for extension extendee + 0, // [0:152] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -14580,8 +14893,8 @@ func file_openshell_proto_init() { (*SandboxStreamEvent_Warning)(nil), (*SandboxStreamEvent_DraftPolicyUpdate)(nil), } - file_openshell_proto_msgTypes[81].OneofWrappers = []any{} - file_openshell_proto_msgTypes[105].OneofWrappers = []any{ + file_openshell_proto_msgTypes[82].OneofWrappers = []any{} + file_openshell_proto_msgTypes[108].OneofWrappers = []any{ (*PolicyMergeOperation_AddRule)(nil), (*PolicyMergeOperation_RemoveEndpoint)(nil), (*PolicyMergeOperation_RemoveRule)(nil), @@ -14589,36 +14902,36 @@ func file_openshell_proto_init() { (*PolicyMergeOperation_AddAllowRules)(nil), (*PolicyMergeOperation_RemoveBinary)(nil), } - file_openshell_proto_msgTypes[124].OneofWrappers = []any{ + file_openshell_proto_msgTypes[127].OneofWrappers = []any{ (*SupervisorMessage_Hello)(nil), (*SupervisorMessage_Heartbeat)(nil), (*SupervisorMessage_RelayOpenResult)(nil), (*SupervisorMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[125].OneofWrappers = []any{ + file_openshell_proto_msgTypes[128].OneofWrappers = []any{ (*GatewayMessage_SessionAccepted)(nil), (*GatewayMessage_SessionRejected)(nil), (*GatewayMessage_Heartbeat)(nil), (*GatewayMessage_RelayOpen)(nil), (*GatewayMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[131].OneofWrappers = []any{ + file_openshell_proto_msgTypes[134].OneofWrappers = []any{ (*RelayOpen_Ssh)(nil), (*RelayOpen_Tcp)(nil), } - file_openshell_proto_msgTypes[135].OneofWrappers = []any{ + file_openshell_proto_msgTypes[138].OneofWrappers = []any{ (*RelayFrame_Init)(nil), (*RelayFrame_Data)(nil), } - file_openshell_proto_msgTypes[165].OneofWrappers = []any{} - file_openshell_proto_msgTypes[166].OneofWrappers = []any{} + file_openshell_proto_msgTypes[168].OneofWrappers = []any{} + file_openshell_proto_msgTypes[169].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_openshell_proto_rawDesc), len(file_openshell_proto_rawDesc)), - NumEnums: 6, - NumMessages: 206, + NumEnums: 7, + NumMessages: 209, NumExtensions: 0, NumServices: 1, }, diff --git a/sdk/go/proto/openshellv1/openshell_grpc.pb.go b/sdk/go/proto/openshellv1/openshell_grpc.pb.go index 40d625a394..4ccc875fc8 100644 --- a/sdk/go/proto/openshellv1/openshell_grpc.pb.go +++ b/sdk/go/proto/openshellv1/openshell_grpc.pb.go @@ -64,6 +64,7 @@ const ( OpenShell_ListSandboxPolicies_FullMethodName = "/openshell.v1.OpenShell/ListSandboxPolicies" OpenShell_ReportPolicyStatus_FullMethodName = "/openshell.v1.OpenShell/ReportPolicyStatus" OpenShell_GetSandboxProviderEnvironment_FullMethodName = "/openshell.v1.OpenShell/GetSandboxProviderEnvironment" + OpenShell_ExchangeProviderSubjectToken_FullMethodName = "/openshell.v1.OpenShell/ExchangeProviderSubjectToken" OpenShell_GetSandboxLogs_FullMethodName = "/openshell.v1.OpenShell/GetSandboxLogs" OpenShell_PushSandboxLogs_FullMethodName = "/openshell.v1.OpenShell/PushSandboxLogs" OpenShell_ConnectSupervisor_FullMethodName = "/openshell.v1.OpenShell/ConnectSupervisor" @@ -193,6 +194,9 @@ type OpenShellClient interface { ReportPolicyStatus(ctx context.Context, in *ReportPolicyStatusRequest, opts ...grpc.CallOption) (*ReportPolicyStatusResponse, error) // Get provider environment for a sandbox (called by sandbox supervisor at startup). GetSandboxProviderEnvironment(ctx context.Context, in *GetSandboxProviderEnvironmentRequest, opts ...grpc.CallOption) (*GetSandboxProviderEnvironmentResponse, error) + // Exchange a stored provider subject token for an intermediate token scoped + // to the calling supervisor's SPIFFE identity. + ExchangeProviderSubjectToken(ctx context.Context, in *ExchangeProviderSubjectTokenRequest, opts ...grpc.CallOption) (*ExchangeProviderSubjectTokenResponse, error) // Fetch recent sandbox logs (one-shot). GetSandboxLogs(ctx context.Context, in *GetSandboxLogsRequest, opts ...grpc.CallOption) (*GetSandboxLogsResponse, error) // Push sandbox supervisor logs to the server (client-streaming). @@ -704,6 +708,16 @@ func (c *openShellClient) GetSandboxProviderEnvironment(ctx context.Context, in return out, nil } +func (c *openShellClient) ExchangeProviderSubjectToken(ctx context.Context, in *ExchangeProviderSubjectTokenRequest, opts ...grpc.CallOption) (*ExchangeProviderSubjectTokenResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExchangeProviderSubjectTokenResponse) + err := c.cc.Invoke(ctx, OpenShell_ExchangeProviderSubjectToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) GetSandboxLogs(ctx context.Context, in *GetSandboxLogsRequest, opts ...grpc.CallOption) (*GetSandboxLogsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetSandboxLogsResponse) @@ -1056,6 +1070,9 @@ type OpenShellServer interface { ReportPolicyStatus(context.Context, *ReportPolicyStatusRequest) (*ReportPolicyStatusResponse, error) // Get provider environment for a sandbox (called by sandbox supervisor at startup). GetSandboxProviderEnvironment(context.Context, *GetSandboxProviderEnvironmentRequest) (*GetSandboxProviderEnvironmentResponse, error) + // Exchange a stored provider subject token for an intermediate token scoped + // to the calling supervisor's SPIFFE identity. + ExchangeProviderSubjectToken(context.Context, *ExchangeProviderSubjectTokenRequest) (*ExchangeProviderSubjectTokenResponse, error) // Fetch recent sandbox logs (one-shot). GetSandboxLogs(context.Context, *GetSandboxLogsRequest) (*GetSandboxLogsResponse, error) // Push sandbox supervisor logs to the server (client-streaming). @@ -1265,6 +1282,9 @@ func (UnimplementedOpenShellServer) ReportPolicyStatus(context.Context, *ReportP func (UnimplementedOpenShellServer) GetSandboxProviderEnvironment(context.Context, *GetSandboxProviderEnvironmentRequest) (*GetSandboxProviderEnvironmentResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetSandboxProviderEnvironment not implemented") } +func (UnimplementedOpenShellServer) ExchangeProviderSubjectToken(context.Context, *ExchangeProviderSubjectTokenRequest) (*ExchangeProviderSubjectTokenResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ExchangeProviderSubjectToken not implemented") +} func (UnimplementedOpenShellServer) GetSandboxLogs(context.Context, *GetSandboxLogsRequest) (*GetSandboxLogsResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetSandboxLogs not implemented") } @@ -2064,6 +2084,24 @@ func _OpenShell_GetSandboxProviderEnvironment_Handler(srv interface{}, ctx conte return interceptor(ctx, in, info, handler) } +func _OpenShell_ExchangeProviderSubjectToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExchangeProviderSubjectTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ExchangeProviderSubjectToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ExchangeProviderSubjectToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ExchangeProviderSubjectToken(ctx, req.(*ExchangeProviderSubjectTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_GetSandboxLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetSandboxLogsRequest) if err := dec(in); err != nil { @@ -2597,6 +2635,10 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetSandboxProviderEnvironment", Handler: _OpenShell_GetSandboxProviderEnvironment_Handler, }, + { + MethodName: "ExchangeProviderSubjectToken", + Handler: _OpenShell_ExchangeProviderSubjectToken_Handler, + }, { MethodName: "GetSandboxLogs", Handler: _OpenShell_GetSandboxLogs_Handler,