diff --git a/.agents/skills/debug-openshell-cluster/SKILL.md b/.agents/skills/debug-openshell-cluster/SKILL.md index 6a9bcd25b9..19bcaf2b68 100644 --- a/.agents/skills/debug-openshell-cluster/SKILL.md +++ b/.agents/skills/debug-openshell-cluster/SKILL.md @@ -354,14 +354,20 @@ kubectl -n openshell get statefulset openshell -o jsonpath='{.spec.template.spec 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 c30f890914..7441a6bb19 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", ] @@ -4156,6 +4285,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "socket2 0.6.3", + "spiffe", "sqlx", "tempfile", "thiserror 2.0.18", @@ -4434,30 +4564,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", ] @@ -4467,11 +4621,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", ] @@ -4846,21 +5000,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", @@ -5377,6 +5540,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", ] [[package]] @@ -5420,13 +5584,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", ] @@ -5483,7 +5657,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", @@ -5509,16 +5683,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", @@ -5535,8 +5709,8 @@ dependencies = [ "ml-kem", "module-lattice", "num-bigint", - "p256", - "p384", + "p256 0.14.0", + "p384 0.14.0", "p521", "pageant", "pbkdf2", @@ -5551,7 +5725,7 @@ dependencies = [ "russh-util", "salsa20", "scrypt", - "sec1", + "sec1 0.8.1", "sha1 0.11.0", "sha2 0.11.0", "sha3 0.12.0", @@ -5853,13 +6027,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", @@ -6056,7 +6244,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9af4a3e75ebd5599b30d4de5768e00b5095d518a79fefc3ecbaf77e665d1ec06" dependencies = [ - "base16ct", + "base16ct 1.0.0", "serde", ] @@ -6274,6 +6462,7 @@ dependencies = [ "fastrand", "futures", "hyper-util", + "jsonwebtoken", "log", "prost", "prost-types", @@ -6540,7 +6729,7 @@ checksum = "7b54d0ed0498daf3f78d82e00e28c8eec9d75a067c4cfbcc7a0f7d0f4077749e" dependencies = [ "base64ct", "bytes", - "crypto-bigint", + "crypto-bigint 0.7.5", "ctutils", "digest 0.11.2", "pem-rfc7468 1.0.0", @@ -6556,15 +6745,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", @@ -8338,8 +8527,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 e6ec528bd8..e92b5525bc 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -262,12 +262,39 @@ 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 caching follows response-derived and profile override TTL rules. +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 +either a stored provider subject credential or the sandbox creator's delegated +OIDC identity, plus the gateway's own SPIFFE JWT-SVID; the supervisor then +exchanges that intermediate token for the final upstream token using its own +JWT-SVID. Delegated identity is opt-in at sandbox creation, stores one +gateway-scoped credential per issuer/client/user subject, and stores only a +per-sandbox authorization window on the sandbox. Only the delegating user can +extend or withdraw that window; workspace admins and platform admins can still +delete the sandbox to recover workspace resources. The gateway rejects exchange +after expiry, withdrawal, missing credential state, or credential revocation. +The gateway validates that its own JWT-SVID has the +requested audience, a SPIFFE subject, and a non-expired `exp` claim when +present. For provider-credential subject tokens, it also validates that the +stored subject credential is declared by the provider profile. For delegated +identity subject tokens, it validates that the sandbox was created with active +delegation for the stored credential principal. The gateway also verifies 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 7cefd3669a..319eb5204c 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -564,6 +564,13 @@ enum Commands { command: Option, }, + /// Manage delegated identity credential records. + #[command(help_template = SUBCOMMAND_HELP_TEMPLATE)] + DelegatedCredential { + #[command(subcommand)] + command: Option, + }, + /// Manage workspaces. #[command(alias = "ws", after_help = WORKSPACE_EXAMPLES, help_template = SUBCOMMAND_HELP_TEMPLATE)] Workspace { @@ -811,7 +818,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 +829,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 +843,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 +924,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", @@ -1094,6 +1109,62 @@ enum ProviderProfileCommands { }, } +#[derive(Subcommand, Debug)] +enum DelegatedCredentialCommands { + /// List delegated identity credentials. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + List { + /// Maximum number of credentials to return. + #[arg(long, default_value_t = 100)] + limit: u32, + + /// Number of credentials to skip. + #[arg(long, default_value_t = 0)] + offset: u32, + + /// Output only credential IDs. + #[arg(long, conflicts_with = "output")] + ids: bool, + + /// Output format. + #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table, conflicts_with = "ids")] + output: OutputFormat, + }, + + /// Show delegated identity credential status. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + Status { + /// Delegated identity credential ID. + id: String, + + /// Output format. + #[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)] + output: OutputFormat, + }, + + /// Revoke a delegated identity credential. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + Revoke { + /// Delegated identity credential ID. + id: String, + + /// Expected resource version for compare-and-swap updates. + #[arg(long = "resource-version", default_value_t = 0)] + resource_version: u64, + }, + + /// Delete a delegated identity credential record. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + Delete { + /// Delegated identity credential ID. + id: String, + + /// Expected resource version for compare-and-swap deletes. + #[arg(long = "resource-version", default_value_t = 0)] + resource_version: u64, + }, +} + // ----------------------------------------------------------------------- // Gateway commands (replaces the old `cluster` / `cluster admin` groups) // ----------------------------------------------------------------------- @@ -1397,6 +1468,11 @@ enum SandboxCommands { #[arg(long = "provider")] providers: Vec, + /// Delegate the current OIDC identity to the sandbox for a bounded duration. + /// Accepts positive durations with m, h, or d suffixes, for example 30m, 8h, or 7d. + #[arg(long = "delegate-identity-for", value_name = "DURATION")] + delegate_identity_for: Option, + /// Path to a custom sandbox policy YAML file. /// Overrides the built-in default and the `OPENSHELL_SANDBOX_POLICY` env var. #[arg(long, value_hint = ValueHint::FilePath)] @@ -1651,6 +1727,41 @@ enum SandboxCommands { /// Manage providers attached to a sandbox. #[command(subcommand)] Provider(SandboxProviderCommands), + + /// Manage sandbox delegated identity. + #[command(subcommand, name = "delegated-identity")] + DelegatedIdentity(SandboxDelegatedIdentityCommands), +} + +#[derive(Subcommand, Debug)] +enum SandboxDelegatedIdentityCommands { + /// Show delegated identity status for a sandbox. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + Status { + /// Sandbox name. + #[arg(add = ArgValueCompleter::new(completers::complete_sandbox_names))] + name: String, + }, + + /// Withdraw delegated identity from a sandbox. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + Withdraw { + /// Sandbox name. + #[arg(add = ArgValueCompleter::new(completers::complete_sandbox_names))] + name: String, + }, + + /// Set delegated identity expiry to now plus the requested duration. + #[command(help_template = LEAF_HELP_TEMPLATE, next_help_heading = "FLAGS")] + Extend { + /// Sandbox name. + #[arg(add = ArgValueCompleter::new(completers::complete_sandbox_names))] + name: String, + + /// New authorization window, for example 24h. + #[arg(long = "for", value_name = "DURATION")] + duration: String, + }, } #[derive(Subcommand, Debug)] @@ -2971,6 +3082,7 @@ async fn run_async() -> Result<()> { memory, driver_config_json, providers, + delegate_identity_for, policy, forward, tty, @@ -3061,6 +3173,7 @@ async fn run_async() -> Result<()> { driver_config_json: driver_config_json.as_deref(), editor, providers: &providers, + delegate_identity_for: delegate_identity_for.as_deref(), policy: policy.as_deref(), forward, command: &command, @@ -3271,6 +3384,37 @@ async fn run_async() -> Result<()> { .await?; } }, + SandboxCommands::DelegatedIdentity(command) => match command { + SandboxDelegatedIdentityCommands::Status { name } => { + run::sandbox_delegated_identity_status( + endpoint, + &name, + &cli.workspace, + &tls, + ) + .await?; + } + SandboxDelegatedIdentityCommands::Withdraw { name } => { + run::sandbox_delegated_identity_withdraw( + endpoint, + &name, + &cli.workspace, + &tls, + ) + .await?; + } + SandboxDelegatedIdentityCommands::Extend { name, duration } => { + run::sandbox_delegated_identity_extend( + endpoint, + &ctx.name, + &name, + &duration, + &cli.workspace, + &tls, + ) + .await?; + } + }, } } } @@ -3352,24 +3496,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 { @@ -3522,20 +3676,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 } => { @@ -3543,6 +3699,61 @@ async fn run_async() -> Result<()> { } } } + Some(Commands::DelegatedCredential { + command: Some(command), + }) => { + let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; + let endpoint = &ctx.endpoint; + let mut tls = tls.with_gateway_name(&ctx.name); + apply_auth(&mut tls, &ctx.name); + + match command { + DelegatedCredentialCommands::List { + limit, + offset, + ids, + output, + } => { + run::delegated_identity_credential_list( + endpoint, + limit, + offset, + ids, + output.as_str(), + &tls, + ) + .await?; + } + DelegatedCredentialCommands::Status { id, output } => { + run::delegated_identity_credential_status(endpoint, &id, output.as_str(), &tls) + .await?; + } + DelegatedCredentialCommands::Revoke { + id, + resource_version, + } => { + run::delegated_identity_credential_revoke( + endpoint, + &id, + resource_version, + &tls, + ) + .await?; + } + DelegatedCredentialCommands::Delete { + id, + resource_version, + } => { + run::delegated_identity_credential_delete( + endpoint, + &id, + resource_version, + &tls, + ) + .await?; + } + } + } Some(Commands::Term { theme }) => { let ctx = resolve_gateway(&cli.gateway, &cli.gateway_endpoint)?; let mut tls = tls.with_gateway_name(&ctx.name); @@ -3674,6 +3885,13 @@ async fn run_async() -> Result<()> { .print_help() .expect("Failed to print help"); } + Some(Commands::DelegatedCredential { command: None }) => { + Cli::command() + .find_subcommand_mut("delegated-credential") + .expect("delegated-credential subcommand exists") + .print_help() + .expect("Failed to print help"); + } Some(Commands::Gateway { command: None }) => { Cli::command() .find_subcommand_mut("gateway") @@ -4394,6 +4612,60 @@ mod tests { )); } + #[test] + fn delegated_credential_commands_parse() { + let list = Cli::try_parse_from(["openshell", "delegated-credential", "list", "--ids"]) + .expect("delegated credential list should parse"); + assert!(matches!( + list.command, + Some(Commands::DelegatedCredential { + command: Some(DelegatedCredentialCommands::List { + ids: true, + output: OutputFormat::Table, + .. + }) + }) + )); + + let status = Cli::try_parse_from([ + "openshell", + "delegated-credential", + "status", + "delegated-identity-123", + "-o", + "json", + ]) + .expect("delegated credential status should parse"); + assert!(matches!( + status.command, + Some(Commands::DelegatedCredential { + command: Some(DelegatedCredentialCommands::Status { + id, + output: OutputFormat::Json, + }) + }) if id == "delegated-identity-123" + )); + + let revoke = Cli::try_parse_from([ + "openshell", + "delegated-credential", + "revoke", + "delegated-identity-123", + "--resource-version", + "7", + ]) + .expect("delegated credential revoke should parse"); + assert!(matches!( + revoke.command, + Some(Commands::DelegatedCredential { + command: Some(DelegatedCredentialCommands::Revoke { + id, + resource_version: 7, + }) + }) if id == "delegated-identity-123" + )); + } + #[test] fn provider_profile_commands_parse() { let export = Cli::try_parse_from([ 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 e376d08dc5..9b4ca2dd16 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -23,7 +23,7 @@ pub use crate::commands::gateway::{ }; use crate::policy_update::build_policy_update_plan; -use crate::tls::{TlsOptions, grpc_client, grpc_inference_client}; +use crate::tls::{GrpcClient, TlsOptions, grpc_client, grpc_inference_client}; use dialoguer::Confirm; use futures::StreamExt; use indicatif::{ProgressBar, ProgressStyle}; @@ -36,24 +36,29 @@ use openshell_core::proto::ProviderProfileCategory; use openshell_core::proto::{ ApproveAllDraftChunksRequest, ApproveDraftChunkRequest, AttachSandboxProviderRequest, ClearDraftChunksRequest, ConfigureProviderRefreshRequest, CreateProviderRequest, - CreateSandboxRequest, CreateSshSessionRequest, DeleteInferenceRouteRequest, - DeleteProviderProfileRequest, DeleteProviderRefreshRequest, DeleteProviderRequest, - DeleteSandboxRequest, DeleteServiceRequest, DetachSandboxProviderRequest, ExecSandboxRequest, - ExposeServiceRequest, GetCurrentUserRequest, GetDraftHistoryRequest, GetDraftPolicyRequest, + CreateSandboxRequest, CreateSshSessionRequest, DelegatedIdentityCredentialSummary, + DelegatedIdentityRequest, DeleteDelegatedIdentityCredentialRequest, + DeleteInferenceRouteRequest, DeleteProviderProfileRequest, DeleteProviderRefreshRequest, + DeleteProviderRequest, DeleteSandboxRequest, DeleteServiceRequest, + DetachSandboxProviderRequest, ExecSandboxRequest, ExposeServiceRequest, + ExtendSandboxDelegatedIdentityRequest, GetCurrentUserRequest, + GetDelegatedIdentityCredentialStatusRequest, GetDraftHistoryRequest, GetDraftPolicyRequest, GetGatewayConfigRequest, GetInferenceRouteRequest, GetProviderProfileRequest, GetProviderRefreshStatusRequest, GetProviderRequest, GetSandboxConfigRequest, - GetSandboxConfigResponse, GetSandboxLogsRequest, GetSandboxPolicyStatusRequest, - GetSandboxRequest, GetServiceRequest, GpuResourceRequirements, ImportProviderProfilesRequest, - LintProviderProfilesRequest, ListProviderProfilesRequest, ListProvidersRequest, + GetSandboxConfigResponse, GetSandboxDelegatedIdentityStatusRequest, GetSandboxLogsRequest, + GetSandboxPolicyStatusRequest, GetSandboxRequest, GetServiceRequest, GpuResourceRequirements, + ImportProviderProfilesRequest, LintProviderProfilesRequest, + ListDelegatedIdentityCredentialsRequest, 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, + ProviderCredentialRefreshStrategy, ProviderCredentialTokenGrantType, ProviderProfile, + ProviderProfileDiagnostic, ProviderProfileImportItem, RejectDraftChunkRequest, + ResourceRequirements, RevokeDelegatedIdentityCredentialRequest, RevokeSshSessionRequest, + RotateProviderCredentialRequest, Sandbox, SandboxPhase, SandboxPolicy, SandboxSpec, + SandboxTemplate, ServiceEndpointResponse, SetInferenceRouteRequest, SettingScope, StartSandboxRequest, StopSandboxRequest, TcpForwardFrame, TcpForwardInit, TcpRelayTarget, UpdateConfigRequest, UpdateProviderProfilesRequest, UpdateProviderRequest, WatchSandboxRequest, - exec_sandbox_event, setting_value, tcp_forward_init, + WithdrawSandboxDelegatedIdentityRequest, exec_sandbox_event, setting_value, tcp_forward_init, }; use openshell_core::settings; use openshell_core::{ObjectId, ObjectName, ObjectWorkspace}; @@ -383,6 +388,7 @@ pub struct SandboxCreateConfig<'a> { pub environment: HashMap, pub approval_mode: &'a str, pub output: &'a str, + pub delegate_identity_for: Option<&'a str>, } impl Default for SandboxCreateConfig<'_> { @@ -407,6 +413,7 @@ impl Default for SandboxCreateConfig<'_> { environment: HashMap::new(), approval_mode: "manual", output: "table", + delegate_identity_for: None, } } } @@ -439,6 +446,7 @@ pub async fn sandbox_create( environment, approval_mode, output, + delegate_identity_for, } = config; if editor.is_some() && !command.is_empty() { @@ -501,6 +509,9 @@ pub async fn sandbox_create( workspace, ) .await?; + if delegate_identity_for.is_none() { + warn_delegated_identity_profiles(&mut client, &configured_providers, workspace).await?; + } let policy = load_sandbox_policy(policy)?; let resource_limits = build_sandbox_resource_limits(cpu, memory)?; @@ -520,6 +531,8 @@ pub async fn sandbox_create( }; let resource_requirements = gpu_requirements.map(|gpu| ResourceRequirements { gpu: Some(gpu) }); + let delegated_identity = + delegated_identity_request(gateway_name, tls, delegate_identity_for).await?; let request = CreateSandboxRequest { spec: Some(SandboxSpec { @@ -534,6 +547,7 @@ pub async fn sandbox_create( labels, annotations: HashMap::new(), workspace: workspace.to_string(), + delegated_identity, }; let response = match client.create_sandbox(request).await { @@ -1260,6 +1274,622 @@ pub async fn sandbox_sync_command( Ok(()) } +async fn delegated_identity_request( + gateway_name: &str, + tls: &TlsOptions, + duration: Option<&str>, +) -> Result> { + let Some(duration) = duration else { + return Ok(None); + }; + let duration_ms = parse_delegated_identity_duration_ms(duration)?; + 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 delegated identity: {err}" + ) + })?; + let scopes = openshell_bootstrap::load_gateway_metadata(gateway_name) + .ok() + .and_then(|metadata| metadata.oidc_scopes); + let bundle = + crate::oidc_auth::oidc_refresh_token(&bundle, scopes.as_deref(), tls.gateway_insecure) + .await + .and_then(|refreshed| { + openshell_bootstrap::oidc_token::store_oidc_token(gateway_name, &refreshed)?; + Ok(refreshed) + }) + .map_err(|err| { + miette::miette!( + "failed to refresh local OIDC token for delegated identity: {err}\n\ + Re-authenticate with `openshell gateway logout` followed by `openshell gateway login`, then retry this command." + ) + })?; + let refresh_token = bundle.refresh_token.ok_or_else(|| { + miette::miette!( + "--delegate-identity-for requires a local OIDC refresh token; run `openshell gateway login` and ensure the gateway OIDC client issues refresh tokens" + ) + })?; + let now_ms = current_time_ms(); + Ok(Some(DelegatedIdentityRequest { + delegated_until_ms: now_ms.saturating_add(duration_ms), + issuer: bundle.issuer, + client_id: bundle.client_id, + refresh_token, + access_token: bundle.access_token, + scopes: scopes.unwrap_or_default(), + audience: openshell_bootstrap::load_gateway_metadata(gateway_name) + .ok() + .and_then(|metadata| metadata.oidc_audience) + .unwrap_or_default(), + })) +} + +fn parse_delegated_identity_duration_ms(value: &str) -> Result { + let value = value.trim(); + let (number, multiplier): (&str, i64) = match value.as_bytes().last().copied() { + Some(b'm') => (&value[..value.len() - 1], 60_000), + Some(b'h') => (&value[..value.len() - 1], 3_600_000), + Some(b'd') => (&value[..value.len() - 1], 86_400_000), + _ => { + return Err(miette::miette!( + "invalid delegated identity duration '{value}'; use a positive duration with m, h, or d suffix" + )); + } + }; + let amount = number.parse::().map_err(|_| { + miette::miette!( + "invalid delegated identity duration '{value}'; use a positive integer with m, h, or d suffix" + ) + })?; + if amount <= 0 { + return Err(miette::miette!( + "delegated identity duration must be greater than zero" + )); + } + amount + .checked_mul(multiplier) + .ok_or_else(|| miette::miette!("delegated identity duration is too large")) +} + +fn current_time_ms() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() + .try_into() + .unwrap_or(i64::MAX) +} + +async fn warn_delegated_identity_profiles( + client: &mut GrpcClient, + provider_names: &[String], + workspace: &str, +) -> Result<()> { + for provider_name in provider_names { + let provider = client + .get_provider(GetProviderRequest { + name: provider_name.clone(), + workspace: workspace.to_string(), + }) + .await + .map_err(|status| miette::miette!(status.to_string()))? + .into_inner() + .provider; + let Some(provider) = provider else { + continue; + }; + let profile_id = normalize_provider_type(&provider.r#type).unwrap_or(&provider.r#type); + let profile = client + .get_provider_profile(GetProviderProfileRequest { + id: profile_id.to_string(), + workspace: provider.profile_workspace.clone(), + }) + .await + .ok() + .and_then(|response| response.into_inner().profile); + let Some(profile) = profile else { + continue; + }; + if profile_uses_sandbox_delegated_identity(&profile) { + eprintln!( + "Provider '{provider_name}' uses sandbox delegated identity. Token exchange will fail because this sandbox was not created with delegated identity. Delete and recreate the sandbox with --delegate-identity-for= to enable it." + ); + } + } + Ok(()) +} + +fn profile_uses_sandbox_delegated_identity(profile: &ProviderProfile) -> bool { + profile.credentials.iter().any(|credential| { + credential + .token_grant + .as_ref() + .and_then(|grant| grant.subject_token.as_ref()) + .is_some_and(|subject| subject.source == "sandbox_delegated_identity") + }) +} + +pub async fn sandbox_delegated_identity_status( + server: &str, + name: &str, + workspace: &str, + tls: &TlsOptions, +) -> Result<()> { + let mut client = grpc_client(server, tls).await?; + let response = client + .get_sandbox_delegated_identity_status(GetSandboxDelegatedIdentityStatusRequest { + name: name.to_string(), + workspace: workspace.to_string(), + }) + .await + .map_err(|status| miette::miette!(status.to_string()))? + .into_inner(); + let Some(delegation) = response.delegated_identity else { + println!("Delegated identity: disabled"); + return Ok(()); + }; + let status = sandbox_delegation_status( + response.credential_missing, + response.credential_revoked_at_ms, + delegation.withdrawn_at_ms, + delegation.delegated_until_ms, + response.now_ms, + ); + println!("Delegated identity: enabled"); + println!("Credential ID: {}", delegation.credential_id); + println!("Principal: {}", delegation.principal_subject); + println!("Status: {status}"); + match status { + "active" => println!( + "Valid for: {}", + format_remaining_duration(delegation.delegated_until_ms, response.now_ms) + ), + "withdrawn" => println!( + "Withdrawn: {}", + format_age(delegation.withdrawn_at_ms, response.now_ms) + ), + "revoked" => println!( + "Revoked: {}", + format_age(response.credential_revoked_at_ms, response.now_ms) + ), + "credential-missing" => println!("Credential: missing"), + "expired" => println!( + "Expired: {}", + format_age(delegation.delegated_until_ms, response.now_ms) + ), + _ => {} + } + Ok(()) +} + +pub async fn sandbox_delegated_identity_withdraw( + server: &str, + name: &str, + workspace: &str, + tls: &TlsOptions, +) -> Result<()> { + let mut client = grpc_client(server, tls).await?; + let response = client + .withdraw_sandbox_delegated_identity(WithdrawSandboxDelegatedIdentityRequest { + name: name.to_string(), + workspace: workspace.to_string(), + }) + .await + .map_err(|status| miette::miette!(status.to_string()))? + .into_inner(); + let action = if response.withdrawn { + "Withdrew" + } else { + "Already withdrawn" + }; + println!("{action} delegated identity for sandbox {name}"); + Ok(()) +} + +pub async fn sandbox_delegated_identity_extend( + server: &str, + gateway_name: &str, + name: &str, + duration: &str, + workspace: &str, + tls: &TlsOptions, +) -> Result<()> { + let mut client = grpc_client(server, tls).await?; + let delegated_identity = delegated_identity_request(gateway_name, tls, Some(duration)) + .await? + .expect("duration was provided"); + let response = client + .extend_sandbox_delegated_identity(ExtendSandboxDelegatedIdentityRequest { + name: name.to_string(), + workspace: workspace.to_string(), + delegated_identity: Some(delegated_identity), + }) + .await + .map_err(|status| miette::miette!(status.to_string()))? + .into_inner(); + let sandbox_name = response + .sandbox + .as_ref() + .map(ObjectName::object_name) + .filter(|name| !name.is_empty()) + .unwrap_or(name); + println!("Extended delegated identity for sandbox {sandbox_name}"); + Ok(()) +} + +pub async fn delegated_identity_credential_list( + server: &str, + limit: u32, + offset: u32, + ids_only: bool, + output: &str, + tls: &TlsOptions, +) -> Result<()> { + let mut client = grpc_client(server, tls).await?; + let credentials = client + .list_delegated_identity_credentials(ListDelegatedIdentityCredentialsRequest { + limit, + offset, + }) + .await + .map_err(|status| miette::miette!(status.to_string()))? + .into_inner() + .credentials; + + if crate::output::print_output_collection( + output, + &credentials, + delegated_identity_credential_to_json, + )? { + return Ok(()); + } + + if credentials.is_empty() { + if !ids_only { + println!("No delegated identity credentials found."); + } + return Ok(()); + } + + if ids_only { + for credential in credentials { + println!("{}", delegated_credential_object_id(&credential)); + } + return Ok(()); + } + + print_delegated_identity_credential_table(&credentials, current_time_ms()); + Ok(()) +} + +pub async fn delegated_identity_credential_status( + server: &str, + id: &str, + output: &str, + tls: &TlsOptions, +) -> Result<()> { + let mut client = grpc_client(server, tls).await?; + let response = client + .get_delegated_identity_credential_status(GetDelegatedIdentityCredentialStatusRequest { + id: id.to_string(), + }) + .await + .map_err(|status| miette::miette!(status.to_string()))? + .into_inner(); + let credential = response + .credential + .ok_or_else(|| miette::miette!("delegated identity credential missing from response"))?; + let view = serde_json::json!({ + "credential": delegated_identity_credential_to_json(&credential), + "now_ms": response.now_ms, + }); + if crate::output::print_output_single(output, &view, Clone::clone)? { + return Ok(()); + } + + println!("{}", "Delegated identity credential:".cyan().bold()); + println!(); + print_delegated_identity_credential_detail(&credential, response.now_ms); + Ok(()) +} + +pub async fn delegated_identity_credential_revoke( + server: &str, + id: &str, + expected_resource_version: u64, + tls: &TlsOptions, +) -> Result<()> { + let mut client = grpc_client(server, tls).await?; + let response = client + .revoke_delegated_identity_credential(RevokeDelegatedIdentityCredentialRequest { + id: id.to_string(), + expected_resource_version, + }) + .await + .map_err(|status| miette::miette!(status.to_string()))? + .into_inner(); + if response.revoked { + println!("Revoked delegated identity credential {id}"); + } else { + println!("Delegated identity credential {id} was already revoked"); + } + Ok(()) +} + +pub async fn delegated_identity_credential_delete( + server: &str, + id: &str, + expected_resource_version: u64, + tls: &TlsOptions, +) -> Result<()> { + let mut client = grpc_client(server, tls).await?; + let response = client + .delete_delegated_identity_credential(DeleteDelegatedIdentityCredentialRequest { + id: id.to_string(), + expected_resource_version, + }) + .await + .map_err(|status| miette::miette!(status.to_string()))? + .into_inner(); + if response.deleted { + println!("Deleted delegated identity credential {id}"); + } else { + println!("Delegated identity credential {id} was not found"); + } + Ok(()) +} + +fn delegated_identity_credential_to_json( + credential: &DelegatedIdentityCredentialSummary, +) -> serde_json::Value { + let metadata = credential.metadata.as_ref(); + serde_json::json!({ + "id": delegated_credential_object_id(credential), + "name": delegated_credential_object_name(credential), + "workspace": delegated_credential_object_workspace(credential), + "resource_version": metadata.map(|meta| meta.resource_version).unwrap_or_default(), + "created_at_ms": metadata.map(|meta| meta.created_at_ms).unwrap_or_default(), + "issuer": credential.issuer, + "client_id": credential.client_id, + "principal_subject": credential.principal_subject, + "access_token_present": credential.access_token_present, + "refresh_token_present": credential.refresh_token_present, + "access_token_expires_at_ms": credential.access_token_expires_at_ms, + "scopes": credential.scopes, + "audience": credential.audience, + "last_refresh_at_ms": credential.last_refresh_at_ms, + "revoked_at_ms": credential.revoked_at_ms, + }) +} + +fn print_delegated_identity_credential_table( + credentials: &[DelegatedIdentityCredentialSummary], + now: i64, +) { + println!( + "{:<84} {:<40} {:<24} {:<10} {:<16} {:<14} {:>8}", + "ID", "PRINCIPAL", "CLIENT_ID", "STATUS", "ACCESS_VALID_FOR", "LAST_REFRESH", "RV" + ); + println!("{}", "-".repeat(208)); + for credential in credentials { + let resource_version = credential + .metadata + .as_ref() + .map(|meta| meta.resource_version) + .unwrap_or_default(); + println!( + "{:<84} {:<40} {:<24} {:<10} {:<16} {:<14} {:>8}", + delegated_credential_object_id(credential), + credential.principal_subject, + truncate_for_table(&credential.client_id, 24), + delegated_credential_status(credential, now), + credential_access_valid_for(credential, now), + format_optional_age(credential.last_refresh_at_ms, now), + resource_version, + ); + } +} + +fn print_delegated_identity_credential_detail( + credential: &DelegatedIdentityCredentialSummary, + now: i64, +) { + let metadata = credential.metadata.as_ref(); + println!( + " {:<22} {}", + "id:", + delegated_credential_object_id(credential) + ); + println!(" {:<22} {}", "issuer:", credential.issuer); + println!(" {:<22} {}", "client_id:", credential.client_id); + println!( + " {:<22} {}", + "principal_subject:", credential.principal_subject + ); + println!( + " {:<22} {}", + "resource_version:", + metadata + .map(|meta| meta.resource_version) + .unwrap_or_default() + ); + println!( + " {:<22} {}", + "created_at_ms:", + metadata.map(|meta| meta.created_at_ms).unwrap_or_default() + ); + println!( + " {:<22} {}", + "access_token_present:", credential.access_token_present + ); + println!( + " {:<22} {}", + "refresh_token_present:", credential.refresh_token_present + ); + println!( + " {:<22} {}", + "status:", + delegated_credential_status(credential, now) + ); + println!( + " {:<22} {}", + "access_valid_for:", + credential_access_valid_for(credential, now) + ); + println!(" {:<22} {}", "scopes:", credential.scopes); + println!(" {:<22} {}", "audience:", credential.audience); + println!( + " {:<22} {}", + "last_refresh:", + format_optional_age(credential.last_refresh_at_ms, now) + ); + if credential.revoked_at_ms > 0 { + println!( + " {:<22} {}", + "revoked:", + format_age(credential.revoked_at_ms, now) + ); + } +} + +fn sandbox_delegation_status( + credential_missing: bool, + credential_revoked_at_ms: i64, + withdrawn_at_ms: i64, + delegated_until_ms: i64, + now_ms: i64, +) -> &'static str { + if credential_missing { + "credential-missing" + } else if credential_revoked_at_ms > 0 { + "revoked" + } else if withdrawn_at_ms > 0 { + "withdrawn" + } else if delegated_until_ms <= now_ms { + "expired" + } else { + "active" + } +} + +fn delegated_credential_status( + credential: &DelegatedIdentityCredentialSummary, + now: i64, +) -> &'static str { + if credential.revoked_at_ms > 0 { + "revoked" + } else if credential.access_token_expires_at_ms > 0 + && credential.access_token_expires_at_ms <= now + { + "expired" + } else { + "active" + } +} + +fn delegated_credential_object_id(credential: &DelegatedIdentityCredentialSummary) -> &str { + credential + .metadata + .as_ref() + .map(|metadata| metadata.id.as_str()) + .unwrap_or_default() +} + +fn delegated_credential_object_name(credential: &DelegatedIdentityCredentialSummary) -> &str { + credential + .metadata + .as_ref() + .map(|metadata| metadata.name.as_str()) + .unwrap_or_default() +} + +fn delegated_credential_object_workspace(credential: &DelegatedIdentityCredentialSummary) -> &str { + credential + .metadata + .as_ref() + .map(|metadata| metadata.workspace.as_str()) + .unwrap_or_default() +} + +fn credential_access_valid_for( + credential: &DelegatedIdentityCredentialSummary, + now: i64, +) -> String { + if delegated_credential_status(credential, now) != "active" { + "-".to_string() + } else if credential.access_token_expires_at_ms == 0 { + "unknown".to_string() + } else { + format_remaining_duration(credential.access_token_expires_at_ms, now) + } +} + +fn format_remaining_duration(until_ms: i64, now_ms: i64) -> String { + if until_ms <= now_ms { + "-".to_string() + } else { + format_compact_duration_ms(until_ms.saturating_sub(now_ms)) + } +} + +fn format_optional_age(timestamp_ms: i64, now_ms: i64) -> String { + if timestamp_ms > 0 { + format_age(timestamp_ms, now_ms) + } else { + "never".to_string() + } +} + +fn format_age(timestamp_ms: i64, now_ms: i64) -> String { + if timestamp_ms <= 0 { + return "never".to_string(); + } + if timestamp_ms > now_ms { + return format!( + "in {}", + format_compact_duration_ms(timestamp_ms.saturating_sub(now_ms)) + ); + } + format!( + "{} ago", + format_compact_duration_ms(now_ms.saturating_sub(timestamp_ms)) + ) +} + +fn format_compact_duration_ms(duration_ms: i64) -> String { + let seconds = duration_ms.saturating_add(999) / 1000; + if seconds < 60 { + return format!("{}s", seconds.max(0)); + } + let minutes = seconds / 60; + if minutes < 60 { + return format!("{minutes}m"); + } + let hours = minutes / 60; + if hours < 48 { + return format!("{hours}h"); + } + let days = hours / 24; + format!("{days}d") +} + +fn truncate_for_table(value: &str, max_len: usize) -> String { + if value.len() <= max_len { + value.to_string() + } else if max_len <= 1 { + ".".to_string() + } else { + let prefix = value + .chars() + .take(max_len.saturating_sub(3)) + .collect::(); + format!("{prefix}...") + } +} + /// Fetch a sandbox by name. /// /// Policy always comes from [`GetSandboxConfig`] (effective active policy, sandbox @@ -1614,7 +2244,7 @@ pub async fn service_forward_tcp( } async fn create_forward_session_token( - client: &mut crate::tls::GrpcClient, + client: &mut GrpcClient, sandbox_id: &str, ) -> std::result::Result { let response = client @@ -1627,7 +2257,7 @@ async fn create_forward_session_token( } async fn fetch_ready_sandbox_for_forward( - client: &mut crate::tls::GrpcClient, + client: &mut GrpcClient, name: &str, workspace: &str, ) -> Result { @@ -1717,7 +2347,7 @@ fn parse_tcp_forward_spec(local: Option<&str>, default_port: u16) -> Result<(Str } async fn forward_one_tcp_connection( - client: &mut crate::tls::GrpcClient, + client: &mut GrpcClient, socket: tokio::net::TcpStream, sandbox_id: String, target_host: String, @@ -1831,7 +2461,7 @@ impl Drop for TaskGuard { } async fn sandbox_exec_interactive_grpc( - mut client: crate::tls::GrpcClient, + mut client: GrpcClient, sandbox: &Sandbox, command: &[String], workdir: Option<&str>, @@ -2474,7 +3104,7 @@ pub async fn sandbox_start( } async fn wait_for_lifecycle_phase( - client: &mut crate::tls::GrpcClient, + client: &mut GrpcClient, sandbox: Sandbox, target: SandboxPhase, ) -> Result { @@ -2564,7 +3194,7 @@ fn inferred_provider_type(command: &[String]) -> Option { /// Returns a deduplicated list of provider **names** suitable for /// `SandboxSpec.providers`. pub async fn ensure_required_providers( - client: &mut crate::tls::GrpcClient, + client: &mut GrpcClient, explicit_names: &[String], inferred_types: &[String], auto_providers_override: Option, @@ -2686,7 +3316,7 @@ pub async fn ensure_required_providers( /// defaults to the type and retries with suffixes on conflict (used for /// inferred provider types). async fn auto_create_provider( - client: &mut crate::tls::GrpcClient, + client: &mut GrpcClient, provider_type: &str, preferred_name: Option<&str>, auto_providers_override: Option, @@ -3212,7 +3842,7 @@ fn read_gcloud_adc() -> Result<(String, String, String)> { } async fn rollback_provider_create_after_gcloud_adc_failure( - client: &mut crate::tls::GrpcClient, + client: &mut GrpcClient, provider_name: &str, stage: &str, source: &Status, @@ -3266,7 +3896,7 @@ fn service_url_for_gateway(service_url: &str, gateway_endpoint: &str) -> String service_url.to_string() } -async fn gateway_providers_v2_enabled(client: &mut crate::tls::GrpcClient) -> Result { +async fn gateway_providers_v2_enabled(client: &mut GrpcClient) -> Result { let response = client .get_gateway_config(GetGatewayConfigRequest {}) .await @@ -3286,7 +3916,7 @@ async fn gateway_providers_v2_enabled(client: &mut crate::tls::GrpcClient) -> Re } async fn fetch_provider_profile( - client: &mut crate::tls::GrpcClient, + client: &mut GrpcClient, provider_type: &str, workspace: &str, ) -> Result { @@ -3313,7 +3943,7 @@ async fn fetch_provider_profile( } async fn discover_existing_provider_data( - client: &mut crate::tls::GrpcClient, + client: &mut GrpcClient, provider_type: &str, workspace: &str, ) -> Result> { @@ -3380,6 +4010,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, @@ -3392,44 +4142,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() { @@ -3499,7 +4282,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 { @@ -3573,7 +4366,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(), }), @@ -4601,28 +5394,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. @@ -8541,4 +9379,63 @@ mod tests { assert!(json["revision"].is_null()); assert!(json["policy"].is_null()); } + + #[test] + fn delegated_identity_human_status_formats_are_state_oriented() { + let now = 10_000; + assert_eq!( + super::sandbox_delegation_status(false, 0, 0, 70_000, now), + "active" + ); + assert_eq!( + super::sandbox_delegation_status(false, 0, 9_000, 70_000, now), + "withdrawn" + ); + assert_eq!( + super::sandbox_delegation_status(false, 0, 0, 9_000, now), + "expired" + ); + assert_eq!( + super::sandbox_delegation_status(false, 8_000, 0, 70_000, now), + "revoked" + ); + assert_eq!( + super::sandbox_delegation_status(true, 0, 0, 70_000, now), + "credential-missing" + ); + + assert_eq!(super::format_remaining_duration(70_000, now), "1m"); + assert_eq!(super::format_age(7_000, now), "3s ago"); + assert_eq!(super::format_optional_age(0, now), "never"); + } + + #[test] + fn delegated_identity_credential_status_formats_are_redacted_and_readable() { + let now = 10_000; + let mut credential = openshell_core::proto::DelegatedIdentityCredentialSummary { + access_token_expires_at_ms: 70_000, + last_refresh_at_ms: 5_000, + ..Default::default() + }; + + assert_eq!( + super::delegated_credential_status(&credential, now), + "active" + ); + assert_eq!(super::credential_access_valid_for(&credential, now), "1m"); + + credential.access_token_expires_at_ms = 9_000; + assert_eq!( + super::delegated_credential_status(&credential, now), + "expired" + ); + assert_eq!(super::credential_access_valid_for(&credential, now), "-"); + + credential.revoked_at_ms = 8_000; + assert_eq!( + super::delegated_credential_status(&credential, now), + "revoked" + ); + assert_eq!(super::credential_access_valid_for(&credential, now), "-"); + } } diff --git a/crates/openshell-cli/tests/ensure_providers_integration.rs b/crates/openshell-cli/tests/ensure_providers_integration.rs index 3d628f2c10..8eb1e0458e 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, @@ -125,6 +126,64 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("unused")) } + async fn get_sandbox_delegated_identity_status( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn withdraw_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn extend_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn list_delegated_identity_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn get_delegated_identity_credential_status( + &self, + _request: tonic::Request< + openshell_core::proto::GetDelegatedIdentityCredentialStatusRequest, + >, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn revoke_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn delete_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + async fn get_sandbox( &self, _request: tonic::Request, @@ -234,6 +293,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 60ffbd61f8..4d687e639b 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; @@ -80,6 +81,64 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("unused")) } + async fn get_sandbox_delegated_identity_status( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn withdraw_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn extend_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn list_delegated_identity_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn get_delegated_identity_credential_status( + &self, + _request: tonic::Request< + openshell_core::proto::GetDelegatedIdentityCredentialStatusRequest, + >, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn revoke_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn delete_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + async fn get_sandbox( &self, _request: tonic::Request, @@ -206,6 +265,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 a87ff0a6d8..d0a8390ead 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, @@ -143,6 +144,64 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("unused")) } + async fn get_sandbox_delegated_identity_status( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn withdraw_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn extend_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn list_delegated_identity_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn get_delegated_identity_credential_status( + &self, + _request: tonic::Request< + openshell_core::proto::GetDelegatedIdentityCredentialStatusRequest, + >, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn revoke_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn delete_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + async fn get_sandbox( &self, request: tonic::Request, @@ -365,6 +424,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, @@ -1179,16 +1245,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"); @@ -1537,19 +1604,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"); @@ -2093,16 +2158,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"); @@ -2357,19 +2423,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"); @@ -2435,7 +2499,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()); @@ -2461,7 +2525,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 102cde3714..ca43f5088e 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, @@ -130,6 +131,64 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("unused")) } + async fn get_sandbox_delegated_identity_status( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn withdraw_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn extend_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn list_delegated_identity_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn get_delegated_identity_credential_status( + &self, + _request: tonic::Request< + openshell_core::proto::GetDelegatedIdentityCredentialStatusRequest, + >, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn revoke_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn delete_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + async fn get_sandbox( &self, request: tonic::Request, @@ -276,6 +335,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 41b93bab82..3dd7783caf 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, @@ -93,6 +94,64 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("unused")) } + async fn get_sandbox_delegated_identity_status( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn withdraw_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn extend_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn list_delegated_identity_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn get_delegated_identity_credential_status( + &self, + _request: tonic::Request< + openshell_core::proto::GetDelegatedIdentityCredentialStatusRequest, + >, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn revoke_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn delete_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + async fn get_sandbox( &self, request: tonic::Request, @@ -269,6 +328,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 8b28fafa23..2d7610415a 100644 --- a/crates/openshell-core/Cargo.toml +++ b/crates/openshell-core/Cargo.toml @@ -42,7 +42,9 @@ default = ["telemetry"] ## `--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. driver-extraction = ["dep:tar", "dep:tempfile"] -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/config.rs b/crates/openshell-core/src/config.rs index fcbdeb73b6..7e4a31da39 100644 --- a/crates/openshell-core/src/config.rs +++ b/crates/openshell-core/src/config.rs @@ -37,6 +37,9 @@ pub const DEFAULT_DOCKER_NETWORK_NAME: &str = "openshell-docker"; /// Default domain used for browser-facing sandbox service URLs. pub const DEFAULT_SERVICE_ROUTING_DOMAIN: &str = "openshell.localhost"; +/// Default maximum delegated identity authorization window for one sandbox. +pub const DEFAULT_MAX_DELEGATED_IDENTITY_DURATION_SECS: u64 = 86_400; + /// Gateway posture when a sandbox rejects a candidate policy generation. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -485,6 +488,9 @@ pub struct Config { /// TTL for SSH session tokens, in seconds. 0 disables expiry. pub ssh_session_ttl_secs: u64, + /// Maximum delegated identity authorization window for one sandbox. + pub max_delegated_identity_duration_secs: u64, + /// Maximum gRPC requests allowed per rate-limit window. /// /// When paired with [`Self::grpc_rate_limit_window_secs`], positive values @@ -840,6 +846,7 @@ impl Config { credential_drivers: Vec::new(), default_credential_driver: None, ssh_session_ttl_secs: default_ssh_session_ttl_secs(), + max_delegated_identity_duration_secs: DEFAULT_MAX_DELEGATED_IDENTITY_DURATION_SECS, grpc_rate_limit_requests: None, grpc_rate_limit_window_secs: None, service_routing: ServiceRoutingConfig::default(), @@ -930,6 +937,13 @@ impl Config { self } + /// Create a new configuration with the maximum delegated identity duration. + #[must_use] + pub const fn with_max_delegated_identity_duration_secs(mut self, secs: u64) -> Self { + self.max_delegated_identity_duration_secs = secs; + self + } + /// Set the gateway-wide gRPC request rate limit. #[must_use] pub const fn with_grpc_rate_limit( diff --git a/crates/openshell-core/src/driver_utils.rs b/crates/openshell-core/src/driver_utils.rs index ae621fde08..597ea61675 100644 --- a/crates/openshell-core/src/driver_utils.rs +++ b/crates/openshell-core/src/driver_utils.rs @@ -348,6 +348,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 e5f246e4c0..54f0db6902 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}; @@ -860,6 +860,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 @@ -969,6 +1018,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 { Self::connect_with_credentials(endpoint, ExtensionCredentialStore::new()).await 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/metadata.rs b/crates/openshell-core/src/metadata.rs index 8794c11d5d..c2fe720026 100644 --- a/crates/openshell-core/src/metadata.rs +++ b/crates/openshell-core/src/metadata.rs @@ -6,7 +6,8 @@ //! These traits provide uniform access to `ObjectMeta` fields across all resource types. use crate::proto::{ - InferenceRoute, ObjectForTest, Provider, Sandbox, SandboxStatus, ServiceEndpoint, SshSession, + DelegatedIdentityCredential, InferenceRoute, ObjectForTest, Provider, Sandbox, + SandboxDelegatedIdentityRecord, SandboxStatus, ServiceEndpoint, SshSession, StoredProviderCredentialRefreshState, StoredProviderProfile, Workspace, WorkspaceMember, }; use std::collections::HashMap; @@ -273,6 +274,90 @@ impl ObjectWorkspace for StoredProviderCredentialRefreshState { } } +// Implementations for DelegatedIdentityCredential +impl ObjectId for DelegatedIdentityCredential { + fn object_id(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.id.as_str()) + } +} + +impl ObjectName for DelegatedIdentityCredential { + fn object_name(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.name.as_str()) + } +} + +impl ObjectLabels for DelegatedIdentityCredential { + fn object_labels(&self) -> Option> { + self.metadata.as_ref().map(|m| m.labels.clone()) + } +} + +impl SetResourceVersion for DelegatedIdentityCredential { + fn set_resource_version(&mut self, version: u64) { + if let Some(meta) = self.metadata.as_mut() { + meta.resource_version = version; + } + } +} + +impl GetResourceVersion for DelegatedIdentityCredential { + fn get_resource_version(&self) -> u64 { + self.metadata.as_ref().map_or(0, |m| m.resource_version) + } +} + +impl ObjectWorkspace for DelegatedIdentityCredential { + fn object_workspace(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.workspace.as_str()) + } + fn requires_workspace() -> bool { + false + } +} + +// Implementations for SandboxDelegatedIdentityRecord +impl ObjectId for SandboxDelegatedIdentityRecord { + fn object_id(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.id.as_str()) + } +} + +impl ObjectName for SandboxDelegatedIdentityRecord { + fn object_name(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.name.as_str()) + } +} + +impl ObjectLabels for SandboxDelegatedIdentityRecord { + fn object_labels(&self) -> Option> { + self.metadata.as_ref().map(|m| m.labels.clone()) + } +} + +impl SetResourceVersion for SandboxDelegatedIdentityRecord { + fn set_resource_version(&mut self, version: u64) { + if let Some(meta) = self.metadata.as_mut() { + meta.resource_version = version; + } + } +} + +impl GetResourceVersion for SandboxDelegatedIdentityRecord { + fn get_resource_version(&self) -> u64 { + self.metadata.as_ref().map_or(0, |m| m.resource_version) + } +} + +impl ObjectWorkspace for SandboxDelegatedIdentityRecord { + fn object_workspace(&self) -> &str { + self.metadata.as_ref().map_or("", |m| m.workspace.as_str()) + } + fn requires_workspace() -> bool { + true + } +} + // Implementations for SshSession impl ObjectId for SshSession { fn object_id(&self) -> &str { diff --git a/crates/openshell-core/src/oauth.rs b/crates/openshell-core/src/oauth.rs new file mode 100644 index 0000000000..25eff49d4c --- /dev/null +++ b/crates/openshell-core/src/oauth.rs @@ -0,0 +1,860 @@ +// 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, +} + +/// `OAuth2` refresh-token response. +#[derive(Debug, Clone)] +pub struct OAuthRefreshTokenResponse { + pub access_token: String, + pub refresh_token: Option, + 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, + #[serde(default)] + refresh_token: Option, +} + +#[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 +} + +/// Refresh-token grant form fields. +pub struct RefreshTokenParams<'a> { + pub refresh_token: &'a str, + pub client_id: &'a str, + pub scopes: &'a [String], + pub allow_insecure_http: bool, +} + +/// POST an `OAuth2` refresh-token request to a token endpoint. +pub async fn post_oauth_refresh_token( + client: &reqwest::Client, + token_endpoint: &str, + params: &RefreshTokenParams<'_>, +) -> Result { + let token_endpoint_url = + parse_token_endpoint_url_with_policy(token_endpoint, params.allow_insecure_http)?; + let mut form_params = vec![ + ("grant_type", "refresh_token"), + ("refresh_token", params.refresh_token), + ("client_id", params.client_id), + ]; + + let scope_param; + if !params.scopes.is_empty() { + scope_param = params.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(OAuthRefreshTokenResponse { + access_token: raw.access_token, + refresh_token: raw.refresh_token, + expires_in: raw.expires_in, + token_type: raw.token_type, + }) +} + +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 { + parse_token_endpoint_url_with_policy(token_endpoint, false) +} + +fn parse_token_endpoint_url_with_policy( + token_endpoint: &str, + allow_insecure_http: bool, +) -> Result { + let url = reqwest::Url::parse(token_endpoint) + .into_diagnostic() + .wrap_err("token_endpoint must be an absolute URL")?; + if allow_insecure_http && matches!(url.scheme(), "http" | "https") { + return Ok(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 token_endpoint_url_can_explicitly_allow_plain_http_for_refresh() { + parse_token_endpoint_url("http://auth.example.com/token") + .expect_err("strict validation should reject arbitrary plain HTTP"); + + parse_token_endpoint_url_with_policy("http://auth.example.com/token", true) + .expect("explicit insecure refresh policy should allow HTTP"); + parse_token_endpoint_url_with_policy("https://auth.example.com/token", true) + .expect("explicit insecure refresh policy should allow HTTPS"); + + parse_token_endpoint_url_with_policy("ftp://auth.example.com/token", true) + .expect_err("non-HTTP token endpoints must still be rejected"); + } + + #[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 783611e507..b3a3c548e5 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 @@ -503,6 +506,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, @@ -534,6 +538,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 d011790950..4a0a978286 100644 --- a/crates/openshell-driver-podman/src/container.rs +++ b/crates/openshell-driver-podman/src/container.rs @@ -14,7 +14,6 @@ use openshell_core::{driver_mounts, proto_struct}; use serde::Serialize; use serde_json::Value; use std::collections::{BTreeMap, HashSet}; -#[cfg(target_os = "linux")] use std::path::Path; /// Returns `true` when `SELinux` is enabled (enforcing or permissive). @@ -65,6 +64,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; @@ -544,6 +545,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); // Prevent user-supplied environment from overriding the TLS server name @@ -1284,6 +1292,17 @@ pub fn build_container_spec_for_image( options: opts, }); } + 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 }, @@ -1330,6 +1349,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() { @@ -2927,6 +2973,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-gateway-interceptors/src/proto_json.rs b/crates/openshell-gateway-interceptors/src/proto_json.rs index f6aecbcf67..f1e1c95e7b 100644 --- a/crates/openshell-gateway-interceptors/src/proto_json.rs +++ b/crates/openshell-gateway-interceptors/src/proto_json.rs @@ -316,6 +316,7 @@ mod tests { labels: HashMap::from([("team".to_string(), "agent".to_string())]), annotations: HashMap::new(), workspace: String::new(), + delegated_identity: None, }; let bytes = request.encode_to_vec(); let json = codec diff --git a/crates/openshell-gateway-interceptors/src/runtime.rs b/crates/openshell-gateway-interceptors/src/runtime.rs index 4e7f5ba613..fe431aceec 100644 --- a/crates/openshell-gateway-interceptors/src/runtime.rs +++ b/crates/openshell-gateway-interceptors/src/runtime.rs @@ -1075,6 +1075,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + delegated_identity: None, }; let bytes = request.encode_to_vec(); diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index 42c82c0c2a..df5271ac69 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,87 @@ 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 subject_credential = subject_token.credential.trim(); + match subject_token.source.trim() { + "provider_credential" => { + 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}"), + )); + } + } + "sandbox_delegated_identity" => { + if !subject_credential.is_empty() { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.token_grant.subject_token.credential", + "sandbox_delegated_identity subject_token must not set credential", + )); + } + } + _ => { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + "credentials.token_grant.subject_token.source", + "subject_token.source must be provider_credential or sandbox_delegated_identity", + )); + } + } + } + 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 +2760,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 +3344,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/src/client.rs b/crates/openshell-sdk/src/client.rs index c67e91e219..409ed4f62d 100644 --- a/crates/openshell-sdk/src/client.rs +++ b/crates/openshell-sdk/src/client.rs @@ -819,6 +819,7 @@ fn create_sandbox_request(spec: SandboxSpec) -> proto::CreateSandboxRequest { labels, annotations: HashMap::new(), workspace: String::new(), + delegated_identity: None, } } diff --git a/crates/openshell-sdk/tests/client_mock.rs b/crates/openshell-sdk/tests/client_mock.rs index 09e91330ce..7b14e82d08 100644 --- a/crates/openshell-sdk/tests/client_mock.rs +++ b/crates/openshell-sdk/tests/client_mock.rs @@ -195,6 +195,55 @@ impl OpenShell for TestOpenShell { })) } + async fn get_sandbox_delegated_identity_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn withdraw_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn extend_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn list_delegated_identity_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn get_delegated_identity_credential_status( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn revoke_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + + async fn delete_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> { + Err(Status::unimplemented("unused")) + } + async fn get_sandbox( &self, request: tonic::Request, @@ -492,6 +541,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 772590d1b0..b5ebf1fa74 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" } @@ -100,6 +100,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..ba99edc715 100644 --- a/crates/openshell-server/src/auth/oidc.rs +++ b/crates/openshell-server/src/auth/oidc.rs @@ -95,6 +95,8 @@ struct JwkKey { pub struct OidcClaims { pub sub: String, #[serde(default)] + pub exp: i64, + #[serde(default)] pub preferred_username: Option, #[serde(default)] #[allow(dead_code)] @@ -240,6 +242,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 +289,13 @@ 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 { + Ok(self.validate_token_details(token).await?.identity) + } + + /// Validate a JWT and return the derived identity plus verified token metadata. + pub async fn validate_token_details(&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") @@ -331,6 +341,10 @@ impl JwksCache { })?; let mut claims = token_data.claims; + let expires_at_ms = claims.exp.saturating_mul(1000); + if expires_at_ms <= 0 { + return Err(Status::unauthenticated("invalid token: invalid exp")); + } claims.extract_roles(&self.config.roles_claim); let scopes = if self.config.scopes_claim.is_empty() { @@ -339,16 +353,25 @@ impl JwksCache { claims.extract_scopes(&self.config.scopes_claim) }; - Ok(Identity { - subject: claims.sub, - display_name: claims.preferred_username, - roles: claims.roles, - scopes, - provider: IdentityProvider::Oidc, + Ok(ValidatedOidcToken { + identity: Identity { + subject: claims.sub, + display_name: claims.preferred_username, + roles: claims.roles, + scopes, + provider: IdentityProvider::Oidc, + }, + expires_at_ms, }) } } +/// A verified OIDC access token and server-derived metadata. +pub struct ValidatedOidcToken { + pub identity: Identity, + pub expires_at_ms: i64, +} + /// Authenticator that validates `Authorization: Bearer ` headers against /// the configured OIDC issuer. /// @@ -568,6 +591,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 +707,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 3d66824f20..9dc10b8401 100644 --- a/crates/openshell-server/src/auth/sandbox_jwt.rs +++ b/crates/openshell-server/src/auth/sandbox_jwt.rs @@ -112,6 +112,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}"); @@ -127,6 +129,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 @@ -248,6 +252,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 jwks = GatewayJwks::from_public_key_pem(public_key_pem, kid.clone())?; @@ -276,6 +282,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 5cc9e3693a..89f34d1253 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/cli.rs b/crates/openshell-server/src/cli.rs index 2e86c3a1b5..e96d08276e 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -429,6 +429,13 @@ fn prepare_server_config(args: &mut RunArgs, matches: &ArgMatches) -> Result, #[serde(default)] + pub max_delegated_identity_duration_secs: Option, + #[serde(default)] pub grpc_rate_limit_requests: Option, #[serde(default)] pub grpc_rate_limit_window_seconds: Option, diff --git a/crates/openshell-server/src/delegated_identity.rs b/crates/openshell-server/src/delegated_identity.rs new file mode 100644 index 0000000000..916600cf00 --- /dev/null +++ b/crates/openshell-server/src/delegated_identity.rs @@ -0,0 +1,1639 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Gateway-scoped delegated OIDC identity credentials for sandbox token exchange. + +use crate::ServerState; +use crate::auth::principal::{Principal, UserPrincipal}; +use crate::auth::workspace_authz::{MinWorkspaceRole, authorize_workspace, require_platform_admin}; +use crate::persistence::{ObjectType, WriteCondition, current_time_ms}; +use openshell_core::proto::datamodel::v1::ObjectMeta; +use openshell_core::proto::{ + DelegatedIdentityCredential, DelegatedIdentityCredentialSummary, DelegatedIdentityRequest, + DeleteDelegatedIdentityCredentialRequest, DeleteDelegatedIdentityCredentialResponse, + ExtendSandboxDelegatedIdentityRequest, ExtendSandboxDelegatedIdentityResponse, + GetDelegatedIdentityCredentialStatusRequest, GetDelegatedIdentityCredentialStatusResponse, + GetSandboxDelegatedIdentityStatusRequest, GetSandboxDelegatedIdentityStatusResponse, + ListDelegatedIdentityCredentialsRequest, ListDelegatedIdentityCredentialsResponse, + RevokeDelegatedIdentityCredentialRequest, RevokeDelegatedIdentityCredentialResponse, Sandbox, + SandboxDelegatedIdentity, SandboxDelegatedIdentityRecord, + WithdrawSandboxDelegatedIdentityRequest, WithdrawSandboxDelegatedIdentityResponse, +}; +use openshell_core::{GetResourceVersion, ObjectId, ObjectLabels, ObjectName, ObjectWorkspace}; +use prost::Message; +use serde::Deserialize; +use sha2::{Digest, Sha256}; +use std::collections::HashMap; +use std::sync::{Arc, LazyLock}; +use std::time::Duration; +use tonic::{Request, Response, Status}; + +const CREDENTIAL_OBJECT_TYPE: &str = "delegated_identity_credential"; +const SANDBOX_DELEGATION_OBJECT_TYPE: &str = "sandbox_delegated_identity"; +const GLOBAL_WORKSPACE: &str = ""; +const REFRESH_SKEW_MS: i64 = 60_000; +static DELEGATED_IDENTITY_HTTP_CLIENT: LazyLock> = + LazyLock::new(|| { + reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .connect_timeout(Duration::from_secs(30)) + .build() + .map_err(|err| format!("delegated identity HTTP client configuration failed: {err}")) + }); + +pub struct PreparedSandboxDelegatedIdentity { + pub record: SandboxDelegatedIdentityRecord, + credential_id: String, + credential_resource_version: u64, + credential_created: bool, +} + +impl ObjectType for DelegatedIdentityCredential { + fn object_type() -> &'static str { + CREDENTIAL_OBJECT_TYPE + } +} + +impl ObjectType for SandboxDelegatedIdentityRecord { + fn object_type() -> &'static str { + SANDBOX_DELEGATION_OBJECT_TYPE + } +} + +pub async fn prepare_for_sandbox_create( + state: &Arc, + principal: &Principal, + sandbox: &Sandbox, + request: Option, +) -> Result, Status> { + let Some(request) = request else { + return Ok(None); + }; + let user = require_user(principal)?; + let access_token_expires_at_ms = validate_delegation_request(state, user, &request).await?; + let credential = + upsert_credential(state, user, request.clone(), access_token_expires_at_ms).await?; + let credential_id = credential.credential.object_id().to_string(); + let credential_resource_version = credential.credential.get_resource_version(); + let sandbox_id = sandbox.object_id().to_string(); + let record_id = sandbox_delegated_identity_record_id(&sandbox_id); + Ok(Some(PreparedSandboxDelegatedIdentity { + record: SandboxDelegatedIdentityRecord { + metadata: Some(ObjectMeta { + id: record_id.clone(), + name: record_id, + created_at_ms: current_time_ms(), + labels: HashMap::new(), + resource_version: 0, + annotations: HashMap::new(), + workspace: sandbox.object_workspace().to_string(), + deletion_timestamp_ms: 0, + }), + sandbox_id, + delegated_identity: Some(SandboxDelegatedIdentity { + credential_id: credential_id.clone(), + principal_subject: user.identity.subject.clone(), + delegated_until_ms: request.delegated_until_ms, + withdrawn_at_ms: 0, + }), + }, + credential_id, + credential_resource_version, + credential_created: credential.created, + })) +} + +pub async fn store_prepared_sandbox_delegation( + state: &Arc, + prepared: Option<&PreparedSandboxDelegatedIdentity>, +) -> Result<(), Status> { + let Some(prepared) = prepared else { + return Ok(()); + }; + state + .store + .put_scoped_message(&prepared.record, &prepared.record.sandbox_id) + .await + .map_err(|e| Status::internal(format!("persist sandbox delegated identity failed: {e}"))) +} + +pub async fn delete_prepared_sandbox_delegation( + state: &Arc, + prepared: Option<&PreparedSandboxDelegatedIdentity>, +) -> Result<(), Status> { + let Some(prepared) = prepared else { + return Ok(()); + }; + state + .store + .delete( + SandboxDelegatedIdentityRecord::object_type(), + prepared.record.object_id(), + ) + .await + .map(|_| ()) + .map_err(|e| Status::internal(format!("delete sandbox delegated identity failed: {e}"))) +} + +pub async fn delete_new_prepared_sandbox_credential( + state: &Arc, + prepared: Option<&PreparedSandboxDelegatedIdentity>, +) -> Result<(), Status> { + let Some(prepared) = prepared.filter(|prepared| prepared.credential_created) else { + return Ok(()); + }; + state + .store + .delete_if( + DelegatedIdentityCredential::object_type(), + &prepared.credential_id, + prepared.credential_resource_version, + ) + .await + .map(|_| ()) + .map_err(|e| Status::internal(format!("delete prepared delegated credential failed: {e}"))) +} + +pub async fn resolve_subject_access_token( + state: &Arc, + sandbox: &Sandbox, +) -> Result<(String, i64, String), Status> { + let record = sandbox_delegated_identity_record(state, sandbox).await?; + let delegation = record + .as_ref() + .and_then(|record| record.delegated_identity.as_ref()) + .ok_or_else(|| { + Status::failed_precondition("sandbox was not created with delegated identity") + })?; + if delegation.withdrawn_at_ms > 0 { + return Err(Status::failed_precondition("delegated identity withdrawn")); + } + let now = current_time_ms(); + if delegation.delegated_until_ms <= now { + return Err(Status::failed_precondition("delegated identity expired")); + } + let credential = state + .store + .get_message::(&delegation.credential_id) + .await + .map_err(|e| Status::internal(format!("fetch delegated credential failed: {e}")))? + .ok_or_else(|| Status::failed_precondition("delegated credential missing"))?; + if credential.principal_subject != delegation.principal_subject { + return Err(Status::failed_precondition( + "delegated credential principal does not match sandbox delegation", + )); + } + if credential.revoked_at_ms > 0 { + return Err(Status::failed_precondition("delegated credential revoked")); + } + let credential = refresh_if_needed(state, credential).await?; + let credential_id = credential.object_id().to_string(); + Ok(( + credential.access_token, + credential.access_token_expires_at_ms, + credential_id, + )) +} + +pub async fn handle_status( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = crate::grpc::extract_principal(&request)?; + let req = request.into_inner(); + let sandbox = authorized_sandbox_by_name(state, &principal, &req.workspace, &req.name).await?; + let record = sandbox_delegated_identity_record(state, &sandbox).await?; + let delegation = record + .as_ref() + .and_then(|record| record.delegated_identity.as_ref()); + if delegation.is_some() { + ensure_delegator(&principal, delegation)?; + } + let (credential_revoked_at_ms, credential_missing) = + sandbox_delegated_identity_credential_status(state, delegation).await?; + Ok(Response::new(GetSandboxDelegatedIdentityStatusResponse { + delegated_identity: delegation.cloned(), + now_ms: current_time_ms(), + credential_revoked_at_ms, + credential_missing, + })) +} + +async fn sandbox_delegated_identity_credential_status( + state: &Arc, + delegation: Option<&SandboxDelegatedIdentity>, +) -> Result<(i64, bool), Status> { + let Some(delegation) = delegation else { + return Ok((0, true)); + }; + let credential = state + .store + .get_message::(&delegation.credential_id) + .await + .map_err(|e| Status::internal(format!("fetch delegated credential failed: {e}")))?; + Ok(delegated_credential_status_fields(credential.as_ref())) +} + +fn delegated_credential_status_fields( + credential: Option<&DelegatedIdentityCredential>, +) -> (i64, bool) { + credential.map_or((0, true), |credential| (credential.revoked_at_ms, false)) +} + +async fn sandbox_delegated_identity_record( + state: &Arc, + sandbox: &Sandbox, +) -> Result, Status> { + state + .store + .get_message::(&sandbox_delegated_identity_record_id( + sandbox.object_id(), + )) + .await + .map_err(|e| Status::internal(format!("fetch sandbox delegated identity failed: {e}"))) +} + +pub fn sandbox_delegated_identity_record_id(sandbox_id: &str) -> String { + format!("sandbox-delegated-identity-{sandbox_id}") +} + +pub async fn ensure_delegated_identity_sandbox_user( + state: &Arc, + principal: &Principal, + sandbox: &Sandbox, +) -> Result<(), Status> { + let record = sandbox_delegated_identity_record(state, sandbox).await?; + let Some(delegation) = record + .as_ref() + .and_then(|record| record.delegated_identity.as_ref()) + else { + return Ok(()); + }; + + match principal { + Principal::User(user) if user.identity.subject == delegation.principal_subject => Ok(()), + Principal::User(_) => Err(Status::permission_denied( + "delegated identity sandbox access denied: caller is not the delegating principal", + )), + Principal::Sandbox(_) => Ok(()), + Principal::Anonymous => Err(Status::unauthenticated( + "sandbox-scoped methods require an authenticated caller", + )), + } +} + +pub async fn handle_withdraw( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = crate::grpc::extract_principal(&request)?; + let req = request.into_inner(); + let sandbox = authorized_sandbox_by_name(state, &principal, &req.workspace, &req.name).await?; + let record = sandbox_delegated_identity_record(state, &sandbox).await?; + let delegation = record + .as_ref() + .and_then(|record| record.delegated_identity.as_ref()); + ensure_delegator(&principal, delegation)?; + let Some(record) = record else { + return Err(Status::invalid_argument( + "sandbox delegated identity is not enabled", + )); + }; + let now = current_time_ms(); + let mut changed = false; + state + .store + .update_message_cas::(record.object_id(), 0, |current| { + if let Some(delegation) = current.delegated_identity.as_mut() + && delegation.withdrawn_at_ms == 0 + { + delegation.withdrawn_at_ms = now; + changed = true; + } + }) + .await + .map_err(|e| crate::grpc::persistence_error_to_status(e, "withdraw delegated identity"))?; + Ok(Response::new(WithdrawSandboxDelegatedIdentityResponse { + sandbox: Some(sandbox), + withdrawn: changed, + })) +} + +pub async fn handle_extend( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = crate::grpc::extract_principal(&request)?; + let req = request.into_inner(); + let material = req + .delegated_identity + .ok_or_else(|| Status::invalid_argument("delegated_identity is required"))?; + let sandbox = authorized_sandbox_by_name(state, &principal, &req.workspace, &req.name).await?; + let record = sandbox_delegated_identity_record(state, &sandbox).await?; + let delegation = record + .as_ref() + .and_then(|record| record.delegated_identity.as_ref()); + ensure_delegator(&principal, delegation)?; + let Some(record) = record else { + return Err(Status::invalid_argument( + "sandbox delegated identity is not enabled", + )); + }; + let user = require_user(&principal)?; + let access_token_expires_at_ms = validate_delegation_request(state, user, &material).await?; + let credential = + upsert_credential(state, user, material.clone(), access_token_expires_at_ms).await?; + let update_result = state + .store + .update_message_cas::(record.object_id(), 0, |current| { + if let Some(delegation) = current.delegated_identity.as_mut() { + delegation.credential_id = credential.credential.object_id().to_string(); + delegation.delegated_until_ms = material.delegated_until_ms; + delegation.withdrawn_at_ms = 0; + } + }) + .await; + if let Err(error) = update_result { + cleanup_new_credential_after_prepare_failure(state, &credential).await?; + return Err(crate::grpc::persistence_error_to_status( + error, + "extend delegated identity", + )); + } + Ok(Response::new(ExtendSandboxDelegatedIdentityResponse { + sandbox: Some(sandbox), + })) +} + +pub async fn handle_list_credentials( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = crate::grpc::extract_principal(&request)?; + require_platform_admin(&state.admin_role, &principal)?; + let req = request.into_inner(); + let credentials = state + .store + .list_all_messages::( + crate::grpc::clamp_limit(req.limit, 100, crate::grpc::MAX_PAGE_SIZE), + req.offset, + ) + .await + .map_err(|e| Status::internal(format!("list delegated credentials failed: {e}")))?; + let credentials = credentials + .into_iter() + .map(delegated_credential_summary) + .collect(); + Ok(Response::new(ListDelegatedIdentityCredentialsResponse { + credentials, + })) +} + +pub async fn handle_get_credential_status( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = crate::grpc::extract_principal(&request)?; + require_platform_admin(&state.admin_role, &principal)?; + let req = request.into_inner(); + let credential = state + .store + .get_message::(&req.id) + .await + .map_err(|e| Status::internal(format!("fetch delegated credential failed: {e}")))? + .ok_or_else(|| Status::not_found("delegated credential not found"))?; + Ok(Response::new( + GetDelegatedIdentityCredentialStatusResponse { + credential: Some(delegated_credential_summary(credential)), + now_ms: current_time_ms(), + }, + )) +} + +fn delegated_credential_summary( + credential: DelegatedIdentityCredential, +) -> DelegatedIdentityCredentialSummary { + DelegatedIdentityCredentialSummary { + metadata: credential.metadata, + issuer: credential.issuer, + client_id: credential.client_id, + principal_subject: credential.principal_subject, + refresh_token_present: !credential.refresh_token.is_empty(), + access_token_present: !credential.access_token.is_empty(), + access_token_expires_at_ms: credential.access_token_expires_at_ms, + scopes: credential.scopes, + audience: credential.audience, + last_refresh_at_ms: credential.last_refresh_at_ms, + revoked_at_ms: credential.revoked_at_ms, + } +} + +pub async fn handle_revoke_credential( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = crate::grpc::extract_principal(&request)?; + require_platform_admin(&state.admin_role, &principal)?; + let req = request.into_inner(); + let now = current_time_ms(); + let mut revoked = false; + let credential = state + .store + .update_message_cas::( + &req.id, + req.expected_resource_version, + |credential| { + if credential.revoked_at_ms == 0 { + credential.revoked_at_ms = now; + revoked = true; + } + }, + ) + .await + .map_err(|e| crate::grpc::persistence_error_to_status(e, "revoke delegated credential"))?; + let resource_version = credential + .metadata + .as_ref() + .map(|metadata| metadata.resource_version) + .unwrap_or_default(); + Ok(Response::new(RevokeDelegatedIdentityCredentialResponse { + revoked, + revoked_at_ms: credential.revoked_at_ms, + resource_version, + })) +} + +pub async fn handle_delete_credential( + state: &Arc, + request: Request, +) -> Result, Status> { + let principal = crate::grpc::extract_principal(&request)?; + require_platform_admin(&state.admin_role, &principal)?; + let req = request.into_inner(); + let expected_resource_version = + delete_credential_resource_version(state, &req.id, req.expected_resource_version).await?; + let Some(expected_resource_version) = expected_resource_version else { + return Ok(Response::new(DeleteDelegatedIdentityCredentialResponse { + deleted: false, + })); + }; + let deleted = state + .store + .delete_if( + DelegatedIdentityCredential::object_type(), + &req.id, + expected_resource_version, + ) + .await + .map_err(|e| crate::grpc::persistence_error_to_status(e, "delete delegated credential"))?; + Ok(Response::new(DeleteDelegatedIdentityCredentialResponse { + deleted, + })) +} + +async fn delete_credential_resource_version( + state: &Arc, + id: &str, + expected_resource_version: u64, +) -> Result, Status> { + if expected_resource_version != 0 { + return Ok(Some(expected_resource_version)); + } + let credential = state + .store + .get_message::(id) + .await + .map_err(|e| Status::internal(format!("fetch delegated credential failed: {e}")))?; + Ok(effective_delete_credential_resource_version( + credential.as_ref(), + expected_resource_version, + )) +} + +fn effective_delete_credential_resource_version( + credential: Option<&DelegatedIdentityCredential>, + expected_resource_version: u64, +) -> Option { + if expected_resource_version != 0 { + Some(expected_resource_version) + } else { + credential + .and_then(|credential| credential.metadata.as_ref()) + .map(|metadata| metadata.resource_version) + } +} + +fn require_user(principal: &Principal) -> Result<&UserPrincipal, Status> { + match principal { + Principal::User(user) => Ok(user), + _ => Err(Status::permission_denied( + "delegated identity requires an authenticated user principal", + )), + } +} + +fn ensure_delegator( + principal: &Principal, + delegation: Option<&SandboxDelegatedIdentity>, +) -> Result<(), Status> { + let user = require_user(principal)?; + let delegation = delegation.ok_or_else(|| { + Status::failed_precondition("sandbox was not created with delegated identity") + })?; + if delegation.principal_subject != user.identity.subject { + return Err(Status::permission_denied( + "only the delegating principal may manage this sandbox delegated identity", + )); + } + Ok(()) +} + +async fn validate_delegation_request( + state: &Arc, + user: &UserPrincipal, + request: &DelegatedIdentityRequest, +) -> Result { + if request.issuer.trim().is_empty() { + return Err(Status::invalid_argument( + "delegated_identity.issuer is required", + )); + } + let configured_issuer = state + .config + .oidc + .as_ref() + .map(|oidc| oidc.issuer.trim_end_matches('/')) + .ok_or_else(|| { + Status::failed_precondition( + "delegated identity requires gateway OIDC authentication to be configured", + ) + })?; + if request.issuer.trim_end_matches('/') != configured_issuer { + return Err(Status::invalid_argument( + "delegated_identity.issuer must match the gateway OIDC issuer", + )); + } + if request.client_id.trim().is_empty() { + return Err(Status::invalid_argument( + "delegated_identity.client_id is required", + )); + } + if request.refresh_token.trim().is_empty() { + return Err(Status::invalid_argument( + "delegated_identity.refresh_token is required", + )); + } + if request.access_token.trim().is_empty() { + return Err(Status::invalid_argument( + "delegated_identity.access_token is required", + )); + } + let access_token_expires_at_ms = + validate_delegated_access_token_subject(state, user, request).await?; + let now = current_time_ms(); + if request.delegated_until_ms <= now { + return Err(Status::invalid_argument( + "delegated_identity.delegated_until_ms must be in the future", + )); + } + let max_ms = i64::try_from(state.config.max_delegated_identity_duration_secs) + .unwrap_or(i64::MAX / 1000) + .saturating_mul(1000); + if request.delegated_until_ms.saturating_sub(now) > max_ms { + return Err(Status::failed_precondition(format!( + "delegated identity duration exceeds gateway maximum of {} seconds", + state.config.max_delegated_identity_duration_secs + ))); + } + Ok(access_token_expires_at_ms) +} + +async fn validate_delegated_access_token_subject( + state: &Arc, + user: &UserPrincipal, + request: &DelegatedIdentityRequest, +) -> Result { + validate_delegated_access_token_subject_value( + state, + &request.access_token, + &user.identity.subject, + ) + .await +} + +async fn validate_delegated_access_token_subject_value( + state: &Arc, + access_token: &str, + expected_subject: &str, +) -> Result { + let cache = state.oidc_cache.as_ref().ok_or_else(|| { + Status::failed_precondition( + "delegated identity requires gateway OIDC token validation to be configured", + ) + })?; + let validated = cache.validate_token_details(access_token).await?; + ensure_delegated_token_subject_matches(&validated.identity.subject, expected_subject)?; + Ok(validated.expires_at_ms) +} + +fn ensure_delegated_token_subject_matches( + token_subject: &str, + caller_subject: &str, +) -> Result<(), Status> { + if token_subject == caller_subject { + Ok(()) + } else { + Err(Status::permission_denied( + "delegated_identity.access_token subject must match the authenticated caller", + )) + } +} + +async fn authorized_sandbox_by_name( + state: &Arc, + principal: &Principal, + workspace: &str, + name: &str, +) -> Result { + if name.trim().is_empty() { + return Err(Status::invalid_argument("name is required")); + } + let authz = authorize_workspace( + &state.store, + &state.admin_role, + principal, + workspace, + MinWorkspaceRole::User, + ) + .await?; + let workspace = + crate::grpc::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) + .await? + .name; + state + .store + .get_message_by_name::(&workspace, name) + .await + .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? + .ok_or_else(|| Status::not_found("sandbox not found")) +} + +async fn upsert_credential( + state: &Arc, + user: &UserPrincipal, + request: DelegatedIdentityRequest, + access_token_expires_at_ms: i64, +) -> Result { + let id = delegated_credential_id(&request.issuer, &request.client_id, &user.identity.subject); + let now = current_time_ms(); + let mut credential = DelegatedIdentityCredential { + metadata: Some(ObjectMeta { + id: id.clone(), + name: id.clone(), + created_at_ms: now, + labels: HashMap::new(), + resource_version: 0, + annotations: HashMap::new(), + workspace: GLOBAL_WORKSPACE.to_string(), + deletion_timestamp_ms: 0, + }), + issuer: request.issuer, + client_id: request.client_id, + principal_subject: user.identity.subject.clone(), + refresh_token: request.refresh_token, + access_token: request.access_token, + access_token_expires_at_ms, + scopes: request.scopes, + audience: request.audience, + last_refresh_at_ms: now, + revoked_at_ms: 0, + }; + + let existing = state + .store + .get_message::(&id) + .await + .map_err(|e| Status::internal(format!("fetch delegated credential failed: {e}")))?; + let (created, write_condition) = if let Some(existing) = existing { + ensure_delegated_credential_not_revoked(&existing)?; + let write_condition = delegated_credential_upsert_condition(&existing); + credential.metadata = existing.metadata; + credential.revoked_at_ms = existing.revoked_at_ms; + (false, write_condition) + } else { + (true, WriteCondition::MustCreate) + }; + let labels = credential + .object_labels() + .filter(|labels| !labels.is_empty()) + .map(|labels| { + serde_json::to_string(&labels) + .map_err(|e| Status::internal(format!("serialize labels failed: {e}"))) + }) + .transpose()?; + let result = state + .store + .put_if( + DelegatedIdentityCredential::object_type(), + credential.object_id(), + credential.object_name(), + credential.object_workspace(), + &credential.encode_to_vec(), + labels.as_deref(), + write_condition, + ) + .await + .map_err(|e| crate::grpc::persistence_error_to_status(e, "persist delegated credential"))?; + if let Some(metadata) = credential.metadata.as_mut() { + metadata.resource_version = result.resource_version; + } + Ok(UpsertedCredential { + credential, + created, + }) +} + +fn delegated_credential_upsert_condition(existing: &DelegatedIdentityCredential) -> WriteCondition { + WriteCondition::MatchResourceVersion(existing.get_resource_version()) +} + +struct UpsertedCredential { + credential: DelegatedIdentityCredential, + created: bool, +} + +async fn cleanup_new_credential_after_prepare_failure( + state: &Arc, + credential: &UpsertedCredential, +) -> Result<(), Status> { + if !credential.created { + return Ok(()); + } + state + .store + .delete_if( + DelegatedIdentityCredential::object_type(), + credential.credential.object_id(), + credential.credential.get_resource_version(), + ) + .await + .map(|_| ()) + .map_err(|e| Status::internal(format!("delete prepared delegated credential failed: {e}"))) +} + +fn ensure_delegated_credential_not_revoked( + credential: &DelegatedIdentityCredential, +) -> Result<(), Status> { + if credential.revoked_at_ms > 0 { + Err(Status::failed_precondition( + "delegated identity credential is revoked", + )) + } else { + Ok(()) + } +} + +fn delegated_credential_id(issuer: &str, client_id: &str, principal_subject: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(issuer.trim_end_matches('/')); + hasher.update(b"\0"); + hasher.update(client_id); + hasher.update(b"\0"); + hasher.update(principal_subject); + format!("delegated-identity-{:x}", hasher.finalize()) +} + +async fn refresh_if_needed( + state: &Arc, + credential: DelegatedIdentityCredential, +) -> Result { + let now = current_time_ms(); + if credential.access_token_expires_at_ms > 0 + && credential.access_token_expires_at_ms.saturating_sub(now) > REFRESH_SKEW_MS + { + return Ok(credential); + } + let client = delegated_identity_http_client()?; + let token_endpoint = discover_token_endpoint(client, &credential.issuer).await?; + let scopes = credential + .scopes + .split_whitespace() + .filter(|scope| !scope.is_empty()) + .map(ToString::to_string) + .collect::>(); + let refreshed = openshell_core::oauth::post_oauth_refresh_token( + client, + &token_endpoint, + &openshell_core::oauth::RefreshTokenParams { + refresh_token: &credential.refresh_token, + client_id: &credential.client_id, + scopes: &scopes, + allow_insecure_http: delegated_refresh_allows_insecure_http( + &credential.issuer, + &token_endpoint, + ), + }, + ) + .await + .map_err(|e| Status::failed_precondition(delegated_refresh_error_message(&e.to_string())))?; + let expires_at_ms = validate_delegated_access_token_subject_value( + state, + &refreshed.access_token, + &credential.principal_subject, + ) + .await?; + let refreshed_refresh_token = refreshed.refresh_token; + let refreshed_access_token = refreshed.access_token; + let updated = state + .store + .update_message_cas::( + credential.object_id(), + credential.get_resource_version(), + |current| { + current.access_token.clone_from(&refreshed_access_token); + current.access_token_expires_at_ms = expires_at_ms; + current.last_refresh_at_ms = now; + if let Some(refresh_token) = refreshed_refresh_token.as_ref() { + current.refresh_token.clone_from(refresh_token); + } + }, + ) + .await + .map_err(|e| crate::grpc::persistence_error_to_status(e, "refresh delegated credential"))?; + Ok(updated) +} + +fn delegated_identity_http_client() -> Result<&'static reqwest::Client, Status> { + DELEGATED_IDENTITY_HTTP_CLIENT + .as_ref() + .map_err(|err| Status::internal(err.clone())) +} + +fn delegated_refresh_allows_insecure_http(issuer: &str, token_endpoint: &str) -> bool { + let Ok(issuer) = reqwest::Url::parse(issuer) else { + return false; + }; + let Ok(token_endpoint) = reqwest::Url::parse(token_endpoint) else { + return false; + }; + issuer.scheme() == "http" + && token_endpoint.scheme() == "http" + && issuer.host_str() == token_endpoint.host_str() + && issuer.port_or_known_default() == token_endpoint.port_or_known_default() +} + +fn delegated_refresh_error_message(error: &str) -> String { + let mut message = format!("delegated credential refresh failed: {error}"); + if inactive_refresh_token_error(error) { + message.push_str( + "; the stored delegated identity refresh token is no longer active. \ + Re-authenticate with `openshell gateway logout` followed by `openshell gateway login`, \ + then run `openshell sandbox delegated-identity extend --for=`.", + ); + } + message +} + +fn inactive_refresh_token_error(error: &str) -> bool { + let error = error.to_ascii_lowercase(); + error.contains("invalid_grant") + && (error.contains("session not active") + || error.contains("session inactive") + || error.contains("refresh token")) +} + +#[derive(Debug, Deserialize)] +struct OidcDiscovery { + issuer: String, + token_endpoint: String, +} + +async fn discover_token_endpoint(client: &reqwest::Client, issuer: &str) -> Result { + let normalized = issuer.trim_end_matches('/'); + let url = format!("{normalized}/.well-known/openid-configuration"); + let discovery = client + .get(url) + .send() + .await + .map_err(|e| Status::failed_precondition(format!("OIDC discovery failed: {e}")))? + .error_for_status() + .map_err(|e| Status::failed_precondition(format!("OIDC discovery failed: {e}")))? + .json::() + .await + .map_err(|e| Status::failed_precondition(format!("OIDC discovery parse failed: {e}")))?; + if discovery.issuer.trim_end_matches('/') != normalized { + return Err(Status::failed_precondition( + "OIDC discovery issuer does not match delegated credential issuer", + )); + } + Ok(discovery.token_endpoint) +} + +#[cfg(test)] +mod tests { + use super::{ + PreparedSandboxDelegatedIdentity, delegated_credential_id, + delegated_credential_status_fields, delegated_credential_summary, + delegated_credential_upsert_condition, delegated_refresh_allows_insecure_http, + delegated_refresh_error_message, delete_new_prepared_sandbox_credential, + effective_delete_credential_resource_version, ensure_delegated_credential_not_revoked, + ensure_delegated_token_subject_matches, sandbox_delegated_identity_record_id, + }; + use crate::grpc::test_support::{authed_request, test_server_state}; + use crate::persistence::{ObjectType, Store, WriteCondition, current_time_ms}; + use crate::sandbox_index::SandboxIndex; + use crate::sandbox_watch::SandboxWatchBus; + use crate::supervisor_session::SupervisorSessionRegistry; + use crate::tracing_bus::TracingLogBus; + use openshell_core::proto::datamodel::v1::ObjectMeta; + use openshell_core::proto::{ + DelegatedIdentityCredential, DelegatedIdentityRequest, + ExtendSandboxDelegatedIdentityRequest, GetSandboxDelegatedIdentityStatusRequest, Sandbox, + SandboxDelegatedIdentity, SandboxDelegatedIdentityRecord, SandboxSpec, SandboxStatus, + }; + use openshell_core::{Config, GetResourceVersion, ObjectId, OidcConfig}; + use prost::Message as _; + use std::collections::HashMap; + use std::sync::{Arc, LazyLock}; + use tonic::Code; + use tonic::Request; + + const TEST_KID: &str = "test-signing-key"; + const TEST_AUDIENCE: &str = "openshell-cli"; + + static TEST_RSA_KEY: LazyLock = LazyLock::new(TestRsaKey::generate); + + struct TestRsaKey { + private_pem: String, + modulus_b64: String, + exponent_b64: String, + } + + impl TestRsaKey { + fn generate() -> Self { + use base64::Engine as _; + use rsa::pkcs1::EncodeRsaPrivateKey as _; + use rsa::traits::PublicKeyParts as _; + + let private = rsa::RsaPrivateKey::new(&mut rsa::rand_core::OsRng, 2048) + .expect("generate RSA test key"); + let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD; + Self { + private_pem: private + .to_pkcs1_pem(rsa::pkcs1::LineEnding::LF) + .expect("encode RSA private key as PEM") + .to_string(), + modulus_b64: b64.encode(private.n().to_bytes_be()), + exponent_b64: b64.encode(private.e().to_bytes_be()), + } + } + } + + #[test] + fn delegated_refresh_allows_insecure_http_only_for_same_http_origin() { + assert!(delegated_refresh_allows_insecure_http( + "http://keycloak.127.0.0.1.sslip.io:9090/realms/openshell", + "http://keycloak.127.0.0.1.sslip.io:9090/realms/openshell/protocol/openid-connect/token", + )); + assert!(!delegated_refresh_allows_insecure_http( + "https://idp.example.com/realms/openshell", + "http://idp.example.com/realms/openshell/protocol/openid-connect/token", + )); + assert!(!delegated_refresh_allows_insecure_http( + "http://idp.example.com/realms/openshell", + "http://metadata.internal/token", + )); + assert!(!delegated_refresh_allows_insecure_http( + "not an issuer url", + "http://idp.example.com/token", + )); + } + + #[test] + fn delegated_refresh_error_message_explains_inactive_session_recovery() { + let message = delegated_refresh_error_message( + "token grant failed with status 400 Bad Request: error=invalid_grant; error_description=Session not active", + ); + + assert!(message.contains("delegated credential refresh failed")); + assert!(message.contains("stored delegated identity refresh token is no longer active")); + assert!(message.contains("openshell sandbox delegated-identity extend ")); + } + + #[test] + fn revoked_delegated_credential_rejects_upsert_reactivation() { + let active = DelegatedIdentityCredential { + revoked_at_ms: 0, + ..Default::default() + }; + ensure_delegated_credential_not_revoked(&active).expect("active credential is reusable"); + + let revoked = DelegatedIdentityCredential { + revoked_at_ms: 42, + ..Default::default() + }; + let status = ensure_delegated_credential_not_revoked(&revoked) + .expect_err("revoked credential must stay revoked"); + + assert_eq!(status.code(), Code::FailedPrecondition); + assert!(status.message().contains("credential is revoked")); + } + + #[test] + fn delegated_access_token_subject_must_match_authenticated_caller() { + ensure_delegated_token_subject_matches("user-a", "user-a") + .expect("matching subject should be accepted"); + + let status = ensure_delegated_token_subject_matches("user-b", "user-a") + .expect_err("mismatched subject must be rejected"); + + assert_eq!(status.code(), Code::PermissionDenied); + assert!(status.message().contains("subject must match")); + } + + #[test] + fn delete_credential_resource_version_zero_uses_current_version() { + let credential = DelegatedIdentityCredential { + metadata: Some(ObjectMeta { + id: "delegated-identity-test".to_string(), + name: "delegated-identity-test".to_string(), + resource_version: 7, + workspace: String::new(), + labels: HashMap::new(), + annotations: HashMap::new(), + ..Default::default() + }), + ..Default::default() + }; + + let version = effective_delete_credential_resource_version(Some(&credential), 0); + + assert_eq!(version, Some(7)); + assert_eq!(effective_delete_credential_resource_version(None, 0), None); + assert_eq!( + effective_delete_credential_resource_version(Some(&credential), 42), + Some(42) + ); + } + + #[test] + fn admin_credential_response_uses_non_secret_summary() { + let credential = DelegatedIdentityCredential { + issuer: "https://issuer.example.com".to_string(), + client_id: "openshell-cli".to_string(), + principal_subject: "user-1".to_string(), + refresh_token: "refresh-secret".to_string(), + access_token: "access-secret".to_string(), + access_token_expires_at_ms: 123, + scopes: "openid profile".to_string(), + audience: "api://resource".to_string(), + last_refresh_at_ms: 42, + revoked_at_ms: 0, + ..Default::default() + }; + + let summary = delegated_credential_summary(credential); + + assert!(summary.refresh_token_present); + assert!(summary.access_token_present); + assert_eq!(summary.issuer, "https://issuer.example.com"); + assert_eq!(summary.principal_subject, "user-1"); + assert_eq!(summary.access_token_expires_at_ms, 123); + } + + #[test] + fn delegated_credential_upsert_uses_existing_resource_version_for_cas() { + let credential = DelegatedIdentityCredential { + metadata: Some(ObjectMeta { + id: "delegated-identity-test".to_string(), + name: "delegated-identity-test".to_string(), + resource_version: 7, + workspace: String::new(), + labels: HashMap::new(), + annotations: HashMap::new(), + ..Default::default() + }), + ..Default::default() + }; + + assert!(matches!( + delegated_credential_upsert_condition(&credential), + WriteCondition::MatchResourceVersion(7) + )); + } + + #[test] + fn sandbox_status_reports_revoked_backing_credential() { + let credential = DelegatedIdentityCredential { + metadata: Some(ObjectMeta { + id: "delegated-identity-test".to_string(), + name: "delegated-identity-test".to_string(), + resource_version: 7, + workspace: String::new(), + labels: HashMap::new(), + annotations: HashMap::new(), + ..Default::default() + }), + revoked_at_ms: 42, + ..Default::default() + }; + + let (revoked_at_ms, missing) = delegated_credential_status_fields(Some(&credential)); + assert_eq!(revoked_at_ms, 42); + assert!(!missing); + assert_eq!(delegated_credential_status_fields(None), (0, true)); + } + + #[tokio::test] + async fn prepared_sandbox_create_cleanup_deletes_only_new_credentials() { + let state = test_server_state().await; + let new = put_test_credential(&state, "delegated-identity-new").await; + let reused = put_test_credential(&state, "delegated-identity-reused").await; + + delete_new_prepared_sandbox_credential(&state, Some(&prepared_test_delegation(&new, true))) + .await + .expect("new credential cleanup should succeed"); + delete_new_prepared_sandbox_credential( + &state, + Some(&prepared_test_delegation(&reused, false)), + ) + .await + .expect("reused credential cleanup should be a no-op"); + + assert!( + state + .store + .get_message::(new.object_id()) + .await + .unwrap() + .is_none() + ); + assert!( + state + .store + .get_message::(reused.object_id()) + .await + .unwrap() + .is_some() + ); + } + + #[tokio::test] + async fn delegated_refresh_rejects_access_token_for_different_subject() { + let server = wiremock::MockServer::start().await; + mount_test_oidc_issuer(&server).await; + let issuer = server.uri(); + let state = test_server_state_with_oidc(issuer.clone()).await; + let original_access_token = + mint_test_access_token(&issuer, "alice", current_time_secs() + 3600); + let mismatched_access_token = + mint_test_access_token(&issuer, "bob", current_time_secs() + 3600); + let credential = DelegatedIdentityCredential { + metadata: Some(ObjectMeta { + id: "delegated-identity-refresh".to_string(), + name: "delegated-identity-refresh".to_string(), + workspace: String::new(), + labels: HashMap::new(), + annotations: HashMap::new(), + ..Default::default() + }), + issuer: issuer.clone(), + client_id: TEST_AUDIENCE.to_string(), + principal_subject: "alice".to_string(), + refresh_token: "refresh-token".to_string(), + access_token: original_access_token.clone(), + access_token_expires_at_ms: current_time_ms() - 1, + scopes: "openid profile".to_string(), + ..Default::default() + }; + state.store.put_message(&credential).await.unwrap(); + let credential = state + .store + .get_message::("delegated-identity-refresh") + .await + .unwrap() + .unwrap(); + mount_refresh_token_response(&server, &mismatched_access_token).await; + + let status = super::refresh_if_needed(&state, credential) + .await + .expect_err("mismatched refreshed subject must be rejected"); + + assert_eq!(status.code(), Code::PermissionDenied); + assert!(status.message().contains("subject must match")); + let stored = state + .store + .get_message::("delegated-identity-refresh") + .await + .unwrap() + .unwrap(); + assert_eq!(stored.access_token, original_access_token); + assert_eq!(stored.principal_subject, "alice"); + } + + #[tokio::test] + async fn delegated_request_expiry_is_derived_from_validated_access_token() { + let server = wiremock::MockServer::start().await; + mount_test_oidc_issuer(&server).await; + let issuer = server.uri(); + let state = test_server_state_with_oidc(issuer.clone()).await; + let exp_secs = current_time_secs() + 1800; + let user = crate::auth::principal::UserPrincipal { + identity: crate::auth::identity::Identity { + subject: "alice".to_string(), + display_name: None, + roles: vec!["openshell-user".to_string()], + scopes: vec![], + provider: crate::auth::identity::IdentityProvider::Oidc, + }, + }; + let request = DelegatedIdentityRequest { + issuer: issuer.clone(), + client_id: TEST_AUDIENCE.to_string(), + refresh_token: "refresh-token".to_string(), + access_token: mint_test_access_token(&issuer, "alice", exp_secs), + delegated_until_ms: current_time_ms() + 600_000, + scopes: "openid profile".to_string(), + audience: TEST_AUDIENCE.to_string(), + }; + + let expires_at_ms = super::validate_delegation_request(&state, &user, &request) + .await + .expect("delegation request should validate"); + let upserted = super::upsert_credential(&state, &user, request, expires_at_ms) + .await + .expect("credential should persist"); + + assert_eq!(expires_at_ms, exp_secs.saturating_mul(1000)); + assert_eq!( + upserted.credential.access_token_expires_at_ms, + exp_secs.saturating_mul(1000) + ); + } + + #[tokio::test] + async fn extend_cleanup_deletes_new_credential_when_record_update_fails() { + let server = wiremock::MockServer::start().await; + mount_test_oidc_issuer(&server).await; + let issuer = server.uri(); + let state = test_server_state_with_oidc(issuer.clone()).await; + put_test_sandbox(&state, "delegated").await; + let existing_credential = + put_test_credential_for_subject(&state, "delegated-identity-existing", "dev-user") + .await; + let sandbox_id = "sandbox-delegated"; + let record_id = sandbox_delegated_identity_record_id(sandbox_id); + let malformed_record = SandboxDelegatedIdentityRecord { + metadata: Some(ObjectMeta { + id: String::new(), + name: record_id.clone(), + workspace: "default".to_string(), + labels: HashMap::new(), + annotations: HashMap::new(), + ..Default::default() + }), + sandbox_id: sandbox_id.to_string(), + delegated_identity: Some(SandboxDelegatedIdentity { + credential_id: existing_credential.object_id().to_string(), + principal_subject: "dev-user".to_string(), + delegated_until_ms: current_time_ms() + 600_000, + withdrawn_at_ms: 0, + }), + }; + state + .store + .put_scoped( + SandboxDelegatedIdentityRecord::object_type(), + &record_id, + &record_id, + "default", + sandbox_id, + &malformed_record.encode_to_vec(), + None, + ) + .await + .expect("store malformed record under valid lookup key"); + + let new_client_id = "openshell-cli-extend"; + let new_credential_id = delegated_credential_id(&issuer, new_client_id, "dev-user"); + let request = authed_request(ExtendSandboxDelegatedIdentityRequest { + name: "delegated".to_string(), + workspace: "default".to_string(), + delegated_identity: Some(DelegatedIdentityRequest { + issuer: issuer.clone(), + client_id: new_client_id.to_string(), + refresh_token: "new-refresh".to_string(), + access_token: mint_test_access_token( + &issuer, + "dev-user", + current_time_secs() + 3600, + ), + delegated_until_ms: current_time_ms() + 600_000, + scopes: "openid profile".to_string(), + audience: TEST_AUDIENCE.to_string(), + }), + }); + + let status = super::handle_extend(&state, request) + .await + .expect_err("record update failure should fail extend"); + + assert!( + status.message().contains("extend delegated identity"), + "unexpected error: {status:?}" + ); + assert!( + state + .store + .get_message::(&new_credential_id) + .await + .unwrap() + .is_none(), + "newly-created credential should be cleaned up" + ); + assert!( + state + .store + .get_message::(existing_credential.object_id()) + .await + .unwrap() + .is_some(), + "pre-existing credential should not be cleaned up" + ); + } + + #[tokio::test] + async fn sandbox_delegated_identity_status_reports_disabled_for_regular_sandbox() { + let state = test_server_state().await; + put_test_sandbox(&state, "regular").await; + + let response = super::handle_status( + &state, + authed_request(GetSandboxDelegatedIdentityStatusRequest { + name: "regular".to_string(), + workspace: "default".to_string(), + }), + ) + .await + .expect("regular sandbox status should report disabled") + .into_inner(); + + assert!(response.delegated_identity.is_none()); + assert!(response.credential_missing); + assert_eq!(response.credential_revoked_at_ms, 0); + assert!(response.now_ms > 0); + } + + #[tokio::test] + async fn sandbox_delegated_identity_status_rejects_non_delegating_user() { + let state = test_server_state().await; + put_test_sandbox(&state, "delegated").await; + let credential = put_test_credential(&state, "delegated-identity-status").await; + state + .store + .put_scoped_message( + &SandboxDelegatedIdentityRecord { + metadata: Some(ObjectMeta { + id: "sandbox-delegated-identity-sandbox-delegated".to_string(), + name: "sandbox-delegated-identity-sandbox-delegated".to_string(), + workspace: "default".to_string(), + labels: HashMap::new(), + annotations: HashMap::new(), + ..Default::default() + }), + sandbox_id: "sandbox-delegated".to_string(), + delegated_identity: Some(SandboxDelegatedIdentity { + credential_id: credential.object_id().to_string(), + principal_subject: "alice".to_string(), + delegated_until_ms: 2_000_000, + withdrawn_at_ms: 0, + }), + }, + "sandbox-delegated", + ) + .await + .unwrap(); + + let mut request = Request::new(GetSandboxDelegatedIdentityStatusRequest { + name: "delegated".to_string(), + workspace: "default".to_string(), + }); + request + .extensions_mut() + .insert(crate::auth::principal::Principal::User( + crate::auth::principal::UserPrincipal { + identity: crate::auth::identity::Identity { + subject: "bob".to_string(), + display_name: None, + roles: vec!["openshell-user".to_string()], + scopes: vec![], + provider: crate::auth::identity::IdentityProvider::Oidc, + }, + }, + )); + + let status = super::handle_status(&state, request) + .await + .expect_err("non-delegating user should be rejected"); + + assert_eq!(status.code(), Code::PermissionDenied); + assert!(status.message().contains("delegating principal")); + } + + async fn put_test_credential( + state: &Arc, + id: &str, + ) -> DelegatedIdentityCredential { + put_test_credential_for_subject(state, id, "user-1").await + } + + async fn put_test_credential_for_subject( + state: &Arc, + id: &str, + subject: &str, + ) -> DelegatedIdentityCredential { + let credential = DelegatedIdentityCredential { + metadata: Some(ObjectMeta { + id: id.to_string(), + name: id.to_string(), + workspace: String::new(), + labels: HashMap::new(), + annotations: HashMap::new(), + ..Default::default() + }), + issuer: "https://issuer.example.com".to_string(), + client_id: "openshell-cli".to_string(), + principal_subject: subject.to_string(), + refresh_token: "refresh".to_string(), + access_token: "access".to_string(), + ..Default::default() + }; + state.store.put_message(&credential).await.unwrap(); + state + .store + .get_message::(id) + .await + .unwrap() + .unwrap() + } + + async fn put_test_sandbox(state: &Arc, name: &str) { + state + .store + .put_message(&Sandbox { + metadata: Some(ObjectMeta { + id: format!("sandbox-{name}"), + name: name.to_string(), + workspace: "default".to_string(), + labels: HashMap::new(), + annotations: HashMap::new(), + ..Default::default() + }), + spec: Some(SandboxSpec::default()), + status: Some(SandboxStatus::default()), + }) + .await + .unwrap(); + } + + fn prepared_test_delegation( + credential: &DelegatedIdentityCredential, + credential_created: bool, + ) -> PreparedSandboxDelegatedIdentity { + PreparedSandboxDelegatedIdentity { + record: SandboxDelegatedIdentityRecord { + metadata: Some(ObjectMeta { + id: format!("sandbox-delegated-identity-{credential_created}"), + name: format!("sandbox-delegated-identity-{credential_created}"), + workspace: "default".to_string(), + labels: HashMap::new(), + annotations: HashMap::new(), + ..Default::default() + }), + sandbox_id: "sandbox-test".to_string(), + delegated_identity: Some(SandboxDelegatedIdentity { + credential_id: credential.object_id().to_string(), + principal_subject: credential.principal_subject.clone(), + delegated_until_ms: 2_000_000, + withdrawn_at_ms: 0, + }), + }, + credential_id: credential.object_id().to_string(), + credential_resource_version: credential.get_resource_version(), + credential_created, + } + } + + async fn test_server_state_with_oidc(issuer: String) -> Arc { + let store = Arc::new( + Store::connect("sqlite::memory:?cache=shared") + .await + .unwrap(), + ); + crate::ensure_default_workspace(&store).await.unwrap(); + let compute = crate::compute::new_test_runtime(store.clone()).await; + let oidc = OidcConfig { + issuer, + audience: TEST_AUDIENCE.to_string(), + jwks_ttl_secs: 3600, + roles_claim: "realm_access.roles".to_string(), + admin_role: "openshell-admin".to_string(), + user_role: "openshell-user".to_string(), + scopes_claim: "scope".to_string(), + }; + let oidc_cache = Arc::new( + crate::auth::oidc::JwksCache::new(&oidc) + .await + .expect("OIDC cache should build from mock issuer"), + ); + Arc::new(crate::ServerState::new( + Config::new(None) + .with_database_url("sqlite::memory:?cache=shared") + .with_credential_drivers(["test-static"]) + .with_oidc(oidc), + store, + compute, + SandboxIndex::new(), + SandboxWatchBus::new(), + TracingLogBus::new(), + Arc::new(SupervisorSessionRegistry::new()), + Some(oidc_cache), + )) + } + + async fn mount_test_oidc_issuer(server: &wiremock::MockServer) { + use wiremock::matchers::{method, path}; + use wiremock::{Mock, ResponseTemplate}; + + let issuer = server.uri(); + Mock::given(method("GET")) + .and(path("/.well-known/openid-configuration")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "issuer": issuer, + "jwks_uri": format!("{issuer}/jwks"), + "token_endpoint": format!("{issuer}/token"), + }))) + .mount(server) + .await; + Mock::given(method("GET")) + .and(path("/jwks")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "keys": [{ + "kid": TEST_KID, + "kty": "RSA", + "n": TEST_RSA_KEY.modulus_b64, + "e": TEST_RSA_KEY.exponent_b64, + }], + }))) + .mount(server) + .await; + } + + async fn mount_refresh_token_response(server: &wiremock::MockServer, access_token: &str) { + use wiremock::matchers::{method, path}; + use wiremock::{Mock, ResponseTemplate}; + + Mock::given(method("POST")) + .and(path("/token")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "access_token": access_token, + "token_type": "Bearer", + "expires_in": 3600, + }))) + .mount(server) + .await; + } + + fn mint_test_access_token(issuer: &str, subject: &str, exp: i64) -> String { + crate::install_jsonwebtoken_crypto_provider(); + + let mut header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256); + header.kid = Some(TEST_KID.to_string()); + let key = jsonwebtoken::EncodingKey::from_rsa_pem(TEST_RSA_KEY.private_pem.as_bytes()) + .expect("load RSA signing key"); + jsonwebtoken::encode( + &header, + &serde_json::json!({ + "sub": subject, + "preferred_username": subject, + "iss": issuer, + "aud": TEST_AUDIENCE, + "exp": exp, + "scope": "openid profile sandbox:write", + "realm_access": { "roles": ["openshell-user"] }, + }), + &key, + ) + .expect("sign RS256 token") + } + + fn current_time_secs() -> i64 { + i64::try_from( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock is after the unix epoch") + .as_secs(), + ) + .expect("current time fits in i64") + } +} diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index 2c52acbe12..9104f0b77e 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -18,23 +18,30 @@ use openshell_core::proto::{ ClearDraftChunksResponse, ComputeDriverCapabilities, ComputeDriverInfo, ConfigureProviderRefreshRequest, ConfigureProviderRefreshResponse, CreateProviderRequest, CreateSandboxRequest, CreateSshSessionRequest, CreateSshSessionResponse, - CreateWorkspaceRequest, CreateWorkspaceResponse, DeleteProviderProfileRequest, + CreateWorkspaceRequest, CreateWorkspaceResponse, DeleteDelegatedIdentityCredentialRequest, + DeleteDelegatedIdentityCredentialResponse, DeleteProviderProfileRequest, DeleteProviderProfileResponse, DeleteProviderRefreshRequest, DeleteProviderRefreshResponse, DeleteProviderRequest, DeleteProviderResponse, DeleteSandboxRequest, DeleteSandboxResponse, DeleteServiceRequest, DeleteServiceResponse, DeleteWorkspaceRequest, DeleteWorkspaceResponse, DetachSandboxProviderRequest, DetachSandboxProviderResponse, EditDraftChunkRequest, - EditDraftChunkResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, - ExposeServiceRequest, GatewayMessage, GetCurrentUserRequest, GetCurrentUserResponse, - GetDraftHistoryRequest, GetDraftHistoryResponse, GetDraftPolicyRequest, GetDraftPolicyResponse, - GetGatewayConfigRequest, GetGatewayConfigResponse, GetGatewayInfoRequest, - GetGatewayInfoResponse, GetProviderProfileRequest, GetProviderRefreshStatusRequest, - GetProviderRefreshStatusResponse, GetProviderRequest, GetSandboxConfigRequest, - GetSandboxConfigResponse, GetSandboxLogsRequest, GetSandboxLogsResponse, - GetSandboxPolicyStatusRequest, GetSandboxPolicyStatusResponse, - GetSandboxProviderEnvironmentRequest, GetSandboxProviderEnvironmentResponse, GetSandboxRequest, - GetServiceRequest, GetWorkspaceRequest, GetWorkspaceResponse, HealthRequest, HealthResponse, + EditDraftChunkResponse, ExchangeProviderSubjectTokenRequest, + ExchangeProviderSubjectTokenResponse, ExecSandboxEvent, ExecSandboxInput, ExecSandboxRequest, + ExposeServiceRequest, ExtendSandboxDelegatedIdentityRequest, + ExtendSandboxDelegatedIdentityResponse, GatewayMessage, GetCurrentUserRequest, + GetCurrentUserResponse, GetDelegatedIdentityCredentialStatusRequest, + GetDelegatedIdentityCredentialStatusResponse, GetDraftHistoryRequest, GetDraftHistoryResponse, + GetDraftPolicyRequest, GetDraftPolicyResponse, GetGatewayConfigRequest, + GetGatewayConfigResponse, GetGatewayInfoRequest, GetGatewayInfoResponse, + GetProviderProfileRequest, GetProviderRefreshStatusRequest, GetProviderRefreshStatusResponse, + GetProviderRequest, GetSandboxConfigRequest, GetSandboxConfigResponse, + GetSandboxDelegatedIdentityStatusRequest, GetSandboxDelegatedIdentityStatusResponse, + GetSandboxLogsRequest, GetSandboxLogsResponse, GetSandboxPolicyStatusRequest, + GetSandboxPolicyStatusResponse, GetSandboxProviderEnvironmentRequest, + GetSandboxProviderEnvironmentResponse, GetSandboxRequest, GetServiceRequest, + GetWorkspaceRequest, GetWorkspaceResponse, HealthRequest, HealthResponse, ImportProviderProfilesRequest, ImportProviderProfilesResponse, IssueSandboxTokenRequest, IssueSandboxTokenResponse, LintProviderProfilesRequest, LintProviderProfilesResponse, + ListDelegatedIdentityCredentialsRequest, ListDelegatedIdentityCredentialsResponse, ListProviderProfilesRequest, ListProviderProfilesResponse, ListProvidersRequest, ListProvidersResponse, ListSandboxPoliciesRequest, ListSandboxPoliciesResponse, ListSandboxProvidersRequest, ListSandboxProvidersResponse, ListSandboxesRequest, @@ -44,13 +51,15 @@ use openshell_core::proto::{ RefreshSandboxTokenRequest, RefreshSandboxTokenResponse, RejectDraftChunkRequest, RejectDraftChunkResponse, RelayFrame, RemoveWorkspaceMemberRequest, RemoveWorkspaceMemberResponse, ReportPolicyStatusRequest, ReportPolicyStatusResponse, + RevokeDelegatedIdentityCredentialRequest, RevokeDelegatedIdentityCredentialResponse, RevokeSshSessionRequest, RevokeSshSessionResponse, RotateProviderCredentialRequest, RotateProviderCredentialResponse, SandboxResponse, ServiceEndpointResponse, ServiceStatus, StartSandboxRequest, StopSandboxRequest, SubmitPolicyAnalysisRequest, SubmitPolicyAnalysisResponse, SupervisorMessage, TcpForwardFrame, UndoDraftChunkRequest, UndoDraftChunkResponse, UpdateConfigRequest, UpdateConfigResponse, UpdateProviderProfilesRequest, UpdateProviderProfilesResponse, UpdateProviderRequest, - WatchSandboxRequest, open_shell_server::OpenShell, + WatchSandboxRequest, WithdrawSandboxDelegatedIdentityRequest, + WithdrawSandboxDelegatedIdentityResponse, open_shell_server::OpenShell, }; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -273,6 +282,27 @@ impl OpenShell for OpenShellService { sandbox::handle_create_sandbox(&self.state, request).await } + async fn get_sandbox_delegated_identity_status( + &self, + request: Request, + ) -> Result, Status> { + crate::delegated_identity::handle_status(&self.state, request).await + } + + async fn withdraw_sandbox_delegated_identity( + &self, + request: Request, + ) -> Result, Status> { + crate::delegated_identity::handle_withdraw(&self.state, request).await + } + + async fn extend_sandbox_delegated_identity( + &self, + request: Request, + ) -> Result, Status> { + crate::delegated_identity::handle_extend(&self.state, request).await + } + type WatchSandboxStream = sandbox::WatchSandboxStream; async fn watch_sandbox( @@ -542,6 +572,41 @@ 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 list_delegated_identity_credentials( + &self, + request: Request, + ) -> Result, Status> { + crate::delegated_identity::handle_list_credentials(&self.state, request).await + } + + async fn get_delegated_identity_credential_status( + &self, + request: Request, + ) -> Result, Status> { + crate::delegated_identity::handle_get_credential_status(&self.state, request).await + } + + async fn revoke_delegated_identity_credential( + &self, + request: Request, + ) -> Result, Status> { + crate::delegated_identity::handle_revoke_credential(&self.state, request).await + } + + async fn delete_delegated_identity_credential( + &self, + request: Request, + ) -> Result, Status> { + crate::delegated_identity::handle_delete_credential(&self.state, request).await + } + async fn update_config( &self, request: Request, diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 3b841e66a5..f92b16d0dc 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -2561,6 +2561,8 @@ async fn handle_update_config_inner( .await .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? .ok_or_else(|| Status::not_found("sandbox not found"))?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, principal, &sandbox) + .await?; let sandbox_id = sandbox.object_id().to_string(); let mut response_annotations = sandbox_metadata_annotations(&sandbox); @@ -3338,6 +3340,8 @@ pub(super) async fn handle_submit_policy_analysis( &req.name, ) .await?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let sandbox_id = sandbox.object_id().to_string(); for summary in &req.network_activity_summaries { state @@ -3724,6 +3728,8 @@ async fn handle_approve_draft_chunk_inner( .await .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? .ok_or_else(|| Status::not_found("sandbox not found"))?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let sandbox_id = sandbox.object_id().to_string(); let chunk = state @@ -3847,6 +3853,8 @@ async fn handle_reject_draft_chunk_inner( .await .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? .ok_or_else(|| Status::not_found("sandbox not found"))?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let sandbox_id = sandbox.object_id().to_string(); let chunk = state @@ -3956,6 +3964,8 @@ async fn handle_approve_all_draft_chunks_inner( .await .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? .ok_or_else(|| Status::not_found("sandbox not found"))?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let sandbox_id = sandbox.object_id().to_string(); let pending_chunks = state @@ -4127,6 +4137,8 @@ pub(super) async fn handle_edit_draft_chunk( .await .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? .ok_or_else(|| Status::not_found("sandbox not found"))?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let sandbox_id = sandbox.object_id().to_string(); let chunk = state @@ -4200,6 +4212,8 @@ async fn handle_undo_draft_chunk_inner( .await .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? .ok_or_else(|| Status::not_found("sandbox not found"))?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let sandbox_id = sandbox.object_id().to_string(); let chunk = state @@ -4294,6 +4308,8 @@ pub(super) async fn handle_clear_draft_chunks( .await .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? .ok_or_else(|| Status::not_found("sandbox not found"))?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let sandbox_id = sandbox.object_id().to_string(); let deleted = state diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index 4d67eee018..bfa4d843f0 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, @@ -3273,6 +3364,492 @@ 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"))?; + let (stored_subject_token, subject_token_expires_at_ms, subject_cache_key) = + match subject_token.source.as_str() { + "provider_credential" => { + 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", + )); + } + ( + resolve_subject_token_credential( + &state.credentials, + &provider, + &subject_token.credential, + ) + .await?, + provider_credential_expires_at_ms(&provider, &subject_token.credential), + subject_token.credential.clone(), + ) + } + "sandbox_delegated_identity" => { + if !subject_token.credential.trim().is_empty() { + return Err(Status::failed_precondition( + "sandbox_delegated_identity subject_token must not set credential", + )); + } + let (access_token, expires_at_ms, credential_id) = + crate::delegated_identity::resolve_subject_access_token(state, &sandbox) + .await?; + (access_token, expires_at_ms, credential_id) + } + _ => { + return Err(Status::failed_precondition( + "unsupported subject_token source", + )); + } + }; + + 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_cache_key, + 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_cache_key, + 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, + subject_token_expires_at_ms, + 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, @@ -3969,13 +4546,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() @@ -4745,12 +5325,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(), }), @@ -6681,6 +7264,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/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 9338956950..2b56203290 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -11,7 +11,8 @@ use crate::ServerState; use crate::auth::workspace_authz::{ - MinWorkspaceRole, authorize_sandbox_workspace, authorize_workspace, require_platform_admin, + AuthGrant, MinWorkspaceRole, authorize_sandbox_workspace, authorize_workspace, + require_platform_admin, }; use crate::persistence::{ObjectLabels, ObjectType, WriteCondition, generate_name}; use futures::future; @@ -110,9 +111,12 @@ impl Drop for WatchSandboxStream { } } -/// Fetch a sandbox by ID and authorize the caller in one step, returning -/// `NOT_FOUND` for both missing and unauthorized sandboxes so that callers -/// cannot distinguish the two cases (CWE-203). +/// Fetch a sandbox by ID and authorize the caller in one step. +/// +/// Workspace RBAC denials are normalized to `NOT_FOUND`, matching the legacy +/// cross-workspace behavior. Delegated-identity denials remain +/// `PERMISSION_DENIED`: workspace users may see the sandbox, but only the +/// delegating principal may perform delegated-identity-sensitive operations. pub(super) async fn fetch_and_authorize_sandbox( state: &Arc, principal: &crate::auth::principal::Principal, @@ -139,6 +143,8 @@ pub(super) async fn fetch_and_authorize_sandbox( e } })?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, principal, &sandbox) + .await?; Ok(sandbox) } @@ -217,6 +223,7 @@ async fn handle_create_sandbox_inner( ) -> Result, Status> { let principal = super::extract_principal(&request)?; let request = request.into_inner(); + let delegated_identity_request = request.delegated_identity.clone(); let spec = request .spec .ok_or_else(|| Status::invalid_argument("spec is required"))?; @@ -325,6 +332,14 @@ async fn handle_create_sandbox_inner( status })?; + let delegated_identity = crate::delegated_identity::prepare_for_sandbox_create( + state, + &principal, + &sandbox, + delegated_identity_request, + ) + .await?; + // Mint the gateway JWT for singleplayer drivers. K8s sandboxes skip // this mint and bootstrap via `IssueSandboxToken` at supervisor // startup; identifying "is this K8s?" lives in the compute layer, so @@ -345,7 +360,59 @@ async fn handle_create_sandbox_inner( None => None, }; - let sandbox = state.compute.create_sandbox(sandbox, sandbox_token).await?; + if let Err(status) = crate::delegated_identity::store_prepared_sandbox_delegation( + state, + delegated_identity.as_ref(), + ) + .await + { + if let Err(cleanup_status) = + crate::delegated_identity::delete_new_prepared_sandbox_credential( + state, + delegated_identity.as_ref(), + ) + .await + { + warn!( + sandbox_id = %id, + error = %cleanup_status, + "failed to clean up prepared delegated credential after sandbox delegation persist failure" + ); + } + return Err(status); + } + let sandbox = match state.compute.create_sandbox(sandbox, sandbox_token).await { + Ok(sandbox) => sandbox, + Err(status) => { + if let Err(cleanup_status) = + crate::delegated_identity::delete_prepared_sandbox_delegation( + state, + delegated_identity.as_ref(), + ) + .await + { + warn!( + sandbox_id = %id, + error = %cleanup_status, + "failed to clean up prepared delegated identity after sandbox create failure" + ); + } + if let Err(cleanup_status) = + crate::delegated_identity::delete_new_prepared_sandbox_credential( + state, + delegated_identity.as_ref(), + ) + .await + { + warn!( + sandbox_id = %id, + error = %cleanup_status, + "failed to clean up prepared delegated credential after sandbox create failure" + ); + } + return Err(status); + } + }; info!( sandbox_id = %id, @@ -475,6 +542,8 @@ pub(super) async fn handle_list_sandbox_providers( .await? .name; let sandbox = sandbox_by_name(state, &workspace, &req.sandbox_name).await?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let providers = providers_for_sandbox(state, &sandbox, &workspace).await?; Ok(Response::new(ListSandboxProvidersResponse { providers })) } @@ -525,6 +594,8 @@ pub(super) async fn handle_attach_sandbox_provider( let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await; let sandbox = sandbox_by_name(state, &workspace, &request.sandbox_name).await?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let sandbox_id = sandbox .metadata .as_ref() @@ -648,6 +719,8 @@ pub(super) async fn handle_detach_sandbox_provider( let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await; let sandbox = sandbox_by_name(state, &workspace, &request.sandbox_name).await?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let sandbox_id = sandbox .metadata .as_ref() @@ -753,6 +826,16 @@ async fn handle_delete_sandbox_inner( let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace) .await? .name; + let sandbox = sandbox_by_name(state, &workspace, &name).await?; + if !matches!( + authz.grant, + AuthGrant::PlatformAdmin | AuthGrant::Member(openshell_core::proto::WorkspaceRole::Admin) + ) { + crate::delegated_identity::ensure_delegated_identity_sandbox_user( + state, &principal, &sandbox, + ) + .await?; + } let result = state.compute.delete_sandbox(&workspace, &name).await?; if result.deleted { @@ -1784,6 +1867,14 @@ pub(super) async fn handle_revoke_ssh_session( e } })?; + let sandbox = state + .store + .get_message::(&session.sandbox_id) + .await + .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? + .ok_or_else(|| Status::not_found("sandbox not found"))?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let resource_version = session .metadata @@ -2394,10 +2485,15 @@ async fn run_exec_with_russh( #[cfg(test)] mod tests { use super::*; + use crate::auth::identity::{Identity, IdentityProvider}; + use crate::auth::principal::{Principal, UserPrincipal}; use crate::grpc::test_support::{ authed_request, test_server_state, test_server_state_with_driver, }; use openshell_core::proto::datamodel::v1::ObjectMeta; + use openshell_core::proto::{ + SandboxDelegatedIdentity, SandboxDelegatedIdentityRecord, WorkspaceMember, WorkspaceRole, + }; // ---- shell_escape ---- @@ -2747,6 +2843,100 @@ mod tests { sandbox } + async fn put_delegated_test_sandbox( + state: &Arc, + name: &str, + principal_subject: &str, + ) { + let sandbox = test_sandbox(name, Vec::new()); + let sandbox_id = sandbox.object_id().to_string(); + let record_id = + crate::delegated_identity::sandbox_delegated_identity_record_id(&sandbox_id); + let workspace = sandbox.object_workspace().to_string(); + state.store.put_message(&sandbox).await.unwrap(); + state + .store + .put_scoped_message( + &SandboxDelegatedIdentityRecord { + metadata: Some(ObjectMeta { + id: record_id.clone(), + name: record_id, + created_at_ms: 1_000_000, + labels: HashMap::new(), + resource_version: 0, + annotations: HashMap::new(), + workspace, + deletion_timestamp_ms: 0, + }), + sandbox_id: sandbox_id.clone(), + delegated_identity: Some(SandboxDelegatedIdentity { + credential_id: "delegated-credential".to_string(), + principal_subject: principal_subject.to_string(), + delegated_until_ms: 2_000_000, + withdrawn_at_ms: 0, + }), + }, + &sandbox_id, + ) + .await + .unwrap(); + } + + async fn put_workspace_member(state: &Arc, subject: &str, role: WorkspaceRole) { + state + .store + .put_message(&WorkspaceMember { + metadata: Some(ObjectMeta { + id: format!("workspace-member-{subject}"), + name: subject.to_string(), + created_at_ms: 1_000_000, + labels: HashMap::new(), + resource_version: 0, + annotations: HashMap::new(), + workspace: "default".to_string(), + deletion_timestamp_ms: 0, + }), + principal_subject: subject.to_string(), + role: role.into(), + }) + .await + .unwrap(); + } + + fn user_request(mut request: Request, subject: &str) -> Request { + request + .extensions_mut() + .insert(Principal::User(UserPrincipal { + identity: Identity { + subject: subject.to_string(), + display_name: None, + roles: vec![], + scopes: vec![], + provider: IdentityProvider::Oidc, + }, + })); + request + } + + fn user_request_with_roles( + mut request: Request, + subject: &str, + roles: &[&str], + ) -> Request { + request + .extensions_mut() + .insert(Principal::User(UserPrincipal { + identity: Identity { + subject: subject.to_string(), + display_name: None, + roles: roles.iter().map(|role| (*role).to_string()).collect(), + scopes: vec![], + provider: IdentityProvider::Oidc, + }, + })); + request + } + #[tokio::test] #[ignore = "flaky under concurrent test execution"] async fn watch_producer_releases_request_span_when_client_disconnects() { @@ -3269,6 +3459,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + delegated_identity: None, }), ) .await @@ -3303,6 +3494,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + delegated_identity: None, }), ) .await @@ -3327,6 +3519,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::from([(annotation_key.clone(), annotation_value.clone())]), workspace: String::new(), + delegated_identity: None, }), ) .await @@ -3387,6 +3580,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + delegated_identity: None, }), ) .await @@ -3452,6 +3646,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + delegated_identity: None, }), ) .await @@ -3482,6 +3677,7 @@ mod tests { labels: HashMap::from([("team".to_string(), "x".repeat(512))]), annotations: HashMap::new(), workspace: String::new(), + delegated_identity: None, }), ) .await @@ -3514,6 +3710,7 @@ mod tests { labels: HashMap::new(), annotations: HashMap::new(), workspace: String::new(), + delegated_identity: None, }), ) .await @@ -3810,6 +4007,213 @@ mod tests { assert!(session2.is_some()); } + #[tokio::test] + async fn delegated_identity_sandbox_allows_delegating_user_to_create_ssh_session() { + let state = test_server_state().await; + put_delegated_test_sandbox(&state, "work", "alice").await; + + let response = handle_create_ssh_session( + &state, + user_request( + Request::new(CreateSshSessionRequest { + sandbox_id: "sandbox-work".to_string(), + }), + "alice", + ), + ) + .await + .expect("delegating user should be allowed") + .into_inner(); + + assert_eq!(response.sandbox_id, "sandbox-work"); + } + + #[tokio::test] + async fn delegated_identity_sandbox_rejects_other_user_create_ssh_session() { + let state = test_server_state().await; + put_delegated_test_sandbox(&state, "work", "alice").await; + + let err = handle_create_ssh_session( + &state, + user_request( + Request::new(CreateSshSessionRequest { + sandbox_id: "sandbox-work".to_string(), + }), + "bob", + ), + ) + .await + .expect_err("non-delegating user should be denied"); + + assert_eq!(err.code(), tonic::Code::PermissionDenied); + assert!(err.message().contains("delegating principal")); + } + + #[tokio::test] + async fn delegated_identity_sandbox_remains_visible_to_workspace_user() { + let state = test_server_state().await; + put_delegated_test_sandbox(&state, "work", "alice").await; + + let response = handle_get_sandbox( + &state, + user_request( + Request::new(GetSandboxRequest { + name: "work".to_string(), + workspace: "default".to_string(), + }), + "bob", + ), + ) + .await + .expect("workspace user should still see the sandbox") + .into_inner(); + assert_eq!( + response.sandbox.as_ref().unwrap().object_id(), + "sandbox-work" + ); + + let response = handle_list_sandboxes( + &state, + user_request( + Request::new(ListSandboxesRequest { + limit: 100, + offset: 0, + label_selector: String::new(), + workspace: "default".to_string(), + all_workspaces: false, + }), + "bob", + ), + ) + .await + .expect("workspace user should still list the sandbox") + .into_inner(); + assert_eq!(response.sandboxes.len(), 1); + assert_eq!(response.sandboxes[0].object_id(), "sandbox-work"); + } + + #[tokio::test] + async fn delegated_identity_sandbox_rejects_other_user_delete() { + let mut state = test_server_state().await; + Arc::get_mut(&mut state).unwrap().admin_role = "openshell-admin".to_string(); + put_workspace_member(&state, "bob", WorkspaceRole::User).await; + put_delegated_test_sandbox(&state, "work", "alice").await; + + let err = handle_delete_sandbox( + &state, + user_request( + Request::new(DeleteSandboxRequest { + name: "work".to_string(), + workspace: "default".to_string(), + }), + "bob", + ), + ) + .await + .expect_err("non-delegating user should be denied"); + + assert_eq!(err.code(), tonic::Code::PermissionDenied); + assert!(err.message().contains("delegating principal")); + assert!( + state + .store + .get_message::("sandbox-work") + .await + .unwrap() + .is_some(), + "denied delete must not remove the sandbox" + ); + } + + #[tokio::test] + async fn delegated_identity_sandbox_allows_workspace_admin_delete() { + let mut state = test_server_state().await; + Arc::get_mut(&mut state).unwrap().admin_role = "openshell-admin".to_string(); + put_workspace_member(&state, "bob", WorkspaceRole::Admin).await; + put_delegated_test_sandbox(&state, "work", "alice").await; + + let response = handle_delete_sandbox( + &state, + user_request( + Request::new(DeleteSandboxRequest { + name: "work".to_string(), + workspace: "default".to_string(), + }), + "bob", + ), + ) + .await + .expect("workspace admin should be allowed to delete delegated sandbox") + .into_inner(); + + assert!(response.deleted); + } + + #[tokio::test] + async fn delegated_identity_sandbox_allows_platform_admin_delete() { + let mut state = test_server_state().await; + Arc::get_mut(&mut state).unwrap().admin_role = "openshell-admin".to_string(); + put_delegated_test_sandbox(&state, "work", "alice").await; + + let response = handle_delete_sandbox( + &state, + user_request_with_roles( + Request::new(DeleteSandboxRequest { + name: "work".to_string(), + workspace: "default".to_string(), + }), + "bob", + &["openshell-admin"], + ), + ) + .await + .expect("platform admin should be allowed to delete delegated sandbox") + .into_inner(); + + assert!(response.deleted); + } + + #[tokio::test] + async fn delegated_identity_sandbox_rejects_other_user_revoke_ssh_session() { + let state = test_server_state().await; + put_delegated_test_sandbox(&state, "work", "alice").await; + let token = handle_create_ssh_session( + &state, + user_request( + Request::new(CreateSshSessionRequest { + sandbox_id: "sandbox-work".to_string(), + }), + "alice", + ), + ) + .await + .unwrap() + .into_inner() + .token; + + let err = handle_revoke_ssh_session( + &state, + user_request( + Request::new(RevokeSshSessionRequest { + token: token.clone(), + }), + "bob", + ), + ) + .await + .expect_err("non-delegating user should be denied"); + + assert_eq!(err.code(), tonic::Code::PermissionDenied); + assert!(err.message().contains("delegating principal")); + let session = state + .store + .get_message::(&token) + .await + .unwrap() + .expect("session should still exist after denied revocation"); + assert!(!session.revoked); + } + #[tokio::test] async fn concurrent_revoke_ssh_session_handles_cas_properly() { let state = test_server_state().await; diff --git a/crates/openshell-server/src/grpc/service.rs b/crates/openshell-server/src/grpc/service.rs index 790e26d618..6da17e1072 100644 --- a/crates/openshell-server/src/grpc/service.rs +++ b/crates/openshell-server/src/grpc/service.rs @@ -51,6 +51,8 @@ pub(super) async fn handle_expose_service( .await .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? .ok_or_else(|| Status::not_found("sandbox not found"))?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let now = crate::persistence::current_time_ms(); let key = service_routing::endpoint_key(&req.sandbox, &req.service); @@ -255,6 +257,14 @@ pub(super) async fn handle_delete_service( let Some(endpoint) = endpoint else { return Ok(Response::new(DeleteServiceResponse { deleted: false })); }; + let sandbox = state + .store + .get_message::(&endpoint.sandbox_id) + .await + .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? + .ok_or_else(|| Status::not_found("sandbox not found"))?; + crate::delegated_identity::ensure_delegated_identity_sandbox_user(state, &principal, &sandbox) + .await?; let key = service_routing::endpoint_key(&req.sandbox, &req.service); let deleted = state diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index a6031a9bc6..5382b6df7e 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -30,6 +30,7 @@ mod compute; pub mod config_file; mod credentials; mod defaults; +mod delegated_identity; mod gateway_listener; mod grpc; mod http; @@ -82,6 +83,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 dc039265f6..54daf1727c 100644 --- a/crates/openshell-server/src/provider_refresh.rs +++ b/crates/openshell-server/src/provider_refresh.rs @@ -787,6 +787,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 a2df4755f0..d250482ec8 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, @@ -97,6 +98,30 @@ impl OpenShell for TestOpenShell { Err(Status::unimplemented("unused")) } + async fn get_sandbox_delegated_identity_status( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn withdraw_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn extend_sandbox_delegated_identity( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + async fn get_sandbox( &self, _request: tonic::Request, @@ -120,6 +145,40 @@ impl OpenShell for TestOpenShell { )) } + async fn list_delegated_identity_credentials( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn get_delegated_identity_credential_status( + &self, + _request: tonic::Request< + openshell_core::proto::GetDelegatedIdentityCredentialStatusRequest, + >, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn revoke_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn delete_delegated_identity_credential( + &self, + _request: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + async fn attach_sandbox_provider( &self, _request: tonic::Request, @@ -212,6 +271,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 86c7354647..8e531037d0 100644 --- a/crates/openshell-server/tests/supervisor_relay_integration.rs +++ b/crates/openshell-server/tests/supervisor_relay_integration.rs @@ -85,6 +85,62 @@ impl OpenShell for RelayGateway { Err(Status::unimplemented("unused")) } + async fn get_sandbox_delegated_identity_status( + &self, + _: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn withdraw_sandbox_delegated_identity( + &self, + _: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn extend_sandbox_delegated_identity( + &self, + _: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn list_delegated_identity_credentials( + &self, + _: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn get_delegated_identity_credential_status( + &self, + _: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn revoke_delegated_identity_credential( + &self, + _: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + + async fn delete_delegated_identity_credential( + &self, + _: tonic::Request, + ) -> Result, Status> + { + Err(Status::unimplemented("unused")) + } + type ExecSandboxStream = ReceiverStream>; async fn exec_sandbox( @@ -236,6 +292,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 ae36554be7..671d4ce750 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 6bd0a98476..045151506e 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -3238,6 +3238,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, @@ -3646,6 +3677,36 @@ network_policies: .await; } + 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") } @@ -4020,6 +4081,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) = @@ -6373,6 +6556,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 ce6c581bc8..fc440c6547 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() }, ); @@ -445,6 +474,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" + ); } } @@ -458,6 +520,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() } } @@ -474,6 +554,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 @@ -510,6 +592,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", @@ -751,6 +839,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 b4da286bf8..f0c1c34eed 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(); @@ -6429,6 +6450,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); @@ -6928,20 +6972,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, @@ -6959,6 +6990,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() @@ -9781,6 +9860,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")); @@ -9795,6 +9902,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..210c86f9c8 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,10 @@ 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, -} +const MAX_TOKEN_EXCHANGE_CACHE_TTL_SECONDS: i64 = 300; /// Cached access token with expiration metadata. #[derive(Debug, Clone)] @@ -153,41 +134,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 +240,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 +250,33 @@ 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 cache_ttl_seconds = token_cache_ttl_seconds( + input.cache_ttl_override, + token_response.expires_in, + input.grant_type, + ); 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 +295,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,155 +326,40 @@ 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 { +fn token_cache_ttl_seconds( + cache_ttl_override: i64, + expires_in: i64, + grant_type: ProviderCredentialTokenGrantType, +) -> i64 { if cache_ttl_override > 0 { return cache_ttl_override; } @@ -445,10 +369,22 @@ fn token_cache_ttl_seconds(cache_ttl_override: i64, expires_in: i64) -> i64 { } else { DEFAULT_TOKEN_CACHE_TTL_SECONDS }; + let ttl = token_cache_ttl_cap_for_grant_type(ttl, grant_type); ttl.saturating_sub(TOKEN_CACHE_EXPIRY_SKEW_SECONDS).max(1) } +fn token_cache_ttl_cap_for_grant_type( + ttl_seconds: i64, + grant_type: ProviderCredentialTokenGrantType, +) -> i64 { + if grant_type == ProviderCredentialTokenGrantType::TokenExchange { + ttl_seconds.min(MAX_TOKEN_EXCHANGE_CACHE_TTL_SECONDS) + } else { + ttl_seconds + } +} + /// Derive the issuer/realm URL from a token endpoint URL. /// /// For Keycloak token endpoints like: @@ -483,73 +419,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 +479,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, @@ -764,6 +680,7 @@ mod tests { scopes: &'a [String], cache_ttl_override: i64, expires_in: i64, + grant_type: ProviderCredentialTokenGrantType, grant_calls: Arc, } @@ -778,16 +695,17 @@ mod tests { audience: input.audience, scopes: input.scopes, cache_ttl_override: input.cache_ttl_override, + grant_type: input.grant_type, + 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(" "), }) } }, @@ -795,25 +713,30 @@ mod tests { .await } - async fn obtain_token_without_grant_call( - cache: &TokenCache, - provider_name: &str, - token_endpoint: &str, - jwt_svid_audience: &str, - audience: &str, - scopes: &[String], + struct CachedTokenLookupInput<'a> { + cache: &'a TokenCache, + provider_name: &'a str, + token_endpoint: &'a str, + jwt_svid_audience: &'a str, + audience: &'a str, + scopes: &'a [String], cache_ttl_override: i64, - ) -> Result { + grant_type: ProviderCredentialTokenGrantType, + } + + async fn obtain_token_without_grant_call(input: CachedTokenLookupInput<'_>) -> Result { obtain_provider_token_with_grant( ObtainProviderTokenInput { - cache, - provider_name, - token_endpoint, - jwt_svid_audience, + cache: input.cache, + provider_name: input.provider_name, + token_endpoint: input.token_endpoint, + jwt_svid_audience: input.jwt_svid_audience, client_assertion_type: DEFAULT_CLIENT_ASSERTION_TYPE, - audience, - scopes, - cache_ttl_override, + audience: input.audience, + scopes: input.scopes, + cache_ttl_override: input.cache_ttl_override, + grant_type: input.grant_type, + requested_token_type: "", }, |_| async { Err(miette::miette!("grant should not be called on cache hit")) }, ) @@ -855,125 +778,140 @@ 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] fn token_cache_ttl_uses_override_without_endpoint_skew() { - assert_eq!(token_cache_ttl_seconds(120, 10), 120); - assert_eq!(token_cache_ttl_seconds(120, i64::MAX), 120); + assert_eq!( + token_cache_ttl_seconds(120, 10, ProviderCredentialTokenGrantType::ClientCredentials), + 120 + ); + assert_eq!( + token_cache_ttl_seconds( + 120, + i64::MAX, + ProviderCredentialTokenGrantType::ClientCredentials, + ), + 120 + ); } #[test] fn token_cache_ttl_skews_default_and_response_expires_in() { assert_eq!( - token_cache_ttl_seconds(0, 0), + token_cache_ttl_seconds(0, 0, ProviderCredentialTokenGrantType::ClientCredentials), DEFAULT_TOKEN_CACHE_TTL_SECONDS - TOKEN_CACHE_EXPIRY_SKEW_SECONDS ); - assert_eq!(token_cache_ttl_seconds(0, 60), 30); - assert_eq!(token_cache_ttl_seconds(0, 10), 1); + assert_eq!( + token_cache_ttl_seconds(0, 60, ProviderCredentialTokenGrantType::ClientCredentials), + 30 + ); + assert_eq!( + token_cache_ttl_seconds(0, 10, ProviderCredentialTokenGrantType::ClientCredentials), + 1 + ); } #[test] fn token_cache_ttl_clamps_large_response_expires_in() { assert_eq!( - token_cache_ttl_seconds(0, i64::MAX), + token_cache_ttl_seconds( + 0, + i64::MAX, + ProviderCredentialTokenGrantType::ClientCredentials, + ), MAX_TOKEN_EXPIRES_IN_SECONDS - TOKEN_CACHE_EXPIRY_SKEW_SECONDS ); } @@ -993,19 +931,21 @@ mod tests { scopes: &scopes, cache_ttl_override: 0, expires_in: 60, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, grant_calls: grant_calls.clone(), }) .await .expect("first call should grant token"); - let second = obtain_token_without_grant_call( - &cache, - "api.example.test\t443\t/v1/**\tprovider:access_token", - "https://auth.example.com/token", - "https://auth.example.com", - "api://resource", - &scopes, - 0, - ) + let second = obtain_token_without_grant_call(CachedTokenLookupInput { + cache: &cache, + provider_name: "api.example.test\t443\t/v1/**\tprovider:access_token", + token_endpoint: "https://auth.example.com/token", + jwt_svid_audience: "https://auth.example.com", + audience: "api://resource", + scopes: &scopes, + cache_ttl_override: 0, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, + }) .await .expect("second call should use cache"); @@ -1014,6 +954,64 @@ mod tests { assert_eq!(grant_calls.load(Ordering::SeqCst), 1); } + #[tokio::test] + async fn obtain_provider_token_uses_short_cache_for_token_exchange() { + let cache = TokenCache::new(); + let grant_calls = Arc::new(AtomicUsize::new(0)); + let scopes = vec!["read".to_string()]; + + let first = obtain_counted_test_token(CountedTokenGrantInput { + cache: &cache, + provider_name: "api.example.test\t443\t/v1/**\tprovider:access_token", + token_endpoint: "https://auth.example.com/token", + jwt_svid_audience: "https://auth.example.com", + audience: "api://resource", + scopes: &scopes, + cache_ttl_override: 0, + expires_in: 60, + grant_type: ProviderCredentialTokenGrantType::TokenExchange, + grant_calls: grant_calls.clone(), + }) + .await + .expect("first token exchange should grant token"); + let second = obtain_token_without_grant_call(CachedTokenLookupInput { + cache: &cache, + provider_name: "api.example.test\t443\t/v1/**\tprovider:access_token", + token_endpoint: "https://auth.example.com/token", + jwt_svid_audience: "https://auth.example.com", + audience: "api://resource", + scopes: &scopes, + cache_ttl_override: 0, + grant_type: ProviderCredentialTokenGrantType::TokenExchange, + }) + .await + .expect("second token exchange should use supervisor cache"); + + assert_eq!(first, "token-1"); + assert_eq!(second, "token-1"); + assert_eq!(grant_calls.load(Ordering::SeqCst), 1); + assert_eq!( + token_cache_ttl_seconds(0, i64::MAX, ProviderCredentialTokenGrantType::TokenExchange,), + MAX_TOKEN_EXCHANGE_CACHE_TTL_SECONDS - TOKEN_CACHE_EXPIRY_SKEW_SECONDS + ); + assert_eq!( + token_cache_ttl_seconds( + 120, + i64::MAX, + ProviderCredentialTokenGrantType::TokenExchange, + ), + 120 + ); + assert_eq!( + token_cache_ttl_seconds( + 600, + i64::MAX, + ProviderCredentialTokenGrantType::TokenExchange, + ), + 600 + ); + } + #[tokio::test] async fn obtain_provider_token_separates_cache_by_audience_and_scopes() { let cache = TokenCache::new(); @@ -1030,6 +1028,7 @@ mod tests { scopes: &read_scope, cache_ttl_override: 0, expires_in: 60, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, grant_calls: grant_calls.clone(), }) .await @@ -1043,6 +1042,7 @@ mod tests { scopes: &read_scope, cache_ttl_override: 0, expires_in: 60, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, grant_calls: grant_calls.clone(), }) .await @@ -1056,6 +1056,7 @@ mod tests { scopes: &write_scope, cache_ttl_override: 0, expires_in: 60, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, grant_calls: grant_calls.clone(), }) .await @@ -1077,14 +1078,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(), @@ -1100,6 +1103,7 @@ mod tests { scopes: &scopes, cache_ttl_override: 0, expires_in: 60, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, grant_calls: grant_calls.clone(), }) .await @@ -1109,57 +1113,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(); @@ -1175,19 +1128,21 @@ mod tests { scopes: &scopes, cache_ttl_override: 60, expires_in: 0, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, grant_calls: grant_calls.clone(), }) .await .expect("first override call should grant token"); - let second = obtain_token_without_grant_call( - &cache, - "api.example.test\t443\t/v1/**\tprovider:access_token", - "https://auth.example.com/token", - "https://auth.example.com", - "api://resource", - &scopes, - 60, - ) + let second = obtain_token_without_grant_call(CachedTokenLookupInput { + cache: &cache, + provider_name: "api.example.test\t443\t/v1/**\tprovider:access_token", + token_endpoint: "https://auth.example.com/token", + jwt_svid_audience: "https://auth.example.com", + audience: "api://resource", + scopes: &scopes, + cache_ttl_override: 60, + grant_type: ProviderCredentialTokenGrantType::ClientCredentials, + }) .await .expect("override should keep token cached"); @@ -1382,43 +1337,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/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 1f610015b4..3d5bda3be3 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -1407,6 +1407,7 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender) { labels: HashMap::new(), annotations: HashMap::new(), workspace: workspace.clone(), + delegated_identity: None, }; let sandbox_name = diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index 93dab354b6..6bd909fc2b 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 | @@ -249,8 +254,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 5ff608ae59..a73acc9810 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 @@ -108,6 +113,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 }} @@ -180,6 +190,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 afacd01eb4..eaa2140862 100644 --- a/deploy/helm/openshell/tests/gateway_config_test.yaml +++ b/deploy/helm/openshell/tests/gateway_config_test.yaml @@ -549,14 +549,42 @@ 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 - it: fails when serverIssuerRef is set but certManager is disabled template: templates/statefulset.yaml diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index 33337c768e..b9d7387088 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -366,15 +366,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 8d74b3d44f..b65d9b6cc4 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -85,6 +85,8 @@ credential_drivers = ["kubernetes-secrets"] sandbox_namespace = "openshell" ssh_session_ttl_secs = 3600 +# Maximum sandbox delegated identity window, in seconds. +max_delegated_identity_duration_secs = 86400 # Reject invalid policy generations securely by default. Set # "retain_last_valid" only when availability takes priority. @@ -551,6 +553,14 @@ In managed workspace mode, the Kubernetes driver copies each explicitly named namespace on sandbox creation. Shared and operator modes require the Secret to already exist in the sandbox namespace. +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..570605c97c 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,98 @@ openshell provider create \ --runtime-credentials ``` +For `token_exchange` profiles, choose the subject token source explicitly. +Use `source: provider_credential` when the provider record should hold a subject +credential named 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. + +Use `source: sandbox_delegated_identity` when the subject token should be the +creating user's delegated OIDC identity. Create the provider with +`--runtime-credentials`, then create sandboxes with an explicit authorization +window: + +```shell +openshell provider create \ + --name protected-services \ + --type protected-services-profile \ + --runtime-credentials + +openshell sandbox create \ + --provider protected-services \ + --delegate-identity-for=8h +``` + +Delegation must be enabled when the sandbox is created. A sandbox created +without `--delegate-identity-for` cannot add delegated identity later, although +the original delegator can extend or withdraw an existing delegation: + +```shell +openshell sandbox delegated-identity status my-sandbox +openshell sandbox delegated-identity extend my-sandbox --for=24h +openshell sandbox delegated-identity withdraw my-sandbox +``` + +The status command reports `active`, `withdrawn`, `expired`, `revoked`, or +`credential-missing`. `revoked` means the sandbox delegation window still +exists, but the gateway-scoped delegated credential no longer authorizes token +exchange. + +OpenShell stores the delegated refresh token on the gateway and refreshes the +subject access token automatically while the IdP continues to accept that refresh +token. For long-running delegated identity, configure the gateway OIDC client to +issue non-session-bound refresh tokens, such as with `offline_access` when your +IdP supports it. If the IdP rejects refresh with `invalid_grant` or `Session not +active`, re-authenticate locally with `openshell gateway logout` followed by +`openshell gateway login`, then run `openshell sandbox delegated-identity extend` +to replace the gateway's stored delegated credential. + +Platform admins can inspect and revoke the gateway-scoped delegated credential +records: + +```shell +openshell delegated-credential list +openshell delegated-credential status +openshell delegated-credential revoke +openshell delegated-credential delete +``` + 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. Supports `source: provider_credential`, where `credential` names another credential declared in the same profile, and `source: sandbox_delegated_identity`, where the sandbox must have delegated OIDC identity enabled at create time. | +| `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 679e2c326d..bbccb7496a 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" @@ -115,6 +191,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" @@ -127,20 +209,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" @@ -155,7 +237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -164,6 +246,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" @@ -245,6 +339,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" @@ -268,6 +375,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" @@ -282,9 +408,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", @@ -335,6 +461,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -360,6 +487,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" @@ -523,6 +663,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" @@ -566,12 +732,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" @@ -580,7 +758,7 @@ checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -595,6 +773,40 @@ dependencies = [ "libc", ] +[[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]] name = "once_cell" version = "1.21.4" @@ -613,6 +825,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", + "jsonwebtoken", "nix", "prost", "rand", @@ -624,6 +837,10 @@ dependencies = [ "sha2", "tempfile", "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tower", "url", ] @@ -650,12 +867,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" @@ -671,6 +918,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" @@ -771,6 +1024,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" @@ -781,9 +1048,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" @@ -924,6 +1197,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" @@ -934,6 +1213,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" @@ -953,7 +1244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -984,6 +1275,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" @@ -1005,7 +1302,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1028,6 +1325,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" @@ -1052,18 +1379,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]] @@ -1080,6 +1418,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" @@ -1093,9 +1496,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" @@ -1123,6 +1538,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" @@ -1171,6 +1592,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" @@ -1199,6 +1665,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" @@ -1208,6 +1683,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 078dabfdf7..843106a6fa 100644 --- a/e2e/rust/Cargo.toml +++ b/e2e/rust/Cargo.toml @@ -91,6 +91,11 @@ name = "vm_gateway_start" path = "tests/vm_gateway_start.rs" required-features = ["e2e-vm"] +[[test]] +name = "provider_token_exchange" +path = "tests/provider_token_exchange.rs" +required-features = ["e2e-podman"] + [[test]] name = "readyz_health" path = "tests/readyz_health.rs" @@ -169,8 +174,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" @@ -179,6 +186,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" nix = { version = "0.29", features = ["user"] } 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 a30852664c..43d5aca1d7 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -51,6 +51,36 @@ service OpenShell { }; } + // Fetch delegated identity status for one sandbox. + rpc GetSandboxDelegatedIdentityStatus(GetSandboxDelegatedIdentityStatusRequest) + returns (GetSandboxDelegatedIdentityStatusResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:read" + workspace_role: "user" + }; + } + + // Withdraw delegated identity from one sandbox. + rpc WithdrawSandboxDelegatedIdentity(WithdrawSandboxDelegatedIdentityRequest) + returns (WithdrawSandboxDelegatedIdentityResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + + // Extend delegated identity for one sandbox. + rpc ExtendSandboxDelegatedIdentity(ExtendSandboxDelegatedIdentityRequest) + returns (ExtendSandboxDelegatedIdentityResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "sandbox:write" + workspace_role: "user" + }; + } + // Fetch a sandbox by name. rpc GetSandbox(GetSandboxRequest) returns (SandboxResponse) { option (openshell.options.v1.authorization) = { @@ -325,6 +355,46 @@ service OpenShell { }; } + // List delegated identity credentials visible to the caller. + rpc ListDelegatedIdentityCredentials(ListDelegatedIdentityCredentialsRequest) + returns (ListDelegatedIdentityCredentialsResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:read" + global_role: "platform_admin" + }; + } + + // Fetch delegated identity credential status. + rpc GetDelegatedIdentityCredentialStatus(GetDelegatedIdentityCredentialStatusRequest) + returns (GetDelegatedIdentityCredentialStatusResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:read" + global_role: "platform_admin" + }; + } + + // Revoke a delegated identity credential. + rpc RevokeDelegatedIdentityCredential(RevokeDelegatedIdentityCredentialRequest) + returns (RevokeDelegatedIdentityCredentialResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:write" + global_role: "platform_admin" + }; + } + + // Delete a delegated identity credential. + rpc DeleteDelegatedIdentityCredential(DeleteDelegatedIdentityCredentialRequest) + returns (DeleteDelegatedIdentityCredentialResponse) { + option (openshell.options.v1.authorization) = { + auth_mode: "bearer" + scope: "provider:write" + global_role: "platform_admin" + }; + } + // Delete gateway-owned refresh configuration for one provider credential. rpc DeleteProviderRefresh(DeleteProviderRefreshRequest) returns (DeleteProviderRefreshResponse) { @@ -426,6 +496,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) = { @@ -789,9 +868,21 @@ message Sandbox { SandboxSpec spec = 2; // Latest user-facing observed status derived by the gateway. SandboxStatus status = 3; + reserved 4, 5, 6; + reserved "phase", "current_policy_version", "delegated_identity"; +} - reserved 4, 5; - reserved "phase", "current_policy_version"; +message SandboxDelegatedIdentity { + string credential_id = 1; + string principal_subject = 2; + int64 delegated_until_ms = 3; + int64 withdrawn_at_ms = 4; +} + +message SandboxDelegatedIdentityRecord { + openshell.datamodel.v1.ObjectMeta metadata = 1; + string sandbox_id = 2; + SandboxDelegatedIdentity delegated_identity = 3; } // Desired sandbox configuration provided through the public API. @@ -939,6 +1030,54 @@ message CreateSandboxRequest { map annotations = 4; // Workspace for the sandbox. Empty defaults to "default". string workspace = 5; + // Optional gateway-owned delegated identity material. The server persists + // this as a gateway-scoped credential and stores only delegation metadata on + // the sandbox. + DelegatedIdentityRequest delegated_identity = 6; +} + +message DelegatedIdentityRequest { + int64 delegated_until_ms = 1; + string issuer = 2; + string client_id = 3; + string refresh_token = 4 [(openshell.options.v1.secret) = true]; + string access_token = 5 [(openshell.options.v1.secret) = true]; + reserved 6; + reserved "access_token_expires_at_ms"; + string scopes = 7; + string audience = 8; +} + +message GetSandboxDelegatedIdentityStatusRequest { + string name = 1; + string workspace = 2; +} + +message GetSandboxDelegatedIdentityStatusResponse { + SandboxDelegatedIdentity delegated_identity = 1; + int64 now_ms = 2; + int64 credential_revoked_at_ms = 3; + bool credential_missing = 4; +} + +message WithdrawSandboxDelegatedIdentityRequest { + string name = 1; + string workspace = 2; +} + +message WithdrawSandboxDelegatedIdentityResponse { + Sandbox sandbox = 1; + bool withdrawn = 2; +} + +message ExtendSandboxDelegatedIdentityRequest { + string name = 1; + string workspace = 2; + DelegatedIdentityRequest delegated_identity = 3; +} + +message ExtendSandboxDelegatedIdentityResponse { + Sandbox sandbox = 1; } // Get sandbox request. @@ -1478,6 +1617,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; @@ -1502,6 +1660,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. @@ -1596,6 +1765,73 @@ message StoredProviderCredentialRefreshState { map additional_output_keys = 17; } +message DelegatedIdentityCredential { + openshell.datamodel.v1.ObjectMeta metadata = 1; + string issuer = 2; + string client_id = 3; + string principal_subject = 4; + string refresh_token = 5 [(openshell.options.v1.secret) = true]; + string access_token = 6 [(openshell.options.v1.secret) = true]; + int64 access_token_expires_at_ms = 7; + string scopes = 8; + string audience = 9; + int64 last_refresh_at_ms = 10; + int64 revoked_at_ms = 11; +} + +message DelegatedIdentityCredentialSummary { + openshell.datamodel.v1.ObjectMeta metadata = 1; + string issuer = 2; + string client_id = 3; + string principal_subject = 4; + bool refresh_token_present = 5; + bool access_token_present = 6; + int64 access_token_expires_at_ms = 7; + string scopes = 8; + string audience = 9; + int64 last_refresh_at_ms = 10; + int64 revoked_at_ms = 11; +} + +message ListDelegatedIdentityCredentialsRequest { + uint32 limit = 1; + uint32 offset = 2; +} + +message ListDelegatedIdentityCredentialsResponse { + repeated DelegatedIdentityCredentialSummary credentials = 1; +} + +message GetDelegatedIdentityCredentialStatusRequest { + string id = 1; +} + +message GetDelegatedIdentityCredentialStatusResponse { + DelegatedIdentityCredentialSummary credential = 1; + int64 now_ms = 2; +} + +message RevokeDelegatedIdentityCredentialRequest { + string id = 1; + uint64 expected_resource_version = 2; +} + +message RevokeDelegatedIdentityCredentialResponse { + reserved 1; + bool revoked = 2; + int64 revoked_at_ms = 3; + uint64 resource_version = 4; +} + +message DeleteDelegatedIdentityCredentialRequest { + string id = 1; + uint64 expected_resource_version = 2; +} + +message DeleteDelegatedIdentityCredentialResponse { + bool deleted = 1; +} + message GetProviderRefreshStatusRequest { string provider = 1; string credential_key = 2; @@ -1814,6 +2050,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/openshell/v1/internal/converter/coverage_test.go b/sdk/go/openshell/v1/internal/converter/coverage_test.go index 34cdc0e05e..703992d645 100644 --- a/sdk/go/openshell/v1/internal/converter/coverage_test.go +++ b/sdk/go/openshell/v1/internal/converter/coverage_test.go @@ -257,6 +257,9 @@ func TestConverterCoversAllProtoFields_ProviderCredentialTokenGrant(t *testing.T "cache_ttl_seconds": true, "audience_overrides": true, "client_assertion_type": true, + "grant_type": true, + "subject_token": true, + "requested_token_type": true, } assertAllFieldsCovered(t, (&pb.ProviderCredentialTokenGrant{}).ProtoReflect().Descriptor(), handled, nil) diff --git a/sdk/go/openshell/v1/internal/converter/profile.go b/sdk/go/openshell/v1/internal/converter/profile.go index b0edaf6445..8bbb346829 100644 --- a/sdk/go/openshell/v1/internal/converter/profile.go +++ b/sdk/go/openshell/v1/internal/converter/profile.go @@ -55,6 +55,30 @@ func ProfileCategoryToProto(c types.ProfileCategory) pb.ProviderProfileCategory } } +// CredentialTokenGrantTypeFromProto converts a proto token grant type to an SDK token grant type. +func CredentialTokenGrantTypeFromProto(t pb.ProviderCredentialTokenGrantType) types.CredentialTokenGrantType { + switch t { + case pb.ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS: + return types.CredentialTokenGrantTypeClientCredentials + case pb.ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE: + return types.CredentialTokenGrantTypeTokenExchange + default: + return types.CredentialTokenGrantType("") + } +} + +// CredentialTokenGrantTypeToProto converts an SDK token grant type to a proto token grant type. +func CredentialTokenGrantTypeToProto(t types.CredentialTokenGrantType) pb.ProviderCredentialTokenGrantType { + switch t { + case types.CredentialTokenGrantTypeClientCredentials: + return pb.ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_CLIENT_CREDENTIALS + case types.CredentialTokenGrantTypeTokenExchange: + return pb.ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE + default: + return pb.ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED + } +} + // --- NetworkEndpoint --- // NetworkEndpointFromProto converts a proto NetworkEndpoint to an SDK NetworkEndpoint. @@ -193,6 +217,9 @@ func tokenGrantFromProto(tg *pb.ProviderCredentialTokenGrant) *types.CredentialT Scopes: CopyStringSlice(tg.GetScopes()), CacheTTLSeconds: tg.GetCacheTtlSeconds(), ClientAssertionType: tg.GetClientAssertionType(), + GrantType: CredentialTokenGrantTypeFromProto(tg.GetGrantType()), + SubjectToken: subjectTokenFromProto(tg.GetSubjectToken()), + RequestedTokenType: tg.GetRequestedTokenType(), } if overrides := tg.GetAudienceOverrides(); len(overrides) > 0 { result.AudienceOverrides = make([]types.TokenGrantAudienceOverride, len(overrides)) @@ -214,6 +241,9 @@ func tokenGrantToProto(tg *types.CredentialTokenGrant) *pb.ProviderCredentialTok Scopes: CopyStringSlice(tg.Scopes), CacheTtlSeconds: tg.CacheTTLSeconds, ClientAssertionType: tg.ClientAssertionType, + GrantType: CredentialTokenGrantTypeToProto(tg.GrantType), + SubjectToken: subjectTokenToProto(tg.SubjectToken), + RequestedTokenType: tg.RequestedTokenType, } if len(tg.AudienceOverrides) > 0 { result.AudienceOverrides = make([]*pb.ProviderCredentialTokenGrantAudienceOverride, len(tg.AudienceOverrides)) @@ -224,6 +254,28 @@ func tokenGrantToProto(tg *types.CredentialTokenGrant) *pb.ProviderCredentialTok return result } +func subjectTokenFromProto(st *pb.ProviderCredentialTokenGrantSubjectToken) *types.TokenGrantSubjectToken { + if st == nil { + return nil + } + return &types.TokenGrantSubjectToken{ + Source: st.GetSource(), + Credential: st.GetCredential(), + SubjectTokenType: st.GetSubjectTokenType(), + } +} + +func subjectTokenToProto(st *types.TokenGrantSubjectToken) *pb.ProviderCredentialTokenGrantSubjectToken { + if st == nil { + return nil + } + return &pb.ProviderCredentialTokenGrantSubjectToken{ + Source: st.Source, + Credential: st.Credential, + SubjectTokenType: st.SubjectTokenType, + } +} + func audienceOverrideFromProto(o *pb.ProviderCredentialTokenGrantAudienceOverride) types.TokenGrantAudienceOverride { if o == nil { return types.TokenGrantAudienceOverride{} diff --git a/sdk/go/openshell/v1/internal/converter/profile_test.go b/sdk/go/openshell/v1/internal/converter/profile_test.go index 8efe8d294b..5d52c0a9ee 100644 --- a/sdk/go/openshell/v1/internal/converter/profile_test.go +++ b/sdk/go/openshell/v1/internal/converter/profile_test.go @@ -156,6 +156,13 @@ func TestProfileCredentialFromProto(t *testing.T) { Scopes: []string{"read", "write"}, CacheTtlSeconds: 300, ClientAssertionType: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + GrantType: pb.ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE, + SubjectToken: &pb.ProviderCredentialTokenGrantSubjectToken{ + Source: "provider_credential", + Credential: "UPSTREAM_TOKEN", + SubjectTokenType: "urn:ietf:params:oauth:token-type:access_token", + }, + RequestedTokenType: "urn:ietf:params:oauth:token-type:refresh_token", AudienceOverrides: []*pb.ProviderCredentialTokenGrantAudienceOverride{ {Host: "special.example.com", Port: 8443, Path: "/api", Audience: "https://special.example.com", Scopes: []string{"admin"}}, }, @@ -182,6 +189,12 @@ func TestProfileCredentialFromProto(t *testing.T) { assert.Equal(t, []string{"read", "write"}, cred.TokenGrant.Scopes) assert.Equal(t, int64(300), cred.TokenGrant.CacheTTLSeconds) assert.Equal(t, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", cred.TokenGrant.ClientAssertionType) + assert.Equal(t, v1.CredentialTokenGrantTypeTokenExchange, cred.TokenGrant.GrantType) + require.NotNil(t, cred.TokenGrant.SubjectToken) + assert.Equal(t, "provider_credential", cred.TokenGrant.SubjectToken.Source) + assert.Equal(t, "UPSTREAM_TOKEN", cred.TokenGrant.SubjectToken.Credential) + assert.Equal(t, "urn:ietf:params:oauth:token-type:access_token", cred.TokenGrant.SubjectToken.SubjectTokenType) + assert.Equal(t, "urn:ietf:params:oauth:token-type:refresh_token", cred.TokenGrant.RequestedTokenType) require.Len(t, cred.TokenGrant.AudienceOverrides, 1) assert.Equal(t, "special.example.com", cred.TokenGrant.AudienceOverrides[0].Host) assert.Equal(t, uint32(8443), cred.TokenGrant.AudienceOverrides[0].Port) @@ -261,6 +274,13 @@ func TestProfileCredentialToProto(t *testing.T) { Scopes: []string{"read"}, CacheTTLSeconds: 300, ClientAssertionType: "urn:custom", + GrantType: v1.CredentialTokenGrantTypeTokenExchange, + SubjectToken: &v1.TokenGrantSubjectToken{ + Source: "provider_credential", + Credential: "UPSTREAM_TOKEN", + SubjectTokenType: "urn:ietf:params:oauth:token-type:access_token", + }, + RequestedTokenType: "urn:ietf:params:oauth:token-type:refresh_token", AudienceOverrides: []v1.TokenGrantAudienceOverride{ {Host: "h", Port: 443, Path: "/p", Audience: "aud", Scopes: []string{"s"}}, }, @@ -292,6 +312,12 @@ func TestProfileCredentialToProto(t *testing.T) { assert.Equal(t, []string{"read"}, proto.TokenGrant.Scopes) assert.Equal(t, int64(300), proto.TokenGrant.CacheTtlSeconds) assert.Equal(t, "urn:custom", proto.TokenGrant.ClientAssertionType) + assert.Equal(t, pb.ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_TOKEN_EXCHANGE, proto.TokenGrant.GrantType) + require.NotNil(t, proto.TokenGrant.SubjectToken) + assert.Equal(t, "provider_credential", proto.TokenGrant.SubjectToken.Source) + assert.Equal(t, "UPSTREAM_TOKEN", proto.TokenGrant.SubjectToken.Credential) + assert.Equal(t, "urn:ietf:params:oauth:token-type:access_token", proto.TokenGrant.SubjectToken.SubjectTokenType) + assert.Equal(t, "urn:ietf:params:oauth:token-type:refresh_token", proto.TokenGrant.RequestedTokenType) require.Len(t, proto.TokenGrant.AudienceOverrides, 1) assert.Equal(t, "h", proto.TokenGrant.AudienceOverrides[0].Host) } diff --git a/sdk/go/openshell/v1/types/profile.go b/sdk/go/openshell/v1/types/profile.go index 2ca7da18ff..e519df634d 100644 --- a/sdk/go/openshell/v1/types/profile.go +++ b/sdk/go/openshell/v1/types/profile.go @@ -75,6 +75,15 @@ type ProfileCredentialRefreshOutput struct { Credential string } +// CredentialTokenGrantType describes which OAuth2 grant type to use for dynamic credentials. +type CredentialTokenGrantType string + +// CredentialTokenGrantType values. +const ( + CredentialTokenGrantTypeClientCredentials CredentialTokenGrantType = "ClientCredentials" + CredentialTokenGrantTypeTokenExchange CredentialTokenGrantType = "TokenExchange" +) + // CredentialTokenGrant configures dynamic credential acquisition via OAuth2 grant. type CredentialTokenGrant struct { TokenEndpoint string @@ -84,6 +93,16 @@ type CredentialTokenGrant struct { CacheTTLSeconds int64 AudienceOverrides []TokenGrantAudienceOverride ClientAssertionType string + GrantType CredentialTokenGrantType + SubjectToken *TokenGrantSubjectToken + RequestedTokenType string +} + +// TokenGrantSubjectToken configures the subject token for token exchange grants. +type TokenGrantSubjectToken struct { + Source string + Credential string + SubjectTokenType string } // TokenGrantAudienceOverride selects an endpoint-specific resource audience. diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index ecff3a1e17..6d71b56d7e 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -99,6 +99,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 ( @@ -144,11 +195,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 { @@ -157,7 +208,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. @@ -209,11 +260,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 { @@ -222,7 +273,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. @@ -269,11 +320,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 { @@ -282,7 +333,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. @@ -322,11 +373,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 { @@ -335,7 +386,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. @@ -372,11 +423,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 { @@ -385,7 +436,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 @@ -1097,6 +1148,134 @@ func (x *Sandbox) GetStatus() *SandboxStatus { return nil } +type SandboxDelegatedIdentity struct { + state protoimpl.MessageState `protogen:"open.v1"` + CredentialId string `protobuf:"bytes,1,opt,name=credential_id,json=credentialId,proto3" json:"credential_id,omitempty"` + PrincipalSubject string `protobuf:"bytes,2,opt,name=principal_subject,json=principalSubject,proto3" json:"principal_subject,omitempty"` + DelegatedUntilMs int64 `protobuf:"varint,3,opt,name=delegated_until_ms,json=delegatedUntilMs,proto3" json:"delegated_until_ms,omitempty"` + WithdrawnAtMs int64 `protobuf:"varint,4,opt,name=withdrawn_at_ms,json=withdrawnAtMs,proto3" json:"withdrawn_at_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxDelegatedIdentity) Reset() { + *x = SandboxDelegatedIdentity{} + mi := &file_openshell_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxDelegatedIdentity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxDelegatedIdentity) ProtoMessage() {} + +func (x *SandboxDelegatedIdentity) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[13] + 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 SandboxDelegatedIdentity.ProtoReflect.Descriptor instead. +func (*SandboxDelegatedIdentity) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{13} +} + +func (x *SandboxDelegatedIdentity) GetCredentialId() string { + if x != nil { + return x.CredentialId + } + return "" +} + +func (x *SandboxDelegatedIdentity) GetPrincipalSubject() string { + if x != nil { + return x.PrincipalSubject + } + return "" +} + +func (x *SandboxDelegatedIdentity) GetDelegatedUntilMs() int64 { + if x != nil { + return x.DelegatedUntilMs + } + return 0 +} + +func (x *SandboxDelegatedIdentity) GetWithdrawnAtMs() int64 { + if x != nil { + return x.WithdrawnAtMs + } + return 0 +} + +type SandboxDelegatedIdentityRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + DelegatedIdentity *SandboxDelegatedIdentity `protobuf:"bytes,3,opt,name=delegated_identity,json=delegatedIdentity,proto3" json:"delegated_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxDelegatedIdentityRecord) Reset() { + *x = SandboxDelegatedIdentityRecord{} + mi := &file_openshell_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxDelegatedIdentityRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxDelegatedIdentityRecord) ProtoMessage() {} + +func (x *SandboxDelegatedIdentityRecord) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[14] + 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 SandboxDelegatedIdentityRecord.ProtoReflect.Descriptor instead. +func (*SandboxDelegatedIdentityRecord) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{14} +} + +func (x *SandboxDelegatedIdentityRecord) GetMetadata() *datamodelv1.ObjectMeta { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *SandboxDelegatedIdentityRecord) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *SandboxDelegatedIdentityRecord) GetDelegatedIdentity() *SandboxDelegatedIdentity { + if x != nil { + return x.DelegatedIdentity + } + return nil +} + // Desired sandbox configuration provided through the public API. type SandboxSpec struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1119,7 +1298,7 @@ type SandboxSpec struct { func (x *SandboxSpec) Reset() { *x = SandboxSpec{} - mi := &file_openshell_proto_msgTypes[13] + mi := &file_openshell_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1131,7 +1310,7 @@ func (x *SandboxSpec) String() string { func (*SandboxSpec) ProtoMessage() {} func (x *SandboxSpec) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[13] + mi := &file_openshell_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1144,7 +1323,7 @@ func (x *SandboxSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxSpec.ProtoReflect.Descriptor instead. func (*SandboxSpec) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{13} + return file_openshell_proto_rawDescGZIP(), []int{15} } func (x *SandboxSpec) GetLogLevel() string { @@ -1199,7 +1378,7 @@ type ResourceRequirements struct { func (x *ResourceRequirements) Reset() { *x = ResourceRequirements{} - mi := &file_openshell_proto_msgTypes[14] + mi := &file_openshell_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1211,7 +1390,7 @@ func (x *ResourceRequirements) String() string { func (*ResourceRequirements) ProtoMessage() {} func (x *ResourceRequirements) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[14] + mi := &file_openshell_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1224,7 +1403,7 @@ func (x *ResourceRequirements) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceRequirements.ProtoReflect.Descriptor instead. func (*ResourceRequirements) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{14} + return file_openshell_proto_rawDescGZIP(), []int{16} } func (x *ResourceRequirements) GetGpu() *GpuResourceRequirements { @@ -1246,7 +1425,7 @@ type GpuResourceRequirements struct { func (x *GpuResourceRequirements) Reset() { *x = GpuResourceRequirements{} - mi := &file_openshell_proto_msgTypes[15] + mi := &file_openshell_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1258,7 +1437,7 @@ func (x *GpuResourceRequirements) String() string { func (*GpuResourceRequirements) ProtoMessage() {} func (x *GpuResourceRequirements) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[15] + mi := &file_openshell_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1271,7 +1450,7 @@ func (x *GpuResourceRequirements) ProtoReflect() protoreflect.Message { // Deprecated: Use GpuResourceRequirements.ProtoReflect.Descriptor instead. func (*GpuResourceRequirements) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{15} + return file_openshell_proto_rawDescGZIP(), []int{17} } func (x *GpuResourceRequirements) GetCount() uint32 { @@ -1315,7 +1494,7 @@ type SandboxTemplate struct { func (x *SandboxTemplate) Reset() { *x = SandboxTemplate{} - mi := &file_openshell_proto_msgTypes[16] + mi := &file_openshell_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1327,7 +1506,7 @@ func (x *SandboxTemplate) String() string { func (*SandboxTemplate) ProtoMessage() {} func (x *SandboxTemplate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[16] + mi := &file_openshell_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1340,7 +1519,7 @@ func (x *SandboxTemplate) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxTemplate.ProtoReflect.Descriptor instead. func (*SandboxTemplate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{16} + return file_openshell_proto_rawDescGZIP(), []int{18} } func (x *SandboxTemplate) GetImage() string { @@ -1431,7 +1610,7 @@ type SandboxStatus struct { func (x *SandboxStatus) Reset() { *x = SandboxStatus{} - mi := &file_openshell_proto_msgTypes[17] + mi := &file_openshell_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1443,7 +1622,7 @@ func (x *SandboxStatus) String() string { func (*SandboxStatus) ProtoMessage() {} func (x *SandboxStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[17] + mi := &file_openshell_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1456,7 +1635,7 @@ func (x *SandboxStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxStatus.ProtoReflect.Descriptor instead. func (*SandboxStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{17} + return file_openshell_proto_rawDescGZIP(), []int{19} } func (x *SandboxStatus) GetSandboxName() string { @@ -1527,7 +1706,7 @@ type SandboxCondition struct { func (x *SandboxCondition) Reset() { *x = SandboxCondition{} - mi := &file_openshell_proto_msgTypes[18] + mi := &file_openshell_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1539,7 +1718,7 @@ func (x *SandboxCondition) String() string { func (*SandboxCondition) ProtoMessage() {} func (x *SandboxCondition) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[18] + mi := &file_openshell_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1552,7 +1731,7 @@ func (x *SandboxCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxCondition.ProtoReflect.Descriptor instead. func (*SandboxCondition) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{18} + return file_openshell_proto_rawDescGZIP(), []int{20} } func (x *SandboxCondition) GetType() string { @@ -1611,7 +1790,7 @@ type PlatformEvent struct { func (x *PlatformEvent) Reset() { *x = PlatformEvent{} - mi := &file_openshell_proto_msgTypes[19] + mi := &file_openshell_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1623,7 +1802,7 @@ func (x *PlatformEvent) String() string { func (*PlatformEvent) ProtoMessage() {} func (x *PlatformEvent) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[19] + mi := &file_openshell_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1636,7 +1815,7 @@ func (x *PlatformEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use PlatformEvent.ProtoReflect.Descriptor instead. func (*PlatformEvent) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{19} + return file_openshell_proto_rawDescGZIP(), []int{21} } func (x *PlatformEvent) GetTimestampMs() int64 { @@ -1692,14 +1871,18 @@ type CreateSandboxRequest struct { // Optional annotations for the sandbox (non-selector metadata). Annotations map[string]string `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Workspace for the sandbox. Empty defaults to "default". - Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` + // Optional gateway-owned delegated identity material. The server persists + // this as a gateway-scoped credential and stores only delegation metadata on + // the sandbox. + DelegatedIdentity *DelegatedIdentityRequest `protobuf:"bytes,6,opt,name=delegated_identity,json=delegatedIdentity,proto3" json:"delegated_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateSandboxRequest) Reset() { *x = CreateSandboxRequest{} - mi := &file_openshell_proto_msgTypes[20] + mi := &file_openshell_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1711,7 +1894,7 @@ func (x *CreateSandboxRequest) String() string { func (*CreateSandboxRequest) ProtoMessage() {} func (x *CreateSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[20] + mi := &file_openshell_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1724,7 +1907,7 @@ func (x *CreateSandboxRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateSandboxRequest.ProtoReflect.Descriptor instead. func (*CreateSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{20} + return file_openshell_proto_rawDescGZIP(), []int{22} } func (x *CreateSandboxRequest) GetSpec() *SandboxSpec { @@ -1762,32 +1945,41 @@ func (x *CreateSandboxRequest) GetWorkspace() string { return "" } -// Get sandbox request. -type GetSandboxRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *CreateSandboxRequest) GetDelegatedIdentity() *DelegatedIdentityRequest { + if x != nil { + return x.DelegatedIdentity + } + return nil } -func (x *GetSandboxRequest) Reset() { - *x = GetSandboxRequest{} - mi := &file_openshell_proto_msgTypes[21] +type DelegatedIdentityRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + DelegatedUntilMs int64 `protobuf:"varint,1,opt,name=delegated_until_ms,json=delegatedUntilMs,proto3" json:"delegated_until_ms,omitempty"` + Issuer string `protobuf:"bytes,2,opt,name=issuer,proto3" json:"issuer,omitempty"` + ClientId string `protobuf:"bytes,3,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + RefreshToken string `protobuf:"bytes,4,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"` + AccessToken string `protobuf:"bytes,5,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + Scopes string `protobuf:"bytes,7,opt,name=scopes,proto3" json:"scopes,omitempty"` + Audience string `protobuf:"bytes,8,opt,name=audience,proto3" json:"audience,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DelegatedIdentityRequest) Reset() { + *x = DelegatedIdentityRequest{} + mi := &file_openshell_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *GetSandboxRequest) String() string { +func (x *DelegatedIdentityRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetSandboxRequest) ProtoMessage() {} +func (*DelegatedIdentityRequest) ProtoMessage() {} -func (x *GetSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[21] +func (x *DelegatedIdentityRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1798,192 +1990,82 @@ func (x *GetSandboxRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GetSandboxRequest.ProtoReflect.Descriptor instead. -func (*GetSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{21} +// Deprecated: Use DelegatedIdentityRequest.ProtoReflect.Descriptor instead. +func (*DelegatedIdentityRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{23} } -func (x *GetSandboxRequest) GetName() string { +func (x *DelegatedIdentityRequest) GetDelegatedUntilMs() int64 { if x != nil { - return x.Name + return x.DelegatedUntilMs + } + return 0 +} + +func (x *DelegatedIdentityRequest) GetIssuer() string { + if x != nil { + return x.Issuer } return "" } -func (x *GetSandboxRequest) GetWorkspace() string { +func (x *DelegatedIdentityRequest) GetClientId() string { if x != nil { - return x.Workspace + return x.ClientId } return "" } -// List sandboxes request. -type ListSandboxesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` - Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` - // Optional label selector for filtering (format: "key1=value1,key2=value2"). - LabelSelector string `protobuf:"bytes,3,opt,name=label_selector,json=labelSelector,proto3" json:"label_selector,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` - // List across all workspaces. Mutually exclusive with workspace. - AllWorkspaces bool `protobuf:"varint,5,opt,name=all_workspaces,json=allWorkspaces,proto3" json:"all_workspaces,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *DelegatedIdentityRequest) GetRefreshToken() string { + if x != nil { + return x.RefreshToken + } + return "" } -func (x *ListSandboxesRequest) Reset() { - *x = ListSandboxesRequest{} - mi := &file_openshell_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSandboxesRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSandboxesRequest) ProtoMessage() {} - -func (x *ListSandboxesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[22] - 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 ListSandboxesRequest.ProtoReflect.Descriptor instead. -func (*ListSandboxesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{22} -} - -func (x *ListSandboxesRequest) GetLimit() uint32 { - if x != nil { - return x.Limit - } - return 0 -} - -func (x *ListSandboxesRequest) GetOffset() uint32 { - if x != nil { - return x.Offset - } - return 0 -} - -func (x *ListSandboxesRequest) GetLabelSelector() string { - if x != nil { - return x.LabelSelector - } - return "" -} - -func (x *ListSandboxesRequest) GetWorkspace() string { +func (x *DelegatedIdentityRequest) GetAccessToken() string { if x != nil { - return x.Workspace + return x.AccessToken } return "" } -func (x *ListSandboxesRequest) GetAllWorkspaces() bool { - if x != nil { - return x.AllWorkspaces - } - return false -} - -// List providers attached to a sandbox request. -type ListSandboxProvidersRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ListSandboxProvidersRequest) Reset() { - *x = ListSandboxProvidersRequest{} - mi := &file_openshell_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ListSandboxProvidersRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ListSandboxProvidersRequest) ProtoMessage() {} - -func (x *ListSandboxProvidersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[23] - 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 ListSandboxProvidersRequest.ProtoReflect.Descriptor instead. -func (*ListSandboxProvidersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{23} -} - -func (x *ListSandboxProvidersRequest) GetSandboxName() string { +func (x *DelegatedIdentityRequest) GetScopes() string { if x != nil { - return x.SandboxName + return x.Scopes } return "" } -func (x *ListSandboxProvidersRequest) GetWorkspace() string { +func (x *DelegatedIdentityRequest) GetAudience() string { if x != nil { - return x.Workspace + return x.Audience } return "" } -// Attach provider to sandbox request. -type AttachSandboxProviderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` - // Provider name to attach. - ProviderName string `protobuf:"bytes,2,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` - // Expected resource version for optimistic concurrency control. - // If 0, the server uses the current version (backward compatibility). - // If non-zero, the server validates that the sandbox's current resource_version - // matches this value before applying the mutation, returning ABORTED on mismatch. - ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` +type GetSandboxDelegatedIdentityStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *AttachSandboxProviderRequest) Reset() { - *x = AttachSandboxProviderRequest{} +func (x *GetSandboxDelegatedIdentityStatusRequest) Reset() { + *x = GetSandboxDelegatedIdentityStatusRequest{} mi := &file_openshell_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AttachSandboxProviderRequest) String() string { +func (x *GetSandboxDelegatedIdentityStatusRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AttachSandboxProviderRequest) ProtoMessage() {} +func (*GetSandboxDelegatedIdentityStatusRequest) ProtoMessage() {} -func (x *AttachSandboxProviderRequest) ProtoReflect() protoreflect.Message { +func (x *GetSandboxDelegatedIdentityStatusRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1995,71 +2077,49 @@ func (x *AttachSandboxProviderRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AttachSandboxProviderRequest.ProtoReflect.Descriptor instead. -func (*AttachSandboxProviderRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use GetSandboxDelegatedIdentityStatusRequest.ProtoReflect.Descriptor instead. +func (*GetSandboxDelegatedIdentityStatusRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{24} } -func (x *AttachSandboxProviderRequest) GetSandboxName() string { - if x != nil { - return x.SandboxName - } - return "" -} - -func (x *AttachSandboxProviderRequest) GetProviderName() string { +func (x *GetSandboxDelegatedIdentityStatusRequest) GetName() string { if x != nil { - return x.ProviderName + return x.Name } return "" } -func (x *AttachSandboxProviderRequest) GetExpectedResourceVersion() uint64 { - if x != nil { - return x.ExpectedResourceVersion - } - return 0 -} - -func (x *AttachSandboxProviderRequest) GetWorkspace() string { +func (x *GetSandboxDelegatedIdentityStatusRequest) GetWorkspace() string { if x != nil { return x.Workspace } return "" } -// Detach provider from sandbox request. -type DetachSandboxProviderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` - // Provider name to detach. - ProviderName string `protobuf:"bytes,2,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` - // Expected resource version for optimistic concurrency control. - // If 0, the server uses the current version (backward compatibility). - // If non-zero, the server validates that the sandbox's current resource_version - // matches this value before applying the mutation, returning ABORTED on mismatch. - ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type GetSandboxDelegatedIdentityStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + DelegatedIdentity *SandboxDelegatedIdentity `protobuf:"bytes,1,opt,name=delegated_identity,json=delegatedIdentity,proto3" json:"delegated_identity,omitempty"` + NowMs int64 `protobuf:"varint,2,opt,name=now_ms,json=nowMs,proto3" json:"now_ms,omitempty"` + CredentialRevokedAtMs int64 `protobuf:"varint,3,opt,name=credential_revoked_at_ms,json=credentialRevokedAtMs,proto3" json:"credential_revoked_at_ms,omitempty"` + CredentialMissing bool `protobuf:"varint,4,opt,name=credential_missing,json=credentialMissing,proto3" json:"credential_missing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *DetachSandboxProviderRequest) Reset() { - *x = DetachSandboxProviderRequest{} +func (x *GetSandboxDelegatedIdentityStatusResponse) Reset() { + *x = GetSandboxDelegatedIdentityStatusResponse{} mi := &file_openshell_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DetachSandboxProviderRequest) String() string { +func (x *GetSandboxDelegatedIdentityStatusResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DetachSandboxProviderRequest) ProtoMessage() {} +func (*GetSandboxDelegatedIdentityStatusResponse) ProtoMessage() {} -func (x *DetachSandboxProviderRequest) ProtoReflect() protoreflect.Message { +func (x *GetSandboxDelegatedIdentityStatusResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2071,64 +2131,61 @@ func (x *DetachSandboxProviderRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DetachSandboxProviderRequest.ProtoReflect.Descriptor instead. -func (*DetachSandboxProviderRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use GetSandboxDelegatedIdentityStatusResponse.ProtoReflect.Descriptor instead. +func (*GetSandboxDelegatedIdentityStatusResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{25} } -func (x *DetachSandboxProviderRequest) GetSandboxName() string { +func (x *GetSandboxDelegatedIdentityStatusResponse) GetDelegatedIdentity() *SandboxDelegatedIdentity { if x != nil { - return x.SandboxName + return x.DelegatedIdentity } - return "" + return nil } -func (x *DetachSandboxProviderRequest) GetProviderName() string { +func (x *GetSandboxDelegatedIdentityStatusResponse) GetNowMs() int64 { if x != nil { - return x.ProviderName + return x.NowMs } - return "" + return 0 } -func (x *DetachSandboxProviderRequest) GetExpectedResourceVersion() uint64 { +func (x *GetSandboxDelegatedIdentityStatusResponse) GetCredentialRevokedAtMs() int64 { if x != nil { - return x.ExpectedResourceVersion + return x.CredentialRevokedAtMs } return 0 } -func (x *DetachSandboxProviderRequest) GetWorkspace() string { +func (x *GetSandboxDelegatedIdentityStatusResponse) GetCredentialMissing() bool { if x != nil { - return x.Workspace + return x.CredentialMissing } - return "" + return false } -// Delete sandbox request. -type DeleteSandboxRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` +type WithdrawSandboxDelegatedIdentityRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *DeleteSandboxRequest) Reset() { - *x = DeleteSandboxRequest{} +func (x *WithdrawSandboxDelegatedIdentityRequest) Reset() { + *x = WithdrawSandboxDelegatedIdentityRequest{} mi := &file_openshell_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteSandboxRequest) String() string { +func (x *WithdrawSandboxDelegatedIdentityRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteSandboxRequest) ProtoMessage() {} +func (*WithdrawSandboxDelegatedIdentityRequest) ProtoMessage() {} -func (x *DeleteSandboxRequest) ProtoReflect() protoreflect.Message { +func (x *WithdrawSandboxDelegatedIdentityRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2140,50 +2197,47 @@ func (x *DeleteSandboxRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeleteSandboxRequest.ProtoReflect.Descriptor instead. -func (*DeleteSandboxRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use WithdrawSandboxDelegatedIdentityRequest.ProtoReflect.Descriptor instead. +func (*WithdrawSandboxDelegatedIdentityRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{26} } -func (x *DeleteSandboxRequest) GetName() string { +func (x *WithdrawSandboxDelegatedIdentityRequest) GetName() string { if x != nil { return x.Name } return "" } -func (x *DeleteSandboxRequest) GetWorkspace() string { +func (x *WithdrawSandboxDelegatedIdentityRequest) GetWorkspace() string { if x != nil { return x.Workspace } return "" } -// Stop sandbox request. -type StopSandboxRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` +type WithdrawSandboxDelegatedIdentityResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + Withdrawn bool `protobuf:"varint,2,opt,name=withdrawn,proto3" json:"withdrawn,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *StopSandboxRequest) Reset() { - *x = StopSandboxRequest{} +func (x *WithdrawSandboxDelegatedIdentityResponse) Reset() { + *x = WithdrawSandboxDelegatedIdentityResponse{} mi := &file_openshell_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *StopSandboxRequest) String() string { +func (x *WithdrawSandboxDelegatedIdentityResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*StopSandboxRequest) ProtoMessage() {} +func (*WithdrawSandboxDelegatedIdentityResponse) ProtoMessage() {} -func (x *StopSandboxRequest) ProtoReflect() protoreflect.Message { +func (x *WithdrawSandboxDelegatedIdentityResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2195,50 +2249,48 @@ func (x *StopSandboxRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use StopSandboxRequest.ProtoReflect.Descriptor instead. -func (*StopSandboxRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use WithdrawSandboxDelegatedIdentityResponse.ProtoReflect.Descriptor instead. +func (*WithdrawSandboxDelegatedIdentityResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{27} } -func (x *StopSandboxRequest) GetName() string { +func (x *WithdrawSandboxDelegatedIdentityResponse) GetSandbox() *Sandbox { if x != nil { - return x.Name + return x.Sandbox } - return "" + return nil } -func (x *StopSandboxRequest) GetWorkspace() string { +func (x *WithdrawSandboxDelegatedIdentityResponse) GetWithdrawn() bool { if x != nil { - return x.Workspace + return x.Withdrawn } - return "" + return false } -// Start sandbox request. -type StartSandboxRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name (canonical lookup key). - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type ExtendSandboxDelegatedIdentityRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` + DelegatedIdentity *DelegatedIdentityRequest `protobuf:"bytes,3,opt,name=delegated_identity,json=delegatedIdentity,proto3" json:"delegated_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *StartSandboxRequest) Reset() { - *x = StartSandboxRequest{} +func (x *ExtendSandboxDelegatedIdentityRequest) Reset() { + *x = ExtendSandboxDelegatedIdentityRequest{} mi := &file_openshell_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *StartSandboxRequest) String() string { +func (x *ExtendSandboxDelegatedIdentityRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*StartSandboxRequest) ProtoMessage() {} +func (*ExtendSandboxDelegatedIdentityRequest) ProtoMessage() {} -func (x *StartSandboxRequest) ProtoReflect() protoreflect.Message { +func (x *ExtendSandboxDelegatedIdentityRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2250,47 +2302,53 @@ func (x *StartSandboxRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use StartSandboxRequest.ProtoReflect.Descriptor instead. -func (*StartSandboxRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use ExtendSandboxDelegatedIdentityRequest.ProtoReflect.Descriptor instead. +func (*ExtendSandboxDelegatedIdentityRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{28} } -func (x *StartSandboxRequest) GetName() string { +func (x *ExtendSandboxDelegatedIdentityRequest) GetName() string { if x != nil { return x.Name } return "" } -func (x *StartSandboxRequest) GetWorkspace() string { +func (x *ExtendSandboxDelegatedIdentityRequest) GetWorkspace() string { if x != nil { return x.Workspace } return "" } -// Sandbox response. -type SandboxResponse struct { +func (x *ExtendSandboxDelegatedIdentityRequest) GetDelegatedIdentity() *DelegatedIdentityRequest { + if x != nil { + return x.DelegatedIdentity + } + return nil +} + +type ExtendSandboxDelegatedIdentityResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SandboxResponse) Reset() { - *x = SandboxResponse{} +func (x *ExtendSandboxDelegatedIdentityResponse) Reset() { + *x = ExtendSandboxDelegatedIdentityResponse{} mi := &file_openshell_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SandboxResponse) String() string { +func (x *ExtendSandboxDelegatedIdentityResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SandboxResponse) ProtoMessage() {} +func (*ExtendSandboxDelegatedIdentityResponse) ProtoMessage() {} -func (x *SandboxResponse) ProtoReflect() protoreflect.Message { +func (x *ExtendSandboxDelegatedIdentityResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2302,40 +2360,43 @@ func (x *SandboxResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SandboxResponse.ProtoReflect.Descriptor instead. -func (*SandboxResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use ExtendSandboxDelegatedIdentityResponse.ProtoReflect.Descriptor instead. +func (*ExtendSandboxDelegatedIdentityResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{29} } -func (x *SandboxResponse) GetSandbox() *Sandbox { +func (x *ExtendSandboxDelegatedIdentityResponse) GetSandbox() *Sandbox { if x != nil { return x.Sandbox } return nil } -// List sandboxes response. -type ListSandboxesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Sandboxes []*Sandbox `protobuf:"bytes,1,rep,name=sandboxes,proto3" json:"sandboxes,omitempty"` +// Get sandbox request. +type GetSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox name (canonical lookup key). + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ListSandboxesResponse) Reset() { - *x = ListSandboxesResponse{} +func (x *GetSandboxRequest) Reset() { + *x = GetSandboxRequest{} mi := &file_openshell_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListSandboxesResponse) String() string { +func (x *GetSandboxRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListSandboxesResponse) ProtoMessage() {} +func (*GetSandboxRequest) ProtoMessage() {} -func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message { +func (x *GetSandboxRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2347,40 +2408,54 @@ func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListSandboxesResponse.ProtoReflect.Descriptor instead. -func (*ListSandboxesResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use GetSandboxRequest.ProtoReflect.Descriptor instead. +func (*GetSandboxRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{30} } -func (x *ListSandboxesResponse) GetSandboxes() []*Sandbox { +func (x *GetSandboxRequest) GetName() string { if x != nil { - return x.Sandboxes + return x.Name } - return nil + return "" } -// List providers attached to a sandbox response. -type ListSandboxProvidersResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Providers []*datamodelv1.Provider `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` +func (x *GetSandboxRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// List sandboxes request. +type ListSandboxesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + // Optional label selector for filtering (format: "key1=value1,key2=value2"). + LabelSelector string `protobuf:"bytes,3,opt,name=label_selector,json=labelSelector,proto3" json:"label_selector,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` + // List across all workspaces. Mutually exclusive with workspace. + AllWorkspaces bool `protobuf:"varint,5,opt,name=all_workspaces,json=allWorkspaces,proto3" json:"all_workspaces,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ListSandboxProvidersResponse) Reset() { - *x = ListSandboxProvidersResponse{} +func (x *ListSandboxesRequest) Reset() { + *x = ListSandboxesRequest{} mi := &file_openshell_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListSandboxProvidersResponse) String() string { +func (x *ListSandboxesRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListSandboxProvidersResponse) ProtoMessage() {} +func (*ListSandboxesRequest) ProtoMessage() {} -func (x *ListSandboxProvidersResponse) ProtoReflect() protoreflect.Message { +func (x *ListSandboxesRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2392,42 +2467,71 @@ func (x *ListSandboxProvidersResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListSandboxProvidersResponse.ProtoReflect.Descriptor instead. -func (*ListSandboxProvidersResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use ListSandboxesRequest.ProtoReflect.Descriptor instead. +func (*ListSandboxesRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{31} } -func (x *ListSandboxProvidersResponse) GetProviders() []*datamodelv1.Provider { +func (x *ListSandboxesRequest) GetLimit() uint32 { if x != nil { - return x.Providers + return x.Limit } - return nil + return 0 } -// Attach provider to sandbox response. -type AttachSandboxProviderResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` - // True when the provider was newly attached. False means it was already attached. - Attached bool `protobuf:"varint,2,opt,name=attached,proto3" json:"attached,omitempty"` +func (x *ListSandboxesRequest) GetOffset() uint32 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *ListSandboxesRequest) GetLabelSelector() string { + if x != nil { + return x.LabelSelector + } + return "" +} + +func (x *ListSandboxesRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *ListSandboxesRequest) GetAllWorkspaces() bool { + if x != nil { + return x.AllWorkspaces + } + return false +} + +// List providers attached to a sandbox request. +type ListSandboxProvidersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox name (canonical lookup key). + SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *AttachSandboxProviderResponse) Reset() { - *x = AttachSandboxProviderResponse{} +func (x *ListSandboxProvidersRequest) Reset() { + *x = ListSandboxProvidersRequest{} mi := &file_openshell_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *AttachSandboxProviderResponse) String() string { +func (x *ListSandboxProvidersRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*AttachSandboxProviderResponse) ProtoMessage() {} +func (*ListSandboxProvidersRequest) ProtoMessage() {} -func (x *AttachSandboxProviderResponse) ProtoReflect() protoreflect.Message { +func (x *ListSandboxProvidersRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2439,49 +2543,57 @@ func (x *AttachSandboxProviderResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use AttachSandboxProviderResponse.ProtoReflect.Descriptor instead. -func (*AttachSandboxProviderResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use ListSandboxProvidersRequest.ProtoReflect.Descriptor instead. +func (*ListSandboxProvidersRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{32} } -func (x *AttachSandboxProviderResponse) GetSandbox() *Sandbox { +func (x *ListSandboxProvidersRequest) GetSandboxName() string { if x != nil { - return x.Sandbox + return x.SandboxName } - return nil + return "" } -func (x *AttachSandboxProviderResponse) GetAttached() bool { +func (x *ListSandboxProvidersRequest) GetWorkspace() string { if x != nil { - return x.Attached + return x.Workspace } - return false + return "" } -// Detach provider from sandbox response. -type DetachSandboxProviderResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` - // True when the provider was removed. False means it was not attached. - Detached bool `protobuf:"varint,2,opt,name=detached,proto3" json:"detached,omitempty"` +// Attach provider to sandbox request. +type AttachSandboxProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox name (canonical lookup key). + SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + // Provider name to attach. + ProviderName string `protobuf:"bytes,2,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + // Expected resource version for optimistic concurrency control. + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *DetachSandboxProviderResponse) Reset() { - *x = DetachSandboxProviderResponse{} +func (x *AttachSandboxProviderRequest) Reset() { + *x = AttachSandboxProviderRequest{} mi := &file_openshell_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DetachSandboxProviderResponse) String() string { +func (x *AttachSandboxProviderRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DetachSandboxProviderResponse) ProtoMessage() {} +func (*AttachSandboxProviderRequest) ProtoMessage() {} -func (x *DetachSandboxProviderResponse) ProtoReflect() protoreflect.Message { +func (x *AttachSandboxProviderRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2493,47 +2605,71 @@ func (x *DetachSandboxProviderResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DetachSandboxProviderResponse.ProtoReflect.Descriptor instead. -func (*DetachSandboxProviderResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use AttachSandboxProviderRequest.ProtoReflect.Descriptor instead. +func (*AttachSandboxProviderRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{33} } -func (x *DetachSandboxProviderResponse) GetSandbox() *Sandbox { +func (x *AttachSandboxProviderRequest) GetSandboxName() string { if x != nil { - return x.Sandbox + return x.SandboxName } - return nil + return "" } -func (x *DetachSandboxProviderResponse) GetDetached() bool { +func (x *AttachSandboxProviderRequest) GetProviderName() string { if x != nil { - return x.Detached + return x.ProviderName } - return false + return "" } -// Delete sandbox response. -type DeleteSandboxResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` +func (x *AttachSandboxProviderRequest) GetExpectedResourceVersion() uint64 { + if x != nil { + return x.ExpectedResourceVersion + } + return 0 +} + +func (x *AttachSandboxProviderRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// Detach provider from sandbox request. +type DetachSandboxProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox name (canonical lookup key). + SandboxName string `protobuf:"bytes,1,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + // Provider name to detach. + ProviderName string `protobuf:"bytes,2,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + // Expected resource version for optimistic concurrency control. + // If 0, the server uses the current version (backward compatibility). + // If non-zero, the server validates that the sandbox's current resource_version + // matches this value before applying the mutation, returning ABORTED on mismatch. + ExpectedResourceVersion uint64 `protobuf:"varint,3,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *DeleteSandboxResponse) Reset() { - *x = DeleteSandboxResponse{} +func (x *DetachSandboxProviderRequest) Reset() { + *x = DetachSandboxProviderRequest{} mi := &file_openshell_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteSandboxResponse) String() string { +func (x *DetachSandboxProviderRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteSandboxResponse) ProtoMessage() {} +func (*DetachSandboxProviderRequest) ProtoMessage() {} -func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { +func (x *DetachSandboxProviderRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2545,41 +2681,64 @@ func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeleteSandboxResponse.ProtoReflect.Descriptor instead. -func (*DeleteSandboxResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use DetachSandboxProviderRequest.ProtoReflect.Descriptor instead. +func (*DetachSandboxProviderRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{34} } -func (x *DeleteSandboxResponse) GetDeleted() bool { +func (x *DetachSandboxProviderRequest) GetSandboxName() string { if x != nil { - return x.Deleted + return x.SandboxName } - return false + return "" } -// Create SSH session request. -type CreateSshSessionRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *DetachSandboxProviderRequest) GetProviderName() string { + if x != nil { + return x.ProviderName + } + return "" } -func (x *CreateSshSessionRequest) Reset() { - *x = CreateSshSessionRequest{} +func (x *DetachSandboxProviderRequest) GetExpectedResourceVersion() uint64 { + if x != nil { + return x.ExpectedResourceVersion + } + return 0 +} + +func (x *DetachSandboxProviderRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// Delete sandbox request. +type DeleteSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox name (canonical lookup key). + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSandboxRequest) Reset() { + *x = DeleteSandboxRequest{} mi := &file_openshell_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CreateSshSessionRequest) String() string { +func (x *DeleteSandboxRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CreateSshSessionRequest) ProtoMessage() {} +func (*DeleteSandboxRequest) ProtoMessage() {} -func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { +func (x *DeleteSandboxRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2591,63 +2750,50 @@ func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CreateSshSessionRequest.ProtoReflect.Descriptor instead. -func (*CreateSshSessionRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use DeleteSandboxRequest.ProtoReflect.Descriptor instead. +func (*DeleteSandboxRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{35} } -func (x *CreateSshSessionRequest) GetSandboxId() string { +func (x *DeleteSandboxRequest) GetName() string { if x != nil { - return x.SandboxId + return x.Name } return "" } -// Create SSH session response. -// -// Fields are interpolated into an SSH `ProxyCommand` string that OpenSSH -// executes through `/bin/sh -c` on the caller's workstation. Servers MUST -// uphold the charset contract below; clients MUST reject responses that -// violate it. The client's own escaping provides defense-in-depth, but -// narrow charsets close injection vectors at the trust boundary. -type CreateSshSessionResponse struct { +func (x *DeleteSandboxRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// Stop sandbox request. +type StopSandboxRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. [A-Za-z0-9._-]{1,128}. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - // Session token for the gateway tunnel. URL-safe ASCII - // ([A-Za-z0-9._~+/=-]) up to 4096 bytes. No shell metacharacters or - // whitespace. - Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` - // Gateway host for SSH proxy connection. IPv4 address, bracketed IPv6 - // address, or DNS hostname (Punycode-encoded for IDN). Alphanumeric plus - // `.-:[]` only, up to 253 bytes. - GatewayHost string `protobuf:"bytes,3,opt,name=gateway_host,json=gatewayHost,proto3" json:"gateway_host,omitempty"` - // Gateway port for SSH proxy connection. Must be in range 1..=65535. - GatewayPort uint32 `protobuf:"varint,4,opt,name=gateway_port,json=gatewayPort,proto3" json:"gateway_port,omitempty"` - // Gateway scheme. Must be exactly "http" or "https". - GatewayScheme string `protobuf:"bytes,5,opt,name=gateway_scheme,json=gatewayScheme,proto3" json:"gateway_scheme,omitempty"` - // Optional host key fingerprint. If non-empty, [A-Za-z0-9:+/=-] only. - HostKeyFingerprint string `protobuf:"bytes,7,opt,name=host_key_fingerprint,json=hostKeyFingerprint,proto3" json:"host_key_fingerprint,omitempty"` - // Expiry timestamp in milliseconds since epoch. 0 means no expiry. - ExpiresAtMs int64 `protobuf:"varint,8,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + // Sandbox name (canonical lookup key). + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *CreateSshSessionResponse) Reset() { - *x = CreateSshSessionResponse{} +func (x *StopSandboxRequest) Reset() { + *x = StopSandboxRequest{} mi := &file_openshell_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CreateSshSessionResponse) String() string { +func (x *StopSandboxRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CreateSshSessionResponse) ProtoMessage() {} +func (*StopSandboxRequest) ProtoMessage() {} -func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { +func (x *StopSandboxRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2659,91 +2805,50 @@ func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CreateSshSessionResponse.ProtoReflect.Descriptor instead. -func (*CreateSshSessionResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use StopSandboxRequest.ProtoReflect.Descriptor instead. +func (*StopSandboxRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{36} } -func (x *CreateSshSessionResponse) GetSandboxId() string { - if x != nil { - return x.SandboxId - } - return "" -} - -func (x *CreateSshSessionResponse) GetToken() string { - if x != nil { - return x.Token - } - return "" -} - -func (x *CreateSshSessionResponse) GetGatewayHost() string { - if x != nil { - return x.GatewayHost - } - return "" -} - -func (x *CreateSshSessionResponse) GetGatewayPort() uint32 { - if x != nil { - return x.GatewayPort - } - return 0 -} - -func (x *CreateSshSessionResponse) GetGatewayScheme() string { +func (x *StopSandboxRequest) GetName() string { if x != nil { - return x.GatewayScheme + return x.Name } return "" } -func (x *CreateSshSessionResponse) GetHostKeyFingerprint() string { +func (x *StopSandboxRequest) GetWorkspace() string { if x != nil { - return x.HostKeyFingerprint + return x.Workspace } return "" } -func (x *CreateSshSessionResponse) GetExpiresAtMs() int64 { - if x != nil { - return x.ExpiresAtMs - } - return 0 -} - -// Request to expose an HTTP service running inside a sandbox. -type ExposeServiceRequest struct { +// Start sandbox request. +type StartSandboxRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` - // Service name within the sandbox. - Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` - // Loopback TCP port inside the sandbox. - TargetPort uint32 `protobuf:"varint,3,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` - // Whether to print/use the browser-facing service URL. - Domain bool `protobuf:"varint,4,opt,name=domain,proto3" json:"domain,omitempty"` + // Sandbox name (canonical lookup key). + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExposeServiceRequest) Reset() { - *x = ExposeServiceRequest{} +func (x *StartSandboxRequest) Reset() { + *x = StartSandboxRequest{} mi := &file_openshell_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExposeServiceRequest) String() string { +func (x *StartSandboxRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExposeServiceRequest) ProtoMessage() {} +func (*StartSandboxRequest) ProtoMessage() {} -func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { +func (x *StartSandboxRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2755,73 +2860,47 @@ func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. -func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use StartSandboxRequest.ProtoReflect.Descriptor instead. +func (*StartSandboxRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{37} } -func (x *ExposeServiceRequest) GetSandbox() string { - if x != nil { - return x.Sandbox - } - return "" -} - -func (x *ExposeServiceRequest) GetService() string { +func (x *StartSandboxRequest) GetName() string { if x != nil { - return x.Service + return x.Name } return "" } -func (x *ExposeServiceRequest) GetTargetPort() uint32 { - if x != nil { - return x.TargetPort - } - return 0 -} - -func (x *ExposeServiceRequest) GetDomain() bool { - if x != nil { - return x.Domain - } - return false -} - -func (x *ExposeServiceRequest) GetWorkspace() string { +func (x *StartSandboxRequest) GetWorkspace() string { if x != nil { return x.Workspace } return "" } -// Request to fetch an exposed sandbox service endpoint. -type GetServiceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` - // Service name within the sandbox. Empty selects the unnamed endpoint. - Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` +// Sandbox response. +type SandboxResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *GetServiceRequest) Reset() { - *x = GetServiceRequest{} +func (x *SandboxResponse) Reset() { + *x = SandboxResponse{} mi := &file_openshell_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *GetServiceRequest) String() string { +func (x *SandboxResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetServiceRequest) ProtoMessage() {} +func (*SandboxResponse) ProtoMessage() {} -func (x *GetServiceRequest) ProtoReflect() protoreflect.Message { +func (x *SandboxResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2833,63 +2912,40 @@ func (x *GetServiceRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GetServiceRequest.ProtoReflect.Descriptor instead. -func (*GetServiceRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use SandboxResponse.ProtoReflect.Descriptor instead. +func (*SandboxResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{38} } -func (x *GetServiceRequest) GetSandbox() string { +func (x *SandboxResponse) GetSandbox() *Sandbox { if x != nil { return x.Sandbox } - return "" -} - -func (x *GetServiceRequest) GetService() string { - if x != nil { - return x.Service - } - return "" -} - -func (x *GetServiceRequest) GetWorkspace() string { - if x != nil { - return x.Workspace - } - return "" + return nil } -// Request to list exposed sandbox service endpoints. -type ListServicesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Optional sandbox name. Empty lists endpoints for all sandboxes. - Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` - // Page size. Zero uses the server default. - Limit uint32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` - // Page offset. - Offset uint32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` - // List across all workspaces. Mutually exclusive with workspace. - AllWorkspaces bool `protobuf:"varint,5,opt,name=all_workspaces,json=allWorkspaces,proto3" json:"all_workspaces,omitempty"` +// List sandboxes response. +type ListSandboxesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandboxes []*Sandbox `protobuf:"bytes,1,rep,name=sandboxes,proto3" json:"sandboxes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ListServicesRequest) Reset() { - *x = ListServicesRequest{} +func (x *ListSandboxesResponse) Reset() { + *x = ListSandboxesResponse{} mi := &file_openshell_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListServicesRequest) String() string { +func (x *ListSandboxesResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListServicesRequest) ProtoMessage() {} +func (*ListSandboxesResponse) ProtoMessage() {} -func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { +func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2901,68 +2957,40 @@ func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListServicesRequest.ProtoReflect.Descriptor instead. -func (*ListServicesRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use ListSandboxesResponse.ProtoReflect.Descriptor instead. +func (*ListSandboxesResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{39} } -func (x *ListServicesRequest) GetSandbox() string { - if x != nil { - return x.Sandbox - } - return "" -} - -func (x *ListServicesRequest) GetLimit() uint32 { - if x != nil { - return x.Limit - } - return 0 -} - -func (x *ListServicesRequest) GetOffset() uint32 { - if x != nil { - return x.Offset - } - return 0 -} - -func (x *ListServicesRequest) GetWorkspace() string { - if x != nil { - return x.Workspace - } - return "" -} - -func (x *ListServicesRequest) GetAllWorkspaces() bool { +func (x *ListSandboxesResponse) GetSandboxes() []*Sandbox { if x != nil { - return x.AllWorkspaces + return x.Sandboxes } - return false + return nil } -// Response containing exposed sandbox service endpoints. -type ListServicesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Services []*ServiceEndpointResponse `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` +// List providers attached to a sandbox response. +type ListSandboxProvidersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Providers []*datamodelv1.Provider `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ListServicesResponse) Reset() { - *x = ListServicesResponse{} +func (x *ListSandboxProvidersResponse) Reset() { + *x = ListSandboxProvidersResponse{} mi := &file_openshell_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListServicesResponse) String() string { +func (x *ListSandboxProvidersResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListServicesResponse) ProtoMessage() {} +func (*ListSandboxProvidersResponse) ProtoMessage() {} -func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { +func (x *ListSandboxProvidersResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2974,45 +3002,42 @@ func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListServicesResponse.ProtoReflect.Descriptor instead. -func (*ListServicesResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use ListSandboxProvidersResponse.ProtoReflect.Descriptor instead. +func (*ListSandboxProvidersResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{40} } -func (x *ListServicesResponse) GetServices() []*ServiceEndpointResponse { +func (x *ListSandboxProvidersResponse) GetProviders() []*datamodelv1.Provider { if x != nil { - return x.Services + return x.Providers } return nil } -// Request to delete an exposed sandbox service endpoint. -type DeleteServiceRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox name. - Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` - // Service name within the sandbox. Empty selects the unnamed endpoint. - Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` +// Attach provider to sandbox response. +type AttachSandboxProviderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // True when the provider was newly attached. False means it was already attached. + Attached bool `protobuf:"varint,2,opt,name=attached,proto3" json:"attached,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *DeleteServiceRequest) Reset() { - *x = DeleteServiceRequest{} +func (x *AttachSandboxProviderResponse) Reset() { + *x = AttachSandboxProviderResponse{} mi := &file_openshell_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteServiceRequest) String() string { +func (x *AttachSandboxProviderResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteServiceRequest) ProtoMessage() {} +func (*AttachSandboxProviderResponse) ProtoMessage() {} -func (x *DeleteServiceRequest) ProtoReflect() protoreflect.Message { +func (x *AttachSandboxProviderResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3024,55 +3049,49 @@ func (x *DeleteServiceRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeleteServiceRequest.ProtoReflect.Descriptor instead. -func (*DeleteServiceRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use AttachSandboxProviderResponse.ProtoReflect.Descriptor instead. +func (*AttachSandboxProviderResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{41} } -func (x *DeleteServiceRequest) GetSandbox() string { +func (x *AttachSandboxProviderResponse) GetSandbox() *Sandbox { if x != nil { return x.Sandbox } - return "" -} - -func (x *DeleteServiceRequest) GetService() string { - if x != nil { - return x.Service - } - return "" + return nil } -func (x *DeleteServiceRequest) GetWorkspace() string { +func (x *AttachSandboxProviderResponse) GetAttached() bool { if x != nil { - return x.Workspace + return x.Attached } - return "" + return false } -// Response for deleting an exposed sandbox service endpoint. -type DeleteServiceResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // True when an endpoint existed and was deleted. - Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` +// Detach provider from sandbox response. +type DetachSandboxProviderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // True when the provider was removed. False means it was not attached. + Detached bool `protobuf:"varint,2,opt,name=detached,proto3" json:"detached,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *DeleteServiceResponse) Reset() { - *x = DeleteServiceResponse{} +func (x *DetachSandboxProviderResponse) Reset() { + *x = DetachSandboxProviderResponse{} mi := &file_openshell_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteServiceResponse) String() string { +func (x *DetachSandboxProviderResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteServiceResponse) ProtoMessage() {} +func (*DetachSandboxProviderResponse) ProtoMessage() {} -func (x *DeleteServiceResponse) ProtoReflect() protoreflect.Message { +func (x *DetachSandboxProviderResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3084,51 +3103,47 @@ func (x *DeleteServiceResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeleteServiceResponse.ProtoReflect.Descriptor instead. -func (*DeleteServiceResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use DetachSandboxProviderResponse.ProtoReflect.Descriptor instead. +func (*DetachSandboxProviderResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{42} } -func (x *DeleteServiceResponse) GetDeleted() bool { +func (x *DetachSandboxProviderResponse) GetSandbox() *Sandbox { if x != nil { - return x.Deleted + return x.Sandbox + } + return nil +} + +func (x *DetachSandboxProviderResponse) GetDetached() bool { + if x != nil { + return x.Detached } return false } -// Persisted sandbox service endpoint. -type ServiceEndpoint struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Kubernetes-style metadata. - Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - // Sandbox object ID. - SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - // Sandbox name. - SandboxName string `protobuf:"bytes,3,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` - // Service name within the sandbox. - ServiceName string `protobuf:"bytes,4,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` - // Loopback TCP port inside the sandbox. - TargetPort uint32 `protobuf:"varint,5,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` - // Whether browser-facing service routing is enabled for this endpoint. - Domain bool `protobuf:"varint,6,opt,name=domain,proto3" json:"domain,omitempty"` +// Delete sandbox response. +type DeleteSandboxResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ServiceEndpoint) Reset() { - *x = ServiceEndpoint{} +func (x *DeleteSandboxResponse) Reset() { + *x = DeleteSandboxResponse{} mi := &file_openshell_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ServiceEndpoint) String() string { +func (x *DeleteSandboxResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ServiceEndpoint) ProtoMessage() {} +func (*DeleteSandboxResponse) ProtoMessage() {} -func (x *ServiceEndpoint) ProtoReflect() protoreflect.Message { +func (x *DeleteSandboxResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3140,76 +3155,41 @@ func (x *ServiceEndpoint) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ServiceEndpoint.ProtoReflect.Descriptor instead. -func (*ServiceEndpoint) Descriptor() ([]byte, []int) { +// Deprecated: Use DeleteSandboxResponse.ProtoReflect.Descriptor instead. +func (*DeleteSandboxResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{43} } -func (x *ServiceEndpoint) GetMetadata() *datamodelv1.ObjectMeta { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *ServiceEndpoint) GetSandboxId() string { - if x != nil { - return x.SandboxId - } - return "" -} - -func (x *ServiceEndpoint) GetSandboxName() string { - if x != nil { - return x.SandboxName - } - return "" -} - -func (x *ServiceEndpoint) GetServiceName() string { - if x != nil { - return x.ServiceName - } - return "" -} - -func (x *ServiceEndpoint) GetTargetPort() uint32 { - if x != nil { - return x.TargetPort - } - return 0 -} - -func (x *ServiceEndpoint) GetDomain() bool { +func (x *DeleteSandboxResponse) GetDeleted() bool { if x != nil { - return x.Domain + return x.Deleted } return false } -// Response containing a service endpoint and, when available, its local URL. -type ServiceEndpointResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Endpoint *ServiceEndpoint `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"` - Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` +// Create SSH session request. +type CreateSshSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ServiceEndpointResponse) Reset() { - *x = ServiceEndpointResponse{} +func (x *CreateSshSessionRequest) Reset() { + *x = CreateSshSessionRequest{} mi := &file_openshell_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ServiceEndpointResponse) String() string { +func (x *CreateSshSessionRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ServiceEndpointResponse) ProtoMessage() {} +func (*CreateSshSessionRequest) ProtoMessage() {} -func (x *ServiceEndpointResponse) ProtoReflect() protoreflect.Message { +func (x *CreateSshSessionRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3221,48 +3201,63 @@ func (x *ServiceEndpointResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ServiceEndpointResponse.ProtoReflect.Descriptor instead. -func (*ServiceEndpointResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use CreateSshSessionRequest.ProtoReflect.Descriptor instead. +func (*CreateSshSessionRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{44} } -func (x *ServiceEndpointResponse) GetEndpoint() *ServiceEndpoint { +func (x *CreateSshSessionRequest) GetSandboxId() string { if x != nil { - return x.Endpoint - } - return nil -} - -func (x *ServiceEndpointResponse) GetUrl() string { - if x != nil { - return x.Url + return x.SandboxId } return "" } -// Revoke SSH session request. -type RevokeSshSessionRequest struct { +// Create SSH session response. +// +// Fields are interpolated into an SSH `ProxyCommand` string that OpenSSH +// executes through `/bin/sh -c` on the caller's workstation. Servers MUST +// uphold the charset contract below; clients MUST reject responses that +// violate it. The client's own escaping provides defense-in-depth, but +// narrow charsets close injection vectors at the trust boundary. +type CreateSshSessionResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - // Session token to revoke. - Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + // Sandbox id. [A-Za-z0-9._-]{1,128}. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Session token for the gateway tunnel. URL-safe ASCII + // ([A-Za-z0-9._~+/=-]) up to 4096 bytes. No shell metacharacters or + // whitespace. + Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` + // Gateway host for SSH proxy connection. IPv4 address, bracketed IPv6 + // address, or DNS hostname (Punycode-encoded for IDN). Alphanumeric plus + // `.-:[]` only, up to 253 bytes. + GatewayHost string `protobuf:"bytes,3,opt,name=gateway_host,json=gatewayHost,proto3" json:"gateway_host,omitempty"` + // Gateway port for SSH proxy connection. Must be in range 1..=65535. + GatewayPort uint32 `protobuf:"varint,4,opt,name=gateway_port,json=gatewayPort,proto3" json:"gateway_port,omitempty"` + // Gateway scheme. Must be exactly "http" or "https". + GatewayScheme string `protobuf:"bytes,5,opt,name=gateway_scheme,json=gatewayScheme,proto3" json:"gateway_scheme,omitempty"` + // Optional host key fingerprint. If non-empty, [A-Za-z0-9:+/=-] only. + HostKeyFingerprint string `protobuf:"bytes,7,opt,name=host_key_fingerprint,json=hostKeyFingerprint,proto3" json:"host_key_fingerprint,omitempty"` + // Expiry timestamp in milliseconds since epoch. 0 means no expiry. + ExpiresAtMs int64 `protobuf:"varint,8,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *RevokeSshSessionRequest) Reset() { - *x = RevokeSshSessionRequest{} +func (x *CreateSshSessionResponse) Reset() { + *x = CreateSshSessionResponse{} mi := &file_openshell_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *RevokeSshSessionRequest) String() string { +func (x *CreateSshSessionResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*RevokeSshSessionRequest) ProtoMessage() {} +func (*CreateSshSessionResponse) ProtoMessage() {} -func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { +func (x *CreateSshSessionResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3274,104 +3269,92 @@ func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use RevokeSshSessionRequest.ProtoReflect.Descriptor instead. -func (*RevokeSshSessionRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use CreateSshSessionResponse.ProtoReflect.Descriptor instead. +func (*CreateSshSessionResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{45} } -func (x *RevokeSshSessionRequest) GetToken() string { +func (x *CreateSshSessionResponse) GetSandboxId() string { if x != nil { - return x.Token + return x.SandboxId } return "" } -// Revoke SSH session response. -type RevokeSshSessionResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // True when a session was revoked. - Revoked bool `protobuf:"varint,1,opt,name=revoked,proto3" json:"revoked,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *CreateSshSessionResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" } -func (x *RevokeSshSessionResponse) Reset() { - *x = RevokeSshSessionResponse{} - mi := &file_openshell_proto_msgTypes[46] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *CreateSshSessionResponse) GetGatewayHost() string { + if x != nil { + return x.GatewayHost + } + return "" } -func (x *RevokeSshSessionResponse) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *CreateSshSessionResponse) GetGatewayPort() uint32 { + if x != nil { + return x.GatewayPort + } + return 0 } -func (*RevokeSshSessionResponse) ProtoMessage() {} - -func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[46] +func (x *CreateSshSessionResponse) GetGatewayScheme() string { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.GatewayScheme } - return mi.MessageOf(x) + return "" } -// Deprecated: Use RevokeSshSessionResponse.ProtoReflect.Descriptor instead. -func (*RevokeSshSessionResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{46} +func (x *CreateSshSessionResponse) GetHostKeyFingerprint() string { + if x != nil { + return x.HostKeyFingerprint + } + return "" } -func (x *RevokeSshSessionResponse) GetRevoked() bool { +func (x *CreateSshSessionResponse) GetExpiresAtMs() int64 { if x != nil { - return x.Revoked + return x.ExpiresAtMs } - return false + return 0 } -// Execute command request. -type ExecSandboxRequest struct { +// Request to expose an HTTP service running inside a sandbox. +type ExposeServiceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - // Command and arguments. - Command []string `protobuf:"bytes,2,rep,name=command,proto3" json:"command,omitempty"` - // Optional working directory. - Workdir string `protobuf:"bytes,3,opt,name=workdir,proto3" json:"workdir,omitempty"` - // Optional environment overrides. - Environment map[string]string `protobuf:"bytes,4,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Optional timeout in seconds. 0 means no timeout. - TimeoutSeconds uint32 `protobuf:"varint,5,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` - // Optional stdin payload passed to the command. - Stdin []byte `protobuf:"bytes,6,opt,name=stdin,proto3" json:"stdin,omitempty"` - // Request a pseudo-terminal for the remote command. - Tty bool `protobuf:"varint,7,opt,name=tty,proto3" json:"tty,omitempty"` - // Initial terminal columns (used when tty=true, 0 = use default). - Cols uint32 `protobuf:"varint,8,opt,name=cols,proto3" json:"cols,omitempty"` - // Initial terminal rows (used when tty=true, 0 = use default). - Rows uint32 `protobuf:"varint,9,opt,name=rows,proto3" json:"rows,omitempty"` + // Sandbox name. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Service name within the sandbox. + Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` + // Loopback TCP port inside the sandbox. + TargetPort uint32 `protobuf:"varint,3,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` + // Whether to print/use the browser-facing service URL. + Domain bool `protobuf:"varint,4,opt,name=domain,proto3" json:"domain,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,5,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExecSandboxRequest) Reset() { - *x = ExecSandboxRequest{} - mi := &file_openshell_proto_msgTypes[47] +func (x *ExposeServiceRequest) Reset() { + *x = ExposeServiceRequest{} + mi := &file_openshell_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExecSandboxRequest) String() string { +func (x *ExposeServiceRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExecSandboxRequest) ProtoMessage() {} +func (*ExposeServiceRequest) ProtoMessage() {} -func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[47] +func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3382,97 +3365,74 @@ func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExecSandboxRequest.ProtoReflect.Descriptor instead. -func (*ExecSandboxRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{47} +// Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. +func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{46} } -func (x *ExecSandboxRequest) GetSandboxId() string { +func (x *ExposeServiceRequest) GetSandbox() string { if x != nil { - return x.SandboxId + return x.Sandbox } return "" } -func (x *ExecSandboxRequest) GetCommand() []string { - if x != nil { - return x.Command - } - return nil -} - -func (x *ExecSandboxRequest) GetWorkdir() string { +func (x *ExposeServiceRequest) GetService() string { if x != nil { - return x.Workdir + return x.Service } return "" } -func (x *ExecSandboxRequest) GetEnvironment() map[string]string { - if x != nil { - return x.Environment - } - return nil -} - -func (x *ExecSandboxRequest) GetTimeoutSeconds() uint32 { +func (x *ExposeServiceRequest) GetTargetPort() uint32 { if x != nil { - return x.TimeoutSeconds + return x.TargetPort } return 0 } -func (x *ExecSandboxRequest) GetStdin() []byte { - if x != nil { - return x.Stdin - } - return nil -} - -func (x *ExecSandboxRequest) GetTty() bool { +func (x *ExposeServiceRequest) GetDomain() bool { if x != nil { - return x.Tty + return x.Domain } return false } -func (x *ExecSandboxRequest) GetCols() uint32 { - if x != nil { - return x.Cols - } - return 0 -} - -func (x *ExecSandboxRequest) GetRows() uint32 { +func (x *ExposeServiceRequest) GetWorkspace() string { if x != nil { - return x.Rows + return x.Workspace } - return 0 + return "" } -// One stdout chunk from a sandbox exec. -type ExecSandboxStdout struct { - state protoimpl.MessageState `protogen:"open.v1"` - Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` +// Request to fetch an exposed sandbox service endpoint. +type GetServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox name. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Service name within the sandbox. Empty selects the unnamed endpoint. + Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExecSandboxStdout) Reset() { - *x = ExecSandboxStdout{} - mi := &file_openshell_proto_msgTypes[48] +func (x *GetServiceRequest) Reset() { + *x = GetServiceRequest{} + mi := &file_openshell_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExecSandboxStdout) String() string { +func (x *GetServiceRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExecSandboxStdout) ProtoMessage() {} +func (*GetServiceRequest) ProtoMessage() {} -func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[48] +func (x *GetServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3483,41 +3443,64 @@ func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExecSandboxStdout.ProtoReflect.Descriptor instead. -func (*ExecSandboxStdout) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{48} +// Deprecated: Use GetServiceRequest.ProtoReflect.Descriptor instead. +func (*GetServiceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{47} } -func (x *ExecSandboxStdout) GetData() []byte { +func (x *GetServiceRequest) GetSandbox() string { if x != nil { - return x.Data + return x.Sandbox } - return nil + return "" } -// One stderr chunk from a sandbox exec. -type ExecSandboxStderr struct { - state protoimpl.MessageState `protogen:"open.v1"` - Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` +func (x *GetServiceRequest) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *GetServiceRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// Request to list exposed sandbox service endpoints. +type ListServicesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional sandbox name. Empty lists endpoints for all sandboxes. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Page size. Zero uses the server default. + Limit uint32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + // Page offset. + Offset uint32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,4,opt,name=workspace,proto3" json:"workspace,omitempty"` + // List across all workspaces. Mutually exclusive with workspace. + AllWorkspaces bool `protobuf:"varint,5,opt,name=all_workspaces,json=allWorkspaces,proto3" json:"all_workspaces,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExecSandboxStderr) Reset() { - *x = ExecSandboxStderr{} - mi := &file_openshell_proto_msgTypes[49] +func (x *ListServicesRequest) Reset() { + *x = ListServicesRequest{} + mi := &file_openshell_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExecSandboxStderr) String() string { +func (x *ListServicesRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExecSandboxStderr) ProtoMessage() {} +func (*ListServicesRequest) ProtoMessage() {} -func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[49] +func (x *ListServicesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3528,41 +3511,69 @@ func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExecSandboxStderr.ProtoReflect.Descriptor instead. -func (*ExecSandboxStderr) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{49} +// Deprecated: Use ListServicesRequest.ProtoReflect.Descriptor instead. +func (*ListServicesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{48} } -func (x *ExecSandboxStderr) GetData() []byte { +func (x *ListServicesRequest) GetSandbox() string { if x != nil { - return x.Data + return x.Sandbox } - return nil + return "" } -// Final exit status for a sandbox exec. -type ExecSandboxExit struct { - state protoimpl.MessageState `protogen:"open.v1"` - ExitCode int32 `protobuf:"varint,1,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` +func (x *ListServicesRequest) GetLimit() uint32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ListServicesRequest) GetOffset() uint32 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *ListServicesRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *ListServicesRequest) GetAllWorkspaces() bool { + if x != nil { + return x.AllWorkspaces + } + return false +} + +// Response containing exposed sandbox service endpoints. +type ListServicesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Services []*ServiceEndpointResponse `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExecSandboxExit) Reset() { - *x = ExecSandboxExit{} - mi := &file_openshell_proto_msgTypes[50] +func (x *ListServicesResponse) Reset() { + *x = ListServicesResponse{} + mi := &file_openshell_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExecSandboxExit) String() string { +func (x *ListServicesResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExecSandboxExit) ProtoMessage() {} +func (*ListServicesResponse) ProtoMessage() {} -func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[50] +func (x *ListServicesResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3573,46 +3584,46 @@ func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExecSandboxExit.ProtoReflect.Descriptor instead. -func (*ExecSandboxExit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{50} +// Deprecated: Use ListServicesResponse.ProtoReflect.Descriptor instead. +func (*ListServicesResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{49} } -func (x *ExecSandboxExit) GetExitCode() int32 { +func (x *ListServicesResponse) GetServices() []*ServiceEndpointResponse { if x != nil { - return x.ExitCode + return x.Services } - return 0 + return nil } -// One event in a sandbox exec stream. -type ExecSandboxEvent struct { +// Request to delete an exposed sandbox service endpoint. +type DeleteServiceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *ExecSandboxEvent_Stdout - // *ExecSandboxEvent_Stderr - // *ExecSandboxEvent_Exit - Payload isExecSandboxEvent_Payload `protobuf_oneof:"payload"` + // Sandbox name. + Sandbox string `protobuf:"bytes,1,opt,name=sandbox,proto3" json:"sandbox,omitempty"` + // Service name within the sandbox. Empty selects the unnamed endpoint. + Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExecSandboxEvent) Reset() { - *x = ExecSandboxEvent{} - mi := &file_openshell_proto_msgTypes[51] +func (x *DeleteServiceRequest) Reset() { + *x = DeleteServiceRequest{} + mi := &file_openshell_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExecSandboxEvent) String() string { +func (x *DeleteServiceRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExecSandboxEvent) ProtoMessage() {} +func (*DeleteServiceRequest) ProtoMessage() {} -func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[51] +func (x *DeleteServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3623,103 +3634,56 @@ func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExecSandboxEvent.ProtoReflect.Descriptor instead. -func (*ExecSandboxEvent) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{51} -} - -func (x *ExecSandboxEvent) GetPayload() isExecSandboxEvent_Payload { - if x != nil { - return x.Payload - } - return nil +// Deprecated: Use DeleteServiceRequest.ProtoReflect.Descriptor instead. +func (*DeleteServiceRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{50} } -func (x *ExecSandboxEvent) GetStdout() *ExecSandboxStdout { +func (x *DeleteServiceRequest) GetSandbox() string { if x != nil { - if x, ok := x.Payload.(*ExecSandboxEvent_Stdout); ok { - return x.Stdout - } + return x.Sandbox } - return nil + return "" } -func (x *ExecSandboxEvent) GetStderr() *ExecSandboxStderr { +func (x *DeleteServiceRequest) GetService() string { if x != nil { - if x, ok := x.Payload.(*ExecSandboxEvent_Stderr); ok { - return x.Stderr - } + return x.Service } - return nil + return "" } -func (x *ExecSandboxEvent) GetExit() *ExecSandboxExit { +func (x *DeleteServiceRequest) GetWorkspace() string { if x != nil { - if x, ok := x.Payload.(*ExecSandboxEvent_Exit); ok { - return x.Exit - } + return x.Workspace } - return nil -} - -type isExecSandboxEvent_Payload interface { - isExecSandboxEvent_Payload() -} - -type ExecSandboxEvent_Stdout struct { - Stdout *ExecSandboxStdout `protobuf:"bytes,1,opt,name=stdout,proto3,oneof"` -} - -type ExecSandboxEvent_Stderr struct { - Stderr *ExecSandboxStderr `protobuf:"bytes,2,opt,name=stderr,proto3,oneof"` -} - -type ExecSandboxEvent_Exit struct { - Exit *ExecSandboxExit `protobuf:"bytes,3,opt,name=exit,proto3,oneof"` + return "" } -func (*ExecSandboxEvent_Stdout) isExecSandboxEvent_Payload() {} - -func (*ExecSandboxEvent_Stderr) isExecSandboxEvent_Payload() {} - -func (*ExecSandboxEvent_Exit) isExecSandboxEvent_Payload() {} - -// Initial frame for one TCP forward stream. -type TcpForwardInit struct { +// Response for deleting an exposed sandbox service endpoint. +type DeleteServiceResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - // Optional service identifier for audit/correlation. - ServiceId string `protobuf:"bytes,4,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` - // Target the gateway should request from the supervisor. - // - // Types that are valid to be assigned to Target: - // - // *TcpForwardInit_Ssh - // *TcpForwardInit_Tcp - Target isTcpForwardInit_Target `protobuf_oneof:"target"` - // Optional target-specific authorization token. SSH targets use this as the - // short-lived SSH session token issued by CreateSshSession. - AuthorizationToken string `protobuf:"bytes,7,opt,name=authorization_token,json=authorizationToken,proto3" json:"authorization_token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // True when an endpoint existed and was deleted. + Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *TcpForwardInit) Reset() { - *x = TcpForwardInit{} - mi := &file_openshell_proto_msgTypes[52] +func (x *DeleteServiceResponse) Reset() { + *x = DeleteServiceResponse{} + mi := &file_openshell_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *TcpForwardInit) String() string { +func (x *DeleteServiceResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*TcpForwardInit) ProtoMessage() {} +func (*DeleteServiceResponse) ProtoMessage() {} -func (x *TcpForwardInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[52] +func (x *DeleteServiceResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3730,99 +3694,132 @@ func (x *TcpForwardInit) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use TcpForwardInit.ProtoReflect.Descriptor instead. -func (*TcpForwardInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{52} +// Deprecated: Use DeleteServiceResponse.ProtoReflect.Descriptor instead. +func (*DeleteServiceResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{51} } -func (x *TcpForwardInit) GetSandboxId() string { +func (x *DeleteServiceResponse) GetDeleted() bool { if x != nil { - return x.SandboxId + return x.Deleted } - return "" + return false } -func (x *TcpForwardInit) GetServiceId() string { - if x != nil { - return x.ServiceId - } - return "" +// Persisted sandbox service endpoint. +type ServiceEndpoint struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Kubernetes-style metadata. + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Sandbox object ID. + SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Sandbox name. + SandboxName string `protobuf:"bytes,3,opt,name=sandbox_name,json=sandboxName,proto3" json:"sandbox_name,omitempty"` + // Service name within the sandbox. + ServiceName string `protobuf:"bytes,4,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + // Loopback TCP port inside the sandbox. + TargetPort uint32 `protobuf:"varint,5,opt,name=target_port,json=targetPort,proto3" json:"target_port,omitempty"` + // Whether browser-facing service routing is enabled for this endpoint. + Domain bool `protobuf:"varint,6,opt,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *TcpForwardInit) GetTarget() isTcpForwardInit_Target { - if x != nil { - return x.Target - } - return nil +func (x *ServiceEndpoint) Reset() { + *x = ServiceEndpoint{} + mi := &file_openshell_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *TcpForwardInit) GetSsh() *SshRelayTarget { +func (x *ServiceEndpoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceEndpoint) ProtoMessage() {} + +func (x *ServiceEndpoint) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[52] if x != nil { - if x, ok := x.Target.(*TcpForwardInit_Ssh); ok { - return x.Ssh + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms } - return nil + return mi.MessageOf(x) } -func (x *TcpForwardInit) GetTcp() *TcpRelayTarget { +// Deprecated: Use ServiceEndpoint.ProtoReflect.Descriptor instead. +func (*ServiceEndpoint) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{52} +} + +func (x *ServiceEndpoint) GetMetadata() *datamodelv1.ObjectMeta { if x != nil { - if x, ok := x.Target.(*TcpForwardInit_Tcp); ok { - return x.Tcp - } + return x.Metadata } return nil } -func (x *TcpForwardInit) GetAuthorizationToken() string { +func (x *ServiceEndpoint) GetSandboxId() string { if x != nil { - return x.AuthorizationToken + return x.SandboxId } return "" } -type isTcpForwardInit_Target interface { - isTcpForwardInit_Target() +func (x *ServiceEndpoint) GetSandboxName() string { + if x != nil { + return x.SandboxName + } + return "" } -type TcpForwardInit_Ssh struct { - Ssh *SshRelayTarget `protobuf:"bytes,5,opt,name=ssh,proto3,oneof"` +func (x *ServiceEndpoint) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" } -type TcpForwardInit_Tcp struct { - Tcp *TcpRelayTarget `protobuf:"bytes,6,opt,name=tcp,proto3,oneof"` +func (x *ServiceEndpoint) GetTargetPort() uint32 { + if x != nil { + return x.TargetPort + } + return 0 } -func (*TcpForwardInit_Ssh) isTcpForwardInit_Target() {} - -func (*TcpForwardInit_Tcp) isTcpForwardInit_Target() {} +func (x *ServiceEndpoint) GetDomain() bool { + if x != nil { + return x.Domain + } + return false +} -// A single frame on the CLI-to-gateway TCP forward stream. -type TcpForwardFrame struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *TcpForwardFrame_Init - // *TcpForwardFrame_Data - Payload isTcpForwardFrame_Payload `protobuf_oneof:"payload"` +// Response containing a service endpoint and, when available, its local URL. +type ServiceEndpointResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Endpoint *ServiceEndpoint `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"` + Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *TcpForwardFrame) Reset() { - *x = TcpForwardFrame{} +func (x *ServiceEndpointResponse) Reset() { + *x = ServiceEndpointResponse{} mi := &file_openshell_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *TcpForwardFrame) String() string { +func (x *ServiceEndpointResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*TcpForwardFrame) ProtoMessage() {} +func (*ServiceEndpointResponse) ProtoMessage() {} -func (x *TcpForwardFrame) ProtoReflect() protoreflect.Message { +func (x *ServiceEndpointResponse) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3834,79 +3831,48 @@ func (x *TcpForwardFrame) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use TcpForwardFrame.ProtoReflect.Descriptor instead. -func (*TcpForwardFrame) Descriptor() ([]byte, []int) { +// Deprecated: Use ServiceEndpointResponse.ProtoReflect.Descriptor instead. +func (*ServiceEndpointResponse) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{53} } -func (x *TcpForwardFrame) GetPayload() isTcpForwardFrame_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *TcpForwardFrame) GetInit() *TcpForwardInit { +func (x *ServiceEndpointResponse) GetEndpoint() *ServiceEndpoint { if x != nil { - if x, ok := x.Payload.(*TcpForwardFrame_Init); ok { - return x.Init - } + return x.Endpoint } return nil } -func (x *TcpForwardFrame) GetData() []byte { +func (x *ServiceEndpointResponse) GetUrl() string { if x != nil { - if x, ok := x.Payload.(*TcpForwardFrame_Data); ok { - return x.Data - } + return x.Url } - return nil -} - -type isTcpForwardFrame_Payload interface { - isTcpForwardFrame_Payload() -} - -type TcpForwardFrame_Init struct { - Init *TcpForwardInit `protobuf:"bytes,1,opt,name=init,proto3,oneof"` -} - -type TcpForwardFrame_Data struct { - Data []byte `protobuf:"bytes,2,opt,name=data,proto3,oneof"` + return "" } -func (*TcpForwardFrame_Init) isTcpForwardFrame_Payload() {} - -func (*TcpForwardFrame_Data) isTcpForwardFrame_Payload() {} - -// Client-to-server message for interactive exec. -type ExecSandboxInput struct { +// Revoke SSH session request. +type RevokeSshSessionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *ExecSandboxInput_Start - // *ExecSandboxInput_Stdin - // *ExecSandboxInput_Resize - Payload isExecSandboxInput_Payload `protobuf_oneof:"payload"` + // Session token to revoke. + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExecSandboxInput) Reset() { - *x = ExecSandboxInput{} +func (x *RevokeSshSessionRequest) Reset() { + *x = RevokeSshSessionRequest{} mi := &file_openshell_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExecSandboxInput) String() string { +func (x *RevokeSshSessionRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExecSandboxInput) ProtoMessage() {} +func (*RevokeSshSessionRequest) ProtoMessage() {} -func (x *ExecSandboxInput) ProtoReflect() protoreflect.Message { +func (x *RevokeSshSessionRequest) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3918,94 +3884,104 @@ func (x *ExecSandboxInput) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExecSandboxInput.ProtoReflect.Descriptor instead. -func (*ExecSandboxInput) Descriptor() ([]byte, []int) { +// Deprecated: Use RevokeSshSessionRequest.ProtoReflect.Descriptor instead. +func (*RevokeSshSessionRequest) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{54} } -func (x *ExecSandboxInput) GetPayload() isExecSandboxInput_Payload { +func (x *RevokeSshSessionRequest) GetToken() string { if x != nil { - return x.Payload + return x.Token } - return nil + return "" } -func (x *ExecSandboxInput) GetStart() *ExecSandboxRequest { - if x != nil { - if x, ok := x.Payload.(*ExecSandboxInput_Start); ok { - return x.Start - } - } - return nil +// Revoke SSH session response. +type RevokeSshSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // True when a session was revoked. + Revoked bool `protobuf:"varint,1,opt,name=revoked,proto3" json:"revoked,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ExecSandboxInput) GetStdin() []byte { - if x != nil { - if x, ok := x.Payload.(*ExecSandboxInput_Stdin); ok { - return x.Stdin - } - } - return nil +func (x *RevokeSshSessionResponse) Reset() { + *x = RevokeSshSessionResponse{} + mi := &file_openshell_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *ExecSandboxInput) GetResize() *ExecSandboxWindowResize { - if x != nil { - if x, ok := x.Payload.(*ExecSandboxInput_Resize); ok { - return x.Resize - } - } - return nil +func (x *RevokeSshSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -type isExecSandboxInput_Payload interface { - isExecSandboxInput_Payload() -} +func (*RevokeSshSessionResponse) ProtoMessage() {} -type ExecSandboxInput_Start struct { - // First message: exec request metadata. - Start *ExecSandboxRequest `protobuf:"bytes,1,opt,name=start,proto3,oneof"` +func (x *RevokeSshSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -type ExecSandboxInput_Stdin struct { - // Subsequent messages: raw stdin bytes. - Stdin []byte `protobuf:"bytes,2,opt,name=stdin,proto3,oneof"` +// Deprecated: Use RevokeSshSessionResponse.ProtoReflect.Descriptor instead. +func (*RevokeSshSessionResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{55} } -type ExecSandboxInput_Resize struct { - // Terminal window size change. - Resize *ExecSandboxWindowResize `protobuf:"bytes,3,opt,name=resize,proto3,oneof"` +func (x *RevokeSshSessionResponse) GetRevoked() bool { + if x != nil { + return x.Revoked + } + return false } -func (*ExecSandboxInput_Start) isExecSandboxInput_Payload() {} - -func (*ExecSandboxInput_Stdin) isExecSandboxInput_Payload() {} - -func (*ExecSandboxInput_Resize) isExecSandboxInput_Payload() {} - -// Terminal window resize event for interactive exec. -type ExecSandboxWindowResize struct { - state protoimpl.MessageState `protogen:"open.v1"` - Cols uint32 `protobuf:"varint,1,opt,name=cols,proto3" json:"cols,omitempty"` - Rows uint32 `protobuf:"varint,2,opt,name=rows,proto3" json:"rows,omitempty"` +// Execute command request. +type ExecSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Command and arguments. + Command []string `protobuf:"bytes,2,rep,name=command,proto3" json:"command,omitempty"` + // Optional working directory. + Workdir string `protobuf:"bytes,3,opt,name=workdir,proto3" json:"workdir,omitempty"` + // Optional environment overrides. + Environment map[string]string `protobuf:"bytes,4,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Optional timeout in seconds. 0 means no timeout. + TimeoutSeconds uint32 `protobuf:"varint,5,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` + // Optional stdin payload passed to the command. + Stdin []byte `protobuf:"bytes,6,opt,name=stdin,proto3" json:"stdin,omitempty"` + // Request a pseudo-terminal for the remote command. + Tty bool `protobuf:"varint,7,opt,name=tty,proto3" json:"tty,omitempty"` + // Initial terminal columns (used when tty=true, 0 = use default). + Cols uint32 `protobuf:"varint,8,opt,name=cols,proto3" json:"cols,omitempty"` + // Initial terminal rows (used when tty=true, 0 = use default). + Rows uint32 `protobuf:"varint,9,opt,name=rows,proto3" json:"rows,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExecSandboxWindowResize) Reset() { - *x = ExecSandboxWindowResize{} - mi := &file_openshell_proto_msgTypes[55] +func (x *ExecSandboxRequest) Reset() { + *x = ExecSandboxRequest{} + mi := &file_openshell_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExecSandboxWindowResize) String() string { +func (x *ExecSandboxRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExecSandboxWindowResize) ProtoMessage() {} +func (*ExecSandboxRequest) ProtoMessage() {} -func (x *ExecSandboxWindowResize) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[55] +func (x *ExecSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4016,150 +3992,96 @@ func (x *ExecSandboxWindowResize) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExecSandboxWindowResize.ProtoReflect.Descriptor instead. -func (*ExecSandboxWindowResize) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{55} +// Deprecated: Use ExecSandboxRequest.ProtoReflect.Descriptor instead. +func (*ExecSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{56} } -func (x *ExecSandboxWindowResize) GetCols() uint32 { +func (x *ExecSandboxRequest) GetSandboxId() string { if x != nil { - return x.Cols + return x.SandboxId } - return 0 + return "" } -func (x *ExecSandboxWindowResize) GetRows() uint32 { +func (x *ExecSandboxRequest) GetCommand() []string { if x != nil { - return x.Rows + return x.Command } - return 0 -} - -// SSH session record stored in persistence. -type SshSession struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Kubernetes-style metadata (id, name, labels, timestamps, resource version). - Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - // Sandbox id. - SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - // Session token. - Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` - // Expiry timestamp in milliseconds since epoch. 0 means no expiry - // (backward-compatible default for sessions created before this field existed). - ExpiresAtMs int64 `protobuf:"varint,4,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` - // Revoked flag. - Revoked bool `protobuf:"varint,5,opt,name=revoked,proto3" json:"revoked,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SshSession) Reset() { - *x = SshSession{} - mi := &file_openshell_proto_msgTypes[56] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SshSession) String() string { - return protoimpl.X.MessageStringOf(x) + return nil } -func (*SshSession) ProtoMessage() {} - -func (x *SshSession) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[56] +func (x *ExecSandboxRequest) GetWorkdir() string { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Workdir } - return mi.MessageOf(x) + return "" } -// Deprecated: Use SshSession.ProtoReflect.Descriptor instead. -func (*SshSession) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{56} +func (x *ExecSandboxRequest) GetEnvironment() map[string]string { + if x != nil { + return x.Environment + } + return nil } -func (x *SshSession) GetMetadata() *datamodelv1.ObjectMeta { +func (x *ExecSandboxRequest) GetTimeoutSeconds() uint32 { if x != nil { - return x.Metadata + return x.TimeoutSeconds } - return nil + return 0 } -func (x *SshSession) GetSandboxId() string { +func (x *ExecSandboxRequest) GetStdin() []byte { if x != nil { - return x.SandboxId + return x.Stdin } - return "" + return nil } -func (x *SshSession) GetToken() string { +func (x *ExecSandboxRequest) GetTty() bool { if x != nil { - return x.Token + return x.Tty } - return "" + return false } -func (x *SshSession) GetExpiresAtMs() int64 { +func (x *ExecSandboxRequest) GetCols() uint32 { if x != nil { - return x.ExpiresAtMs + return x.Cols } return 0 } -func (x *SshSession) GetRevoked() bool { +func (x *ExecSandboxRequest) GetRows() uint32 { if x != nil { - return x.Revoked + return x.Rows } - return false + return 0 } -// Watch sandbox request. -type WatchSandboxRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Sandbox id. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // Stream sandbox status snapshots. - FollowStatus bool `protobuf:"varint,2,opt,name=follow_status,json=followStatus,proto3" json:"follow_status,omitempty"` - // Stream openshell-server process logs correlated to this sandbox. - FollowLogs bool `protobuf:"varint,3,opt,name=follow_logs,json=followLogs,proto3" json:"follow_logs,omitempty"` - // Stream platform events correlated to this sandbox. - FollowEvents bool `protobuf:"varint,4,opt,name=follow_events,json=followEvents,proto3" json:"follow_events,omitempty"` - // Replay the last N log lines (best-effort) before following. - LogTailLines uint32 `protobuf:"varint,5,opt,name=log_tail_lines,json=logTailLines,proto3" json:"log_tail_lines,omitempty"` - // Replay the last N platform events (best-effort) before following. - EventTail uint32 `protobuf:"varint,6,opt,name=event_tail,json=eventTail,proto3" json:"event_tail,omitempty"` - // Stop streaming once the sandbox reaches a terminal phase (READY or ERROR). - StopOnTerminal bool `protobuf:"varint,7,opt,name=stop_on_terminal,json=stopOnTerminal,proto3" json:"stop_on_terminal,omitempty"` - // Only include log lines with timestamp >= this value (milliseconds since epoch). - // 0 means no time filter. Applies to both tail replay and live streaming. - LogSinceMs int64 `protobuf:"varint,8,opt,name=log_since_ms,json=logSinceMs,proto3" json:"log_since_ms,omitempty"` - // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. - LogSources []string `protobuf:"bytes,9,rep,name=log_sources,json=logSources,proto3" json:"log_sources,omitempty"` - // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. - LogMinLevel string `protobuf:"bytes,10,opt,name=log_min_level,json=logMinLevel,proto3" json:"log_min_level,omitempty"` +// One stdout chunk from a sandbox exec. +type ExecSandboxStdout struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *WatchSandboxRequest) Reset() { - *x = WatchSandboxRequest{} +func (x *ExecSandboxStdout) Reset() { + *x = ExecSandboxStdout{} mi := &file_openshell_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *WatchSandboxRequest) String() string { +func (x *ExecSandboxStdout) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WatchSandboxRequest) ProtoMessage() {} +func (*ExecSandboxStdout) ProtoMessage() {} -func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { +func (x *ExecSandboxStdout) ProtoReflect() protoreflect.Message { mi := &file_openshell_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -4171,111 +4093,136 @@ func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use WatchSandboxRequest.ProtoReflect.Descriptor instead. -func (*WatchSandboxRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use ExecSandboxStdout.ProtoReflect.Descriptor instead. +func (*ExecSandboxStdout) Descriptor() ([]byte, []int) { return file_openshell_proto_rawDescGZIP(), []int{57} } -func (x *WatchSandboxRequest) GetId() string { - if x != nil { - return x.Id - } - return "" -} - -func (x *WatchSandboxRequest) GetFollowStatus() bool { +func (x *ExecSandboxStdout) GetData() []byte { if x != nil { - return x.FollowStatus + return x.Data } - return false + return nil } -func (x *WatchSandboxRequest) GetFollowLogs() bool { - if x != nil { - return x.FollowLogs - } - return false +// One stderr chunk from a sandbox exec. +type ExecSandboxStderr struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *WatchSandboxRequest) GetFollowEvents() bool { - if x != nil { - return x.FollowEvents - } - return false +func (x *ExecSandboxStderr) Reset() { + *x = ExecSandboxStderr{} + mi := &file_openshell_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *WatchSandboxRequest) GetLogTailLines() uint32 { - if x != nil { - return x.LogTailLines - } - return 0 +func (x *ExecSandboxStderr) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *WatchSandboxRequest) GetEventTail() uint32 { - if x != nil { - return x.EventTail - } - return 0 -} +func (*ExecSandboxStderr) ProtoMessage() {} -func (x *WatchSandboxRequest) GetStopOnTerminal() bool { +func (x *ExecSandboxStderr) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[58] if x != nil { - return x.StopOnTerminal + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return false + return mi.MessageOf(x) } -func (x *WatchSandboxRequest) GetLogSinceMs() int64 { - if x != nil { - return x.LogSinceMs - } - return 0 +// Deprecated: Use ExecSandboxStderr.ProtoReflect.Descriptor instead. +func (*ExecSandboxStderr) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{58} } -func (x *WatchSandboxRequest) GetLogSources() []string { +func (x *ExecSandboxStderr) GetData() []byte { if x != nil { - return x.LogSources + return x.Data } return nil } -func (x *WatchSandboxRequest) GetLogMinLevel() string { - if x != nil { - return x.LogMinLevel +// Final exit status for a sandbox exec. +type ExecSandboxExit struct { + state protoimpl.MessageState `protogen:"open.v1"` + ExitCode int32 `protobuf:"varint,1,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxExit) Reset() { + *x = ExecSandboxExit{} + mi := &file_openshell_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxExit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxExit) ProtoMessage() {} + +func (x *ExecSandboxExit) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[59] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return "" + return mi.MessageOf(x) } -// One event in a sandbox watch stream. -type SandboxStreamEvent struct { +// Deprecated: Use ExecSandboxExit.ProtoReflect.Descriptor instead. +func (*ExecSandboxExit) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{59} +} + +func (x *ExecSandboxExit) GetExitCode() int32 { + if x != nil { + return x.ExitCode + } + return 0 +} + +// One event in a sandbox exec stream. +type ExecSandboxEvent struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Payload: // - // *SandboxStreamEvent_Sandbox - // *SandboxStreamEvent_Log - // *SandboxStreamEvent_Event - // *SandboxStreamEvent_Warning - // *SandboxStreamEvent_DraftPolicyUpdate - Payload isSandboxStreamEvent_Payload `protobuf_oneof:"payload"` + // *ExecSandboxEvent_Stdout + // *ExecSandboxEvent_Stderr + // *ExecSandboxEvent_Exit + Payload isExecSandboxEvent_Payload `protobuf_oneof:"payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SandboxStreamEvent) Reset() { - *x = SandboxStreamEvent{} - mi := &file_openshell_proto_msgTypes[58] +func (x *ExecSandboxEvent) Reset() { + *x = ExecSandboxEvent{} + mi := &file_openshell_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SandboxStreamEvent) String() string { +func (x *ExecSandboxEvent) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SandboxStreamEvent) ProtoMessage() {} +func (*ExecSandboxEvent) ProtoMessage() {} -func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[58] +func (x *ExecSandboxEvent) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4286,134 +4233,1435 @@ func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SandboxStreamEvent.ProtoReflect.Descriptor instead. -func (*SandboxStreamEvent) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{58} +// Deprecated: Use ExecSandboxEvent.ProtoReflect.Descriptor instead. +func (*ExecSandboxEvent) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{60} } -func (x *SandboxStreamEvent) GetPayload() isSandboxStreamEvent_Payload { +func (x *ExecSandboxEvent) GetPayload() isExecSandboxEvent_Payload { if x != nil { return x.Payload } return nil } -func (x *SandboxStreamEvent) GetSandbox() *Sandbox { +func (x *ExecSandboxEvent) GetStdout() *ExecSandboxStdout { if x != nil { - if x, ok := x.Payload.(*SandboxStreamEvent_Sandbox); ok { - return x.Sandbox + if x, ok := x.Payload.(*ExecSandboxEvent_Stdout); ok { + return x.Stdout } } return nil } -func (x *SandboxStreamEvent) GetLog() *SandboxLogLine { +func (x *ExecSandboxEvent) GetStderr() *ExecSandboxStderr { if x != nil { - if x, ok := x.Payload.(*SandboxStreamEvent_Log); ok { - return x.Log + if x, ok := x.Payload.(*ExecSandboxEvent_Stderr); ok { + return x.Stderr } } return nil } -func (x *SandboxStreamEvent) GetEvent() *PlatformEvent { +func (x *ExecSandboxEvent) GetExit() *ExecSandboxExit { if x != nil { - if x, ok := x.Payload.(*SandboxStreamEvent_Event); ok { - return x.Event + if x, ok := x.Payload.(*ExecSandboxEvent_Exit); ok { + return x.Exit } } return nil } -func (x *SandboxStreamEvent) GetWarning() *SandboxStreamWarning { +type isExecSandboxEvent_Payload interface { + isExecSandboxEvent_Payload() +} + +type ExecSandboxEvent_Stdout struct { + Stdout *ExecSandboxStdout `protobuf:"bytes,1,opt,name=stdout,proto3,oneof"` +} + +type ExecSandboxEvent_Stderr struct { + Stderr *ExecSandboxStderr `protobuf:"bytes,2,opt,name=stderr,proto3,oneof"` +} + +type ExecSandboxEvent_Exit struct { + Exit *ExecSandboxExit `protobuf:"bytes,3,opt,name=exit,proto3,oneof"` +} + +func (*ExecSandboxEvent_Stdout) isExecSandboxEvent_Payload() {} + +func (*ExecSandboxEvent_Stderr) isExecSandboxEvent_Payload() {} + +func (*ExecSandboxEvent_Exit) isExecSandboxEvent_Payload() {} + +// Initial frame for one TCP forward stream. +type TcpForwardInit struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Optional service identifier for audit/correlation. + ServiceId string `protobuf:"bytes,4,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` + // Target the gateway should request from the supervisor. + // + // Types that are valid to be assigned to Target: + // + // *TcpForwardInit_Ssh + // *TcpForwardInit_Tcp + Target isTcpForwardInit_Target `protobuf_oneof:"target"` + // Optional target-specific authorization token. SSH targets use this as the + // short-lived SSH session token issued by CreateSshSession. + AuthorizationToken string `protobuf:"bytes,7,opt,name=authorization_token,json=authorizationToken,proto3" json:"authorization_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpForwardInit) Reset() { + *x = TcpForwardInit{} + mi := &file_openshell_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpForwardInit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpForwardInit) ProtoMessage() {} + +func (x *TcpForwardInit) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[61] if x != nil { - if x, ok := x.Payload.(*SandboxStreamEvent_Warning); ok { - return x.Warning + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TcpForwardInit.ProtoReflect.Descriptor instead. +func (*TcpForwardInit) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{61} +} + +func (x *TcpForwardInit) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *TcpForwardInit) GetServiceId() string { + if x != nil { + return x.ServiceId + } + return "" +} + +func (x *TcpForwardInit) GetTarget() isTcpForwardInit_Target { + if x != nil { + return x.Target } return nil } -func (x *SandboxStreamEvent) GetDraftPolicyUpdate() *DraftPolicyUpdate { +func (x *TcpForwardInit) GetSsh() *SshRelayTarget { if x != nil { - if x, ok := x.Payload.(*SandboxStreamEvent_DraftPolicyUpdate); ok { - return x.DraftPolicyUpdate + if x, ok := x.Target.(*TcpForwardInit_Ssh); ok { + return x.Ssh } } return nil } -type isSandboxStreamEvent_Payload interface { - isSandboxStreamEvent_Payload() +func (x *TcpForwardInit) GetTcp() *TcpRelayTarget { + if x != nil { + if x, ok := x.Target.(*TcpForwardInit_Tcp); ok { + return x.Tcp + } + } + return nil } -type SandboxStreamEvent_Sandbox struct { - // Latest sandbox snapshot. - Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3,oneof"` +func (x *TcpForwardInit) GetAuthorizationToken() string { + if x != nil { + return x.AuthorizationToken + } + return "" } -type SandboxStreamEvent_Log struct { - // One server log line/event. - Log *SandboxLogLine `protobuf:"bytes,2,opt,name=log,proto3,oneof"` +type isTcpForwardInit_Target interface { + isTcpForwardInit_Target() } -type SandboxStreamEvent_Event struct { - // One platform event. - Event *PlatformEvent `protobuf:"bytes,3,opt,name=event,proto3,oneof"` +type TcpForwardInit_Ssh struct { + Ssh *SshRelayTarget `protobuf:"bytes,5,opt,name=ssh,proto3,oneof"` } -type SandboxStreamEvent_Warning struct { - // Warning from the server (e.g. missed messages due to lag). - Warning *SandboxStreamWarning `protobuf:"bytes,4,opt,name=warning,proto3,oneof"` +type TcpForwardInit_Tcp struct { + Tcp *TcpRelayTarget `protobuf:"bytes,6,opt,name=tcp,proto3,oneof"` } -type SandboxStreamEvent_DraftPolicyUpdate struct { - // Draft policy update notification. - DraftPolicyUpdate *DraftPolicyUpdate `protobuf:"bytes,5,opt,name=draft_policy_update,json=draftPolicyUpdate,proto3,oneof"` -} +func (*TcpForwardInit_Ssh) isTcpForwardInit_Target() {} -func (*SandboxStreamEvent_Sandbox) isSandboxStreamEvent_Payload() {} +func (*TcpForwardInit_Tcp) isTcpForwardInit_Target() {} -func (*SandboxStreamEvent_Log) isSandboxStreamEvent_Payload() {} +// A single frame on the CLI-to-gateway TCP forward stream. +type TcpForwardFrame struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *TcpForwardFrame_Init + // *TcpForwardFrame_Data + Payload isTcpForwardFrame_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} -func (*SandboxStreamEvent_Event) isSandboxStreamEvent_Payload() {} +func (x *TcpForwardFrame) Reset() { + *x = TcpForwardFrame{} + mi := &file_openshell_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} -func (*SandboxStreamEvent_Warning) isSandboxStreamEvent_Payload() {} +func (x *TcpForwardFrame) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpForwardFrame) ProtoMessage() {} + +func (x *TcpForwardFrame) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[62] + 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 TcpForwardFrame.ProtoReflect.Descriptor instead. +func (*TcpForwardFrame) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{62} +} + +func (x *TcpForwardFrame) GetPayload() isTcpForwardFrame_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *TcpForwardFrame) GetInit() *TcpForwardInit { + if x != nil { + if x, ok := x.Payload.(*TcpForwardFrame_Init); ok { + return x.Init + } + } + return nil +} + +func (x *TcpForwardFrame) GetData() []byte { + if x != nil { + if x, ok := x.Payload.(*TcpForwardFrame_Data); ok { + return x.Data + } + } + return nil +} + +type isTcpForwardFrame_Payload interface { + isTcpForwardFrame_Payload() +} + +type TcpForwardFrame_Init struct { + Init *TcpForwardInit `protobuf:"bytes,1,opt,name=init,proto3,oneof"` +} + +type TcpForwardFrame_Data struct { + Data []byte `protobuf:"bytes,2,opt,name=data,proto3,oneof"` +} + +func (*TcpForwardFrame_Init) isTcpForwardFrame_Payload() {} + +func (*TcpForwardFrame_Data) isTcpForwardFrame_Payload() {} + +// Client-to-server message for interactive exec. +type ExecSandboxInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *ExecSandboxInput_Start + // *ExecSandboxInput_Stdin + // *ExecSandboxInput_Resize + Payload isExecSandboxInput_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxInput) Reset() { + *x = ExecSandboxInput{} + mi := &file_openshell_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxInput) ProtoMessage() {} + +func (x *ExecSandboxInput) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[63] + 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 ExecSandboxInput.ProtoReflect.Descriptor instead. +func (*ExecSandboxInput) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{63} +} + +func (x *ExecSandboxInput) GetPayload() isExecSandboxInput_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *ExecSandboxInput) GetStart() *ExecSandboxRequest { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxInput_Start); ok { + return x.Start + } + } + return nil +} + +func (x *ExecSandboxInput) GetStdin() []byte { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxInput_Stdin); ok { + return x.Stdin + } + } + return nil +} + +func (x *ExecSandboxInput) GetResize() *ExecSandboxWindowResize { + if x != nil { + if x, ok := x.Payload.(*ExecSandboxInput_Resize); ok { + return x.Resize + } + } + return nil +} + +type isExecSandboxInput_Payload interface { + isExecSandboxInput_Payload() +} + +type ExecSandboxInput_Start struct { + // First message: exec request metadata. + Start *ExecSandboxRequest `protobuf:"bytes,1,opt,name=start,proto3,oneof"` +} + +type ExecSandboxInput_Stdin struct { + // Subsequent messages: raw stdin bytes. + Stdin []byte `protobuf:"bytes,2,opt,name=stdin,proto3,oneof"` +} + +type ExecSandboxInput_Resize struct { + // Terminal window size change. + Resize *ExecSandboxWindowResize `protobuf:"bytes,3,opt,name=resize,proto3,oneof"` +} + +func (*ExecSandboxInput_Start) isExecSandboxInput_Payload() {} + +func (*ExecSandboxInput_Stdin) isExecSandboxInput_Payload() {} + +func (*ExecSandboxInput_Resize) isExecSandboxInput_Payload() {} + +// Terminal window resize event for interactive exec. +type ExecSandboxWindowResize struct { + state protoimpl.MessageState `protogen:"open.v1"` + Cols uint32 `protobuf:"varint,1,opt,name=cols,proto3" json:"cols,omitempty"` + Rows uint32 `protobuf:"varint,2,opt,name=rows,proto3" json:"rows,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecSandboxWindowResize) Reset() { + *x = ExecSandboxWindowResize{} + mi := &file_openshell_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecSandboxWindowResize) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecSandboxWindowResize) ProtoMessage() {} + +func (x *ExecSandboxWindowResize) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[64] + 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 ExecSandboxWindowResize.ProtoReflect.Descriptor instead. +func (*ExecSandboxWindowResize) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{64} +} + +func (x *ExecSandboxWindowResize) GetCols() uint32 { + if x != nil { + return x.Cols + } + return 0 +} + +func (x *ExecSandboxWindowResize) GetRows() uint32 { + if x != nil { + return x.Rows + } + return 0 +} + +// SSH session record stored in persistence. +type SshSession struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Kubernetes-style metadata (id, name, labels, timestamps, resource version). + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // Sandbox id. + SandboxId string `protobuf:"bytes,2,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + // Session token. + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` + // Expiry timestamp in milliseconds since epoch. 0 means no expiry + // (backward-compatible default for sessions created before this field existed). + ExpiresAtMs int64 `protobuf:"varint,4,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + // Revoked flag. + Revoked bool `protobuf:"varint,5,opt,name=revoked,proto3" json:"revoked,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SshSession) Reset() { + *x = SshSession{} + mi := &file_openshell_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SshSession) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SshSession) ProtoMessage() {} + +func (x *SshSession) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[65] + 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 SshSession.ProtoReflect.Descriptor instead. +func (*SshSession) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{65} +} + +func (x *SshSession) GetMetadata() *datamodelv1.ObjectMeta { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *SshSession) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *SshSession) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *SshSession) GetExpiresAtMs() int64 { + if x != nil { + return x.ExpiresAtMs + } + return 0 +} + +func (x *SshSession) GetRevoked() bool { + if x != nil { + return x.Revoked + } + return false +} + +// Watch sandbox request. +type WatchSandboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Sandbox id. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Stream sandbox status snapshots. + FollowStatus bool `protobuf:"varint,2,opt,name=follow_status,json=followStatus,proto3" json:"follow_status,omitempty"` + // Stream openshell-server process logs correlated to this sandbox. + FollowLogs bool `protobuf:"varint,3,opt,name=follow_logs,json=followLogs,proto3" json:"follow_logs,omitempty"` + // Stream platform events correlated to this sandbox. + FollowEvents bool `protobuf:"varint,4,opt,name=follow_events,json=followEvents,proto3" json:"follow_events,omitempty"` + // Replay the last N log lines (best-effort) before following. + LogTailLines uint32 `protobuf:"varint,5,opt,name=log_tail_lines,json=logTailLines,proto3" json:"log_tail_lines,omitempty"` + // Replay the last N platform events (best-effort) before following. + EventTail uint32 `protobuf:"varint,6,opt,name=event_tail,json=eventTail,proto3" json:"event_tail,omitempty"` + // Stop streaming once the sandbox reaches a terminal phase (READY or ERROR). + StopOnTerminal bool `protobuf:"varint,7,opt,name=stop_on_terminal,json=stopOnTerminal,proto3" json:"stop_on_terminal,omitempty"` + // Only include log lines with timestamp >= this value (milliseconds since epoch). + // 0 means no time filter. Applies to both tail replay and live streaming. + LogSinceMs int64 `protobuf:"varint,8,opt,name=log_since_ms,json=logSinceMs,proto3" json:"log_since_ms,omitempty"` + // Filter by log source (e.g. "gateway", "sandbox"). Empty means all sources. + LogSources []string `protobuf:"bytes,9,rep,name=log_sources,json=logSources,proto3" json:"log_sources,omitempty"` + // Minimum log level to include (e.g. "INFO", "WARN", "ERROR"). Empty means all levels. + LogMinLevel string `protobuf:"bytes,10,opt,name=log_min_level,json=logMinLevel,proto3" json:"log_min_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WatchSandboxRequest) Reset() { + *x = WatchSandboxRequest{} + mi := &file_openshell_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WatchSandboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WatchSandboxRequest) ProtoMessage() {} + +func (x *WatchSandboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[66] + 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 WatchSandboxRequest.ProtoReflect.Descriptor instead. +func (*WatchSandboxRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{66} +} + +func (x *WatchSandboxRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *WatchSandboxRequest) GetFollowStatus() bool { + if x != nil { + return x.FollowStatus + } + return false +} + +func (x *WatchSandboxRequest) GetFollowLogs() bool { + if x != nil { + return x.FollowLogs + } + return false +} + +func (x *WatchSandboxRequest) GetFollowEvents() bool { + if x != nil { + return x.FollowEvents + } + return false +} + +func (x *WatchSandboxRequest) GetLogTailLines() uint32 { + if x != nil { + return x.LogTailLines + } + return 0 +} + +func (x *WatchSandboxRequest) GetEventTail() uint32 { + if x != nil { + return x.EventTail + } + return 0 +} + +func (x *WatchSandboxRequest) GetStopOnTerminal() bool { + if x != nil { + return x.StopOnTerminal + } + return false +} + +func (x *WatchSandboxRequest) GetLogSinceMs() int64 { + if x != nil { + return x.LogSinceMs + } + return 0 +} + +func (x *WatchSandboxRequest) GetLogSources() []string { + if x != nil { + return x.LogSources + } + return nil +} + +func (x *WatchSandboxRequest) GetLogMinLevel() string { + if x != nil { + return x.LogMinLevel + } + return "" +} + +// One event in a sandbox watch stream. +type SandboxStreamEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *SandboxStreamEvent_Sandbox + // *SandboxStreamEvent_Log + // *SandboxStreamEvent_Event + // *SandboxStreamEvent_Warning + // *SandboxStreamEvent_DraftPolicyUpdate + Payload isSandboxStreamEvent_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxStreamEvent) Reset() { + *x = SandboxStreamEvent{} + mi := &file_openshell_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxStreamEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxStreamEvent) ProtoMessage() {} + +func (x *SandboxStreamEvent) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[67] + 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 SandboxStreamEvent.ProtoReflect.Descriptor instead. +func (*SandboxStreamEvent) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{67} +} + +func (x *SandboxStreamEvent) GetPayload() isSandboxStreamEvent_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *SandboxStreamEvent) GetSandbox() *Sandbox { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_Sandbox); ok { + return x.Sandbox + } + } + return nil +} + +func (x *SandboxStreamEvent) GetLog() *SandboxLogLine { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_Log); ok { + return x.Log + } + } + return nil +} + +func (x *SandboxStreamEvent) GetEvent() *PlatformEvent { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_Event); ok { + return x.Event + } + } + return nil +} + +func (x *SandboxStreamEvent) GetWarning() *SandboxStreamWarning { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_Warning); ok { + return x.Warning + } + } + return nil +} + +func (x *SandboxStreamEvent) GetDraftPolicyUpdate() *DraftPolicyUpdate { + if x != nil { + if x, ok := x.Payload.(*SandboxStreamEvent_DraftPolicyUpdate); ok { + return x.DraftPolicyUpdate + } + } + return nil +} + +type isSandboxStreamEvent_Payload interface { + isSandboxStreamEvent_Payload() +} + +type SandboxStreamEvent_Sandbox struct { + // Latest sandbox snapshot. + Sandbox *Sandbox `protobuf:"bytes,1,opt,name=sandbox,proto3,oneof"` +} + +type SandboxStreamEvent_Log struct { + // One server log line/event. + Log *SandboxLogLine `protobuf:"bytes,2,opt,name=log,proto3,oneof"` +} + +type SandboxStreamEvent_Event struct { + // One platform event. + Event *PlatformEvent `protobuf:"bytes,3,opt,name=event,proto3,oneof"` +} + +type SandboxStreamEvent_Warning struct { + // Warning from the server (e.g. missed messages due to lag). + Warning *SandboxStreamWarning `protobuf:"bytes,4,opt,name=warning,proto3,oneof"` +} + +type SandboxStreamEvent_DraftPolicyUpdate struct { + // Draft policy update notification. + DraftPolicyUpdate *DraftPolicyUpdate `protobuf:"bytes,5,opt,name=draft_policy_update,json=draftPolicyUpdate,proto3,oneof"` +} + +func (*SandboxStreamEvent_Sandbox) isSandboxStreamEvent_Payload() {} + +func (*SandboxStreamEvent_Log) isSandboxStreamEvent_Payload() {} + +func (*SandboxStreamEvent_Event) isSandboxStreamEvent_Payload() {} + +func (*SandboxStreamEvent_Warning) isSandboxStreamEvent_Payload() {} func (*SandboxStreamEvent_DraftPolicyUpdate) isSandboxStreamEvent_Payload() {} -// Log line correlated to a sandbox. -type SandboxLogLine struct { - state protoimpl.MessageState `protogen:"open.v1"` - SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` - TimestampMs int64 `protobuf:"varint,2,opt,name=timestamp_ms,json=timestampMs,proto3" json:"timestamp_ms,omitempty"` - Level string `protobuf:"bytes,3,opt,name=level,proto3" json:"level,omitempty"` - Target string `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` - Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` - // Log source: "gateway" (server-side) or "sandbox" (supervisor). - // Empty is treated as "gateway" for backward compatibility. - Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` - // Structured key-value fields from the tracing event (e.g. dst_host, action). - Fields map[string]string `protobuf:"bytes,7,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +// Log line correlated to a sandbox. +type SandboxLogLine struct { + state protoimpl.MessageState `protogen:"open.v1"` + SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"` + TimestampMs int64 `protobuf:"varint,2,opt,name=timestamp_ms,json=timestampMs,proto3" json:"timestamp_ms,omitempty"` + Level string `protobuf:"bytes,3,opt,name=level,proto3" json:"level,omitempty"` + Target string `protobuf:"bytes,4,opt,name=target,proto3" json:"target,omitempty"` + Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + // Log source: "gateway" (server-side) or "sandbox" (supervisor). + // Empty is treated as "gateway" for backward compatibility. + Source string `protobuf:"bytes,6,opt,name=source,proto3" json:"source,omitempty"` + // Structured key-value fields from the tracing event (e.g. dst_host, action). + Fields map[string]string `protobuf:"bytes,7,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxLogLine) Reset() { + *x = SandboxLogLine{} + mi := &file_openshell_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxLogLine) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxLogLine) ProtoMessage() {} + +func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[68] + 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 SandboxLogLine.ProtoReflect.Descriptor instead. +func (*SandboxLogLine) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{68} +} + +func (x *SandboxLogLine) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" +} + +func (x *SandboxLogLine) GetTimestampMs() int64 { + if x != nil { + return x.TimestampMs + } + return 0 +} + +func (x *SandboxLogLine) GetLevel() string { + if x != nil { + return x.Level + } + return "" +} + +func (x *SandboxLogLine) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +func (x *SandboxLogLine) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *SandboxLogLine) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *SandboxLogLine) GetFields() map[string]string { + if x != nil { + return x.Fields + } + return nil +} + +type SandboxStreamWarning struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SandboxStreamWarning) Reset() { + *x = SandboxStreamWarning{} + mi := &file_openshell_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SandboxStreamWarning) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SandboxStreamWarning) ProtoMessage() {} + +func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[69] + 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 SandboxStreamWarning.ProtoReflect.Descriptor instead. +func (*SandboxStreamWarning) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{69} +} + +func (x *SandboxStreamWarning) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// Create provider request. +type CreateProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + // Workspace for the provider. Empty defaults to "default". + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateProviderRequest) Reset() { + *x = CreateProviderRequest{} + mi := &file_openshell_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateProviderRequest) ProtoMessage() {} + +func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[70] + 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 CreateProviderRequest.ProtoReflect.Descriptor instead. +func (*CreateProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{70} +} + +func (x *CreateProviderRequest) GetProvider() *datamodelv1.Provider { + if x != nil { + return x.Provider + } + return nil +} + +func (x *CreateProviderRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// Get provider request. +type GetProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderRequest) Reset() { + *x = GetProviderRequest{} + mi := &file_openshell_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderRequest) ProtoMessage() {} + +func (x *GetProviderRequest) 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 GetProviderRequest.ProtoReflect.Descriptor instead. +func (*GetProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{71} +} + +func (x *GetProviderRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GetProviderRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// List providers request. +type ListProvidersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` + // List across all workspaces. Mutually exclusive with workspace. + AllWorkspaces bool `protobuf:"varint,4,opt,name=all_workspaces,json=allWorkspaces,proto3" json:"all_workspaces,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProvidersRequest) Reset() { + *x = ListProvidersRequest{} + mi := &file_openshell_proto_msgTypes[72] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProvidersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProvidersRequest) ProtoMessage() {} + +func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[72] + 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 ListProvidersRequest.ProtoReflect.Descriptor instead. +func (*ListProvidersRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{72} +} + +func (x *ListProvidersRequest) GetLimit() uint32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ListProvidersRequest) GetOffset() uint32 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *ListProvidersRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *ListProvidersRequest) GetAllWorkspaces() bool { + if x != nil { + return x.AllWorkspaces + } + return false +} + +// Update provider request. +type UpdateProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + // Optional per-credential expiry timestamps to merge into the provider. + // A zero value removes the expiry for that credential. + CredentialExpiresAtMs map[string]int64 `protobuf:"bytes,2,rep,name=credential_expires_at_ms,json=credentialExpiresAtMs,proto3" json:"credential_expires_at_ms,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateProviderRequest) Reset() { + *x = UpdateProviderRequest{} + mi := &file_openshell_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateProviderRequest) ProtoMessage() {} + +func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[73] + 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 UpdateProviderRequest.ProtoReflect.Descriptor instead. +func (*UpdateProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{73} +} + +func (x *UpdateProviderRequest) GetProvider() *datamodelv1.Provider { + if x != nil { + return x.Provider + } + return nil +} + +func (x *UpdateProviderRequest) GetCredentialExpiresAtMs() map[string]int64 { + if x != nil { + return x.CredentialExpiresAtMs + } + return nil +} + +func (x *UpdateProviderRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// Delete provider request. +type DeleteProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Workspace scope. Empty defaults to "default". + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteProviderRequest) Reset() { + *x = DeleteProviderRequest{} + mi := &file_openshell_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteProviderRequest) ProtoMessage() {} + +func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[74] + 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 DeleteProviderRequest.ProtoReflect.Descriptor instead. +func (*DeleteProviderRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{74} +} + +func (x *DeleteProviderRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DeleteProviderRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// Provider response. +type ProviderResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderResponse) Reset() { + *x = ProviderResponse{} + mi := &file_openshell_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderResponse) ProtoMessage() {} + +func (x *ProviderResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[75] + 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 ProviderResponse.ProtoReflect.Descriptor instead. +func (*ProviderResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{75} +} + +func (x *ProviderResponse) GetProvider() *datamodelv1.Provider { + if x != nil { + return x.Provider + } + return nil +} + +// List providers response. +type ListProvidersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Providers []*datamodelv1.Provider `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProvidersResponse) Reset() { + *x = ListProvidersResponse{} + mi := &file_openshell_proto_msgTypes[76] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProvidersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProvidersResponse) ProtoMessage() {} + +func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[76] + 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 ListProvidersResponse.ProtoReflect.Descriptor instead. +func (*ListProvidersResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{76} +} + +func (x *ListProvidersResponse) GetProviders() []*datamodelv1.Provider { + if x != nil { + return x.Providers + } + return nil +} + +// List provider type profiles request. +type ListProviderProfilesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + // Workspace scope. When set, returns workspace-scoped + built-in profiles. + // When empty, returns platform-scoped + built-in only. + Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProviderProfilesRequest) Reset() { + *x = ListProviderProfilesRequest{} + mi := &file_openshell_proto_msgTypes[77] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProviderProfilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProviderProfilesRequest) ProtoMessage() {} + +func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[77] + 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 ListProviderProfilesRequest.ProtoReflect.Descriptor instead. +func (*ListProviderProfilesRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{77} +} + +func (x *ListProviderProfilesRequest) GetLimit() uint32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ListProviderProfilesRequest) GetOffset() uint32 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *ListProviderProfilesRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// Fetch provider type profile request. +type GetProviderProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Workspace scope for two-tier profile resolution. When set, checks + // workspace-scoped profiles first, then platform-scoped, then built-in. + // When empty, checks platform-scoped then built-in only. + Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderProfileRequest) Reset() { + *x = GetProviderProfileRequest{} + mi := &file_openshell_proto_msgTypes[78] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderProfileRequest) ProtoMessage() {} + +func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[78] + 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 GetProviderProfileRequest.ProtoReflect.Descriptor instead. +func (*GetProviderProfileRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{78} +} + +func (x *GetProviderProfileRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *GetProviderProfileRequest) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// Provider profile payload with optional source metadata for diagnostics. +type ProviderProfileImportItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Profile *ProviderProfile `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"` + Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SandboxLogLine) Reset() { - *x = SandboxLogLine{} - mi := &file_openshell_proto_msgTypes[59] +func (x *ProviderProfileImportItem) Reset() { + *x = ProviderProfileImportItem{} + mi := &file_openshell_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SandboxLogLine) String() string { +func (x *ProviderProfileImportItem) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SandboxLogLine) ProtoMessage() {} +func (*ProviderProfileImportItem) ProtoMessage() {} -func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[59] +func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4424,82 +5672,134 @@ func (x *SandboxLogLine) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SandboxLogLine.ProtoReflect.Descriptor instead. -func (*SandboxLogLine) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{59} +// Deprecated: Use ProviderProfileImportItem.ProtoReflect.Descriptor instead. +func (*ProviderProfileImportItem) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{79} } -func (x *SandboxLogLine) GetSandboxId() string { +func (x *ProviderProfileImportItem) GetProfile() *ProviderProfile { if x != nil { - return x.SandboxId + return x.Profile + } + return nil +} + +func (x *ProviderProfileImportItem) GetSource() string { + if x != nil { + return x.Source } return "" } -func (x *SandboxLogLine) GetTimestampMs() int64 { +// Provider profile validation diagnostic. +type ProviderProfileDiagnostic struct { + state protoimpl.MessageState `protogen:"open.v1"` + Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + ProfileId string `protobuf:"bytes,2,opt,name=profile_id,json=profileId,proto3" json:"profile_id,omitempty"` + Field string `protobuf:"bytes,3,opt,name=field,proto3" json:"field,omitempty"` + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + Severity string `protobuf:"bytes,5,opt,name=severity,proto3" json:"severity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderProfileDiagnostic) Reset() { + *x = ProviderProfileDiagnostic{} + mi := &file_openshell_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderProfileDiagnostic) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderProfileDiagnostic) ProtoMessage() {} + +func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[80] if x != nil { - return x.TimestampMs + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return 0 + return mi.MessageOf(x) } -func (x *SandboxLogLine) GetLevel() string { +// Deprecated: Use ProviderProfileDiagnostic.ProtoReflect.Descriptor instead. +func (*ProviderProfileDiagnostic) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{80} +} + +func (x *ProviderProfileDiagnostic) GetSource() string { if x != nil { - return x.Level + return x.Source } return "" } -func (x *SandboxLogLine) GetTarget() string { +func (x *ProviderProfileDiagnostic) GetProfileId() string { if x != nil { - return x.Target + return x.ProfileId } return "" } -func (x *SandboxLogLine) GetMessage() string { +func (x *ProviderProfileDiagnostic) GetField() string { if x != nil { - return x.Message + return x.Field } return "" } -func (x *SandboxLogLine) GetSource() string { +func (x *ProviderProfileDiagnostic) GetMessage() string { if x != nil { - return x.Source + return x.Message } return "" } -func (x *SandboxLogLine) GetFields() map[string]string { +func (x *ProviderProfileDiagnostic) GetSeverity() string { if x != nil { - return x.Fields + return x.Severity } - return nil + return "" } -type SandboxStreamWarning struct { - state protoimpl.MessageState `protogen:"open.v1"` - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +// Endpoint selector for token grant audience overrides. +type ProviderCredentialTokenGrantAudienceOverride struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional: endpoint host selector. If omitted, inherits the profile endpoint host. + Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` + // Optional: endpoint port selector. If omitted, matches the expanded profile endpoint port. + Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` + // Optional: endpoint path selector. If omitted, inherits the profile endpoint path. + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + // Resource audience to request for matching endpoints. + Audience string `protobuf:"bytes,4,opt,name=audience,proto3" json:"audience,omitempty"` + // Optional: OAuth2 scopes to request. If omitted, inherits the token grant scopes. + Scopes []string `protobuf:"bytes,5,rep,name=scopes,proto3" json:"scopes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *SandboxStreamWarning) Reset() { - *x = SandboxStreamWarning{} - mi := &file_openshell_proto_msgTypes[60] +func (x *ProviderCredentialTokenGrantAudienceOverride) Reset() { + *x = ProviderCredentialTokenGrantAudienceOverride{} + mi := &file_openshell_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SandboxStreamWarning) String() string { +func (x *ProviderCredentialTokenGrantAudienceOverride) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SandboxStreamWarning) ProtoMessage() {} +func (*ProviderCredentialTokenGrantAudienceOverride) ProtoMessage() {} -func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[60] +func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4510,97 +5810,75 @@ func (x *SandboxStreamWarning) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SandboxStreamWarning.ProtoReflect.Descriptor instead. -func (*SandboxStreamWarning) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{60} +// Deprecated: Use ProviderCredentialTokenGrantAudienceOverride.ProtoReflect.Descriptor instead. +func (*ProviderCredentialTokenGrantAudienceOverride) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{81} } -func (x *SandboxStreamWarning) GetMessage() string { +func (x *ProviderCredentialTokenGrantAudienceOverride) GetHost() string { if x != nil { - return x.Message + return x.Host } return "" } -// Create provider request. -type CreateProviderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` - // Workspace for the provider. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CreateProviderRequest) Reset() { - *x = CreateProviderRequest{} - mi := &file_openshell_proto_msgTypes[61] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CreateProviderRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CreateProviderRequest) ProtoMessage() {} - -func (x *CreateProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[61] +func (x *ProviderCredentialTokenGrantAudienceOverride) GetPort() uint32 { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Port } - return mi.MessageOf(x) + return 0 } -// Deprecated: Use CreateProviderRequest.ProtoReflect.Descriptor instead. -func (*CreateProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{61} +func (x *ProviderCredentialTokenGrantAudienceOverride) GetPath() string { + if x != nil { + return x.Path + } + return "" } -func (x *CreateProviderRequest) GetProvider() *datamodelv1.Provider { +func (x *ProviderCredentialTokenGrantAudienceOverride) GetAudience() string { if x != nil { - return x.Provider + return x.Audience } - return nil + return "" } -func (x *CreateProviderRequest) GetWorkspace() string { +func (x *ProviderCredentialTokenGrantAudienceOverride) GetScopes() []string { if x != nil { - return x.Workspace + return x.Scopes } - return "" + return nil } -// Get provider request. -type GetProviderRequest struct { +type ProviderCredentialTokenGrantSubjectToken struct { state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // 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 *GetProviderRequest) Reset() { - *x = GetProviderRequest{} - mi := &file_openshell_proto_msgTypes[62] +func (x *ProviderCredentialTokenGrantSubjectToken) Reset() { + *x = ProviderCredentialTokenGrantSubjectToken{} + mi := &file_openshell_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *GetProviderRequest) String() string { +func (x *ProviderCredentialTokenGrantSubjectToken) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetProviderRequest) ProtoMessage() {} +func (*ProviderCredentialTokenGrantSubjectToken) ProtoMessage() {} -func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[62] +func (x *ProviderCredentialTokenGrantSubjectToken) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4611,53 +5889,78 @@ func (x *GetProviderRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GetProviderRequest.ProtoReflect.Descriptor instead. -func (*GetProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{62} +// Deprecated: Use ProviderCredentialTokenGrantSubjectToken.ProtoReflect.Descriptor instead. +func (*ProviderCredentialTokenGrantSubjectToken) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{82} } -func (x *GetProviderRequest) GetName() string { +func (x *ProviderCredentialTokenGrantSubjectToken) GetSource() string { if x != nil { - return x.Name + return x.Source } return "" } -func (x *GetProviderRequest) GetWorkspace() string { +func (x *ProviderCredentialTokenGrantSubjectToken) GetCredential() string { if x != nil { - return x.Workspace + return x.Credential } return "" } -// List providers request. -type ListProvidersRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` - Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - // List across all workspaces. Mutually exclusive with workspace. - AllWorkspaces bool `protobuf:"varint,4,opt,name=all_workspaces,json=allWorkspaces,proto3" json:"all_workspaces,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +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) + TokenEndpoint string `protobuf:"bytes,1,opt,name=token_endpoint,json=tokenEndpoint,proto3" json:"token_endpoint,omitempty"` + // Optional: default resource audience to request from the token service + Audience string `protobuf:"bytes,2,opt,name=audience,proto3" json:"audience,omitempty"` + // Optional: audience to request when fetching the JWT-SVID from SPIRE. + // If omitted, the sandbox derives this from token_endpoint. + JwtSvidAudience string `protobuf:"bytes,6,opt,name=jwt_svid_audience,json=jwtSvidAudience,proto3" json:"jwt_svid_audience,omitempty"` + // Optional: OAuth2 scopes to request + Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` + // Optional: override token cache TTL (seconds) + // If 0 or omitted, use expires_in from token response + CacheTtlSeconds int64 `protobuf:"varint,4,opt,name=cache_ttl_seconds,json=cacheTtlSeconds,proto3" json:"cache_ttl_seconds,omitempty"` + // Optional: endpoint-specific resource audience overrides. + AudienceOverrides []*ProviderCredentialTokenGrantAudienceOverride `protobuf:"bytes,5,rep,name=audience_overrides,json=audienceOverrides,proto3" json:"audience_overrides,omitempty"` + // 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"` + // 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 *ListProvidersRequest) Reset() { - *x = ListProvidersRequest{} - mi := &file_openshell_proto_msgTypes[63] +func (x *ProviderCredentialTokenGrant) Reset() { + *x = ProviderCredentialTokenGrant{} + mi := &file_openshell_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListProvidersRequest) String() string { +func (x *ProviderCredentialTokenGrant) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListProvidersRequest) ProtoMessage() {} +func (*ProviderCredentialTokenGrant) ProtoMessage() {} -func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[63] +func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4668,67 +5971,113 @@ func (x *ListProvidersRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListProvidersRequest.ProtoReflect.Descriptor instead. -func (*ListProvidersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{63} +// Deprecated: Use ProviderCredentialTokenGrant.ProtoReflect.Descriptor instead. +func (*ProviderCredentialTokenGrant) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{83} } -func (x *ListProvidersRequest) GetLimit() uint32 { +func (x *ProviderCredentialTokenGrant) GetTokenEndpoint() string { if x != nil { - return x.Limit + return x.TokenEndpoint } - return 0 + return "" } -func (x *ListProvidersRequest) GetOffset() uint32 { +func (x *ProviderCredentialTokenGrant) GetAudience() string { if x != nil { - return x.Offset + return x.Audience + } + return "" +} + +func (x *ProviderCredentialTokenGrant) GetJwtSvidAudience() string { + if x != nil { + return x.JwtSvidAudience + } + return "" +} + +func (x *ProviderCredentialTokenGrant) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *ProviderCredentialTokenGrant) GetCacheTtlSeconds() int64 { + if x != nil { + return x.CacheTtlSeconds } return 0 } -func (x *ListProvidersRequest) GetWorkspace() string { +func (x *ProviderCredentialTokenGrant) GetAudienceOverrides() []*ProviderCredentialTokenGrantAudienceOverride { if x != nil { - return x.Workspace + return x.AudienceOverrides + } + return nil +} + +func (x *ProviderCredentialTokenGrant) GetClientAssertionType() string { + if x != nil { + return x.ClientAssertionType } return "" } -func (x *ListProvidersRequest) GetAllWorkspaces() bool { +func (x *ProviderCredentialTokenGrant) GetGrantType() ProviderCredentialTokenGrantType { if x != nil { - return x.AllWorkspaces + return x.GrantType } - return false + return ProviderCredentialTokenGrantType_PROVIDER_CREDENTIAL_TOKEN_GRANT_TYPE_UNSPECIFIED } -// Update provider request. -type UpdateProviderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` - // Optional per-credential expiry timestamps to merge into the provider. - // A zero value removes the expiry for that credential. - CredentialExpiresAtMs map[string]int64 `protobuf:"bytes,2,rep,name=credential_expires_at_ms,json=credentialExpiresAtMs,proto3" json:"credential_expires_at_ms,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` +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"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + EnvVars []string `protobuf:"bytes,3,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty"` + Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` + AuthStyle string `protobuf:"bytes,5,opt,name=auth_style,json=authStyle,proto3" json:"auth_style,omitempty"` + HeaderName string `protobuf:"bytes,6,opt,name=header_name,json=headerName,proto3" json:"header_name,omitempty"` + QueryParam string `protobuf:"bytes,7,opt,name=query_param,json=queryParam,proto3" json:"query_param,omitempty"` + Refresh *ProviderCredentialRefresh `protobuf:"bytes,8,opt,name=refresh,proto3" json:"refresh,omitempty"` + PathTemplate string `protobuf:"bytes,9,opt,name=path_template,json=pathTemplate,proto3" json:"path_template,omitempty"` + TokenGrant *ProviderCredentialTokenGrant `protobuf:"bytes,10,opt,name=token_grant,json=tokenGrant,proto3" json:"token_grant,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *UpdateProviderRequest) Reset() { - *x = UpdateProviderRequest{} - mi := &file_openshell_proto_msgTypes[64] +func (x *ProviderProfileCredential) Reset() { + *x = ProviderProfileCredential{} + mi := &file_openshell_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *UpdateProviderRequest) String() string { +func (x *ProviderProfileCredential) String() string { return protoimpl.X.MessageStringOf(x) } -func (*UpdateProviderRequest) ProtoMessage() {} +func (*ProviderProfileCredential) ProtoMessage() {} -func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[64] +func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4739,109 +6088,106 @@ func (x *UpdateProviderRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use UpdateProviderRequest.ProtoReflect.Descriptor instead. -func (*UpdateProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{64} +// Deprecated: Use ProviderProfileCredential.ProtoReflect.Descriptor instead. +func (*ProviderProfileCredential) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{84} } -func (x *UpdateProviderRequest) GetProvider() *datamodelv1.Provider { +func (x *ProviderProfileCredential) GetName() string { if x != nil { - return x.Provider + return x.Name } - return nil + return "" } -func (x *UpdateProviderRequest) GetCredentialExpiresAtMs() map[string]int64 { +func (x *ProviderProfileCredential) GetDescription() string { if x != nil { - return x.CredentialExpiresAtMs + return x.Description } - return nil + return "" } -func (x *UpdateProviderRequest) GetWorkspace() string { +func (x *ProviderProfileCredential) GetEnvVars() []string { if x != nil { - return x.Workspace + return x.EnvVars } - return "" + return nil } -// Delete provider request. -type DeleteProviderRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Workspace scope. Empty defaults to "default". - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *ProviderProfileCredential) GetRequired() bool { + if x != nil { + return x.Required + } + return false } -func (x *DeleteProviderRequest) Reset() { - *x = DeleteProviderRequest{} - mi := &file_openshell_proto_msgTypes[65] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *ProviderProfileCredential) GetAuthStyle() string { + if x != nil { + return x.AuthStyle + } + return "" } -func (x *DeleteProviderRequest) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *ProviderProfileCredential) GetHeaderName() string { + if x != nil { + return x.HeaderName + } + return "" } -func (*DeleteProviderRequest) ProtoMessage() {} - -func (x *DeleteProviderRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[65] +func (x *ProviderProfileCredential) GetQueryParam() string { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.QueryParam } - return mi.MessageOf(x) + return "" } -// Deprecated: Use DeleteProviderRequest.ProtoReflect.Descriptor instead. -func (*DeleteProviderRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{65} +func (x *ProviderProfileCredential) GetRefresh() *ProviderCredentialRefresh { + if x != nil { + return x.Refresh + } + return nil } -func (x *DeleteProviderRequest) GetName() string { +func (x *ProviderProfileCredential) GetPathTemplate() string { if x != nil { - return x.Name + return x.PathTemplate } return "" } -func (x *DeleteProviderRequest) GetWorkspace() string { +func (x *ProviderProfileCredential) GetTokenGrant() *ProviderCredentialTokenGrant { if x != nil { - return x.Workspace + return x.TokenGrant } - return "" + return nil } -// Provider response. -type ProviderResponse struct { +type ProviderCredentialRefreshMaterial struct { state protoimpl.MessageState `protogen:"open.v1"` - Provider *datamodelv1.Provider `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Required bool `protobuf:"varint,3,opt,name=required,proto3" json:"required,omitempty"` + Secret bool `protobuf:"varint,4,opt,name=secret,proto3" json:"secret,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ProviderResponse) Reset() { - *x = ProviderResponse{} - mi := &file_openshell_proto_msgTypes[66] +func (x *ProviderCredentialRefreshMaterial) Reset() { + *x = ProviderCredentialRefreshMaterial{} + mi := &file_openshell_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ProviderResponse) String() string { +func (x *ProviderCredentialRefreshMaterial) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ProviderResponse) ProtoMessage() {} +func (*ProviderCredentialRefreshMaterial) ProtoMessage() {} -func (x *ProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[66] +func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4852,41 +6198,66 @@ func (x *ProviderResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ProviderResponse.ProtoReflect.Descriptor instead. -func (*ProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{66} +// Deprecated: Use ProviderCredentialRefreshMaterial.ProtoReflect.Descriptor instead. +func (*ProviderCredentialRefreshMaterial) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{85} } -func (x *ProviderResponse) GetProvider() *datamodelv1.Provider { +func (x *ProviderCredentialRefreshMaterial) GetName() string { if x != nil { - return x.Provider + return x.Name } - return nil + return "" } -// List providers response. -type ListProvidersResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Providers []*datamodelv1.Provider `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"` +func (x *ProviderCredentialRefreshMaterial) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *ProviderCredentialRefreshMaterial) GetRequired() bool { + if x != nil { + return x.Required + } + return false +} + +func (x *ProviderCredentialRefreshMaterial) GetSecret() bool { + if x != nil { + return x.Secret + } + return false +} + +// Declares that a single refresh operation mints more than one credential. +// The refresh is attached to a primary credential; each additional output +// maps a strategy-defined semantic output id to a sibling credential whose +// env_vars receive the minted value. +type ProviderCredentialRefreshOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` // strategy-defined semantic output id (e.g. "session_token") + Credential string `protobuf:"bytes,2,opt,name=credential,proto3" json:"credential,omitempty"` // sibling credential name whose env_vars receive this output unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ListProvidersResponse) Reset() { - *x = ListProvidersResponse{} - mi := &file_openshell_proto_msgTypes[67] +func (x *ProviderCredentialRefreshOutput) Reset() { + *x = ProviderCredentialRefreshOutput{} + mi := &file_openshell_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListProvidersResponse) String() string { +func (x *ProviderCredentialRefreshOutput) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListProvidersResponse) ProtoMessage() {} +func (*ProviderCredentialRefreshOutput) ProtoMessage() {} -func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[67] +func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4897,45 +6268,53 @@ func (x *ListProvidersResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListProvidersResponse.ProtoReflect.Descriptor instead. -func (*ListProvidersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{67} +// Deprecated: Use ProviderCredentialRefreshOutput.ProtoReflect.Descriptor instead. +func (*ProviderCredentialRefreshOutput) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{86} } -func (x *ListProvidersResponse) GetProviders() []*datamodelv1.Provider { +func (x *ProviderCredentialRefreshOutput) GetOutput() string { if x != nil { - return x.Providers + return x.Output } - return nil + return "" } -// List provider type profiles request. -type ListProviderProfilesRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` - Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` - // Workspace scope. When set, returns workspace-scoped + built-in profiles. - // When empty, returns platform-scoped + built-in only. - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *ProviderCredentialRefreshOutput) GetCredential() string { + if x != nil { + return x.Credential + } + return "" } -func (x *ListProviderProfilesRequest) Reset() { - *x = ListProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[68] +type ProviderCredentialRefresh struct { + state protoimpl.MessageState `protogen:"open.v1"` + Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,1,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` + TokenUrl string `protobuf:"bytes,2,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` + Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` + RefreshBeforeSeconds int64 `protobuf:"varint,4,opt,name=refresh_before_seconds,json=refreshBeforeSeconds,proto3" json:"refresh_before_seconds,omitempty"` + MaxLifetimeSeconds int64 `protobuf:"varint,5,opt,name=max_lifetime_seconds,json=maxLifetimeSeconds,proto3" json:"max_lifetime_seconds,omitempty"` + Material []*ProviderCredentialRefreshMaterial `protobuf:"bytes,6,rep,name=material,proto3" json:"material,omitempty"` + AdditionalOutputs []*ProviderCredentialRefreshOutput `protobuf:"bytes,7,rep,name=additional_outputs,json=additionalOutputs,proto3" json:"additional_outputs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderCredentialRefresh) Reset() { + *x = ProviderCredentialRefresh{} + mi := &file_openshell_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListProviderProfilesRequest) String() string { +func (x *ProviderCredentialRefresh) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListProviderProfilesRequest) ProtoMessage() {} +func (*ProviderCredentialRefresh) ProtoMessage() {} -func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[68] +func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4946,59 +6325,90 @@ func (x *ListProviderProfilesRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListProviderProfilesRequest.ProtoReflect.Descriptor instead. -func (*ListProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{68} +// Deprecated: Use ProviderCredentialRefresh.ProtoReflect.Descriptor instead. +func (*ProviderCredentialRefresh) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{87} } -func (x *ListProviderProfilesRequest) GetLimit() uint32 { +func (x *ProviderCredentialRefresh) GetStrategy() ProviderCredentialRefreshStrategy { if x != nil { - return x.Limit + return x.Strategy + } + return ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED +} + +func (x *ProviderCredentialRefresh) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + +func (x *ProviderCredentialRefresh) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *ProviderCredentialRefresh) GetRefreshBeforeSeconds() int64 { + if x != nil { + return x.RefreshBeforeSeconds } return 0 } -func (x *ListProviderProfilesRequest) GetOffset() uint32 { +func (x *ProviderCredentialRefresh) GetMaxLifetimeSeconds() int64 { if x != nil { - return x.Offset + return x.MaxLifetimeSeconds } return 0 } -func (x *ListProviderProfilesRequest) GetWorkspace() string { +func (x *ProviderCredentialRefresh) GetMaterial() []*ProviderCredentialRefreshMaterial { if x != nil { - return x.Workspace + return x.Material } - return "" + return nil } -// Fetch provider type profile request. -type GetProviderProfileRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // Workspace scope for two-tier profile resolution. When set, checks - // workspace-scoped profiles first, then platform-scoped, then built-in. - // When empty, checks platform-scoped then built-in only. - Workspace string `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *ProviderCredentialRefresh) GetAdditionalOutputs() []*ProviderCredentialRefreshOutput { + if x != nil { + return x.AdditionalOutputs + } + return nil } -func (x *GetProviderProfileRequest) Reset() { - *x = GetProviderProfileRequest{} - mi := &file_openshell_proto_msgTypes[69] +type ProviderCredentialRefreshStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProviderName string `protobuf:"bytes,1,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + ProviderId string `protobuf:"bytes,2,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + CredentialKey string `protobuf:"bytes,3,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,4,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + ExpiresAtMs int64 `protobuf:"varint,6,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + NextRefreshAtMs int64 `protobuf:"varint,7,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` + LastRefreshAtMs int64 `protobuf:"varint,8,opt,name=last_refresh_at_ms,json=lastRefreshAtMs,proto3" json:"last_refresh_at_ms,omitempty"` + LastError string `protobuf:"bytes,9,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderCredentialRefreshStatus) Reset() { + *x = ProviderCredentialRefreshStatus{} + mi := &file_openshell_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *GetProviderProfileRequest) String() string { +func (x *ProviderCredentialRefreshStatus) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetProviderProfileRequest) ProtoMessage() {} +func (*ProviderCredentialRefreshStatus) ProtoMessage() {} -func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[69] +func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5009,105 +6419,98 @@ func (x *GetProviderProfileRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GetProviderProfileRequest.ProtoReflect.Descriptor instead. -func (*GetProviderProfileRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{69} +// Deprecated: Use ProviderCredentialRefreshStatus.ProtoReflect.Descriptor instead. +func (*ProviderCredentialRefreshStatus) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{88} } -func (x *GetProviderProfileRequest) GetId() string { +func (x *ProviderCredentialRefreshStatus) GetProviderName() string { if x != nil { - return x.Id + return x.ProviderName } return "" } -func (x *GetProviderProfileRequest) GetWorkspace() string { +func (x *ProviderCredentialRefreshStatus) GetProviderId() string { if x != nil { - return x.Workspace + return x.ProviderId } return "" } -// Provider profile payload with optional source metadata for diagnostics. -type ProviderProfileImportItem struct { - state protoimpl.MessageState `protogen:"open.v1"` - Profile *ProviderProfile `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"` - Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *ProviderCredentialRefreshStatus) GetCredentialKey() string { + if x != nil { + return x.CredentialKey + } + return "" } -func (x *ProviderProfileImportItem) Reset() { - *x = ProviderProfileImportItem{} - mi := &file_openshell_proto_msgTypes[70] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *ProviderCredentialRefreshStatus) GetStrategy() ProviderCredentialRefreshStrategy { + if x != nil { + return x.Strategy + } + return ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED } -func (x *ProviderProfileImportItem) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *ProviderCredentialRefreshStatus) GetStatus() string { + if x != nil { + return x.Status + } + return "" } -func (*ProviderProfileImportItem) ProtoMessage() {} - -func (x *ProviderProfileImportItem) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[70] +func (x *ProviderCredentialRefreshStatus) GetExpiresAtMs() int64 { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.ExpiresAtMs } - return mi.MessageOf(x) + return 0 } -// Deprecated: Use ProviderProfileImportItem.ProtoReflect.Descriptor instead. -func (*ProviderProfileImportItem) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{70} +func (x *ProviderCredentialRefreshStatus) GetNextRefreshAtMs() int64 { + if x != nil { + return x.NextRefreshAtMs + } + return 0 } -func (x *ProviderProfileImportItem) GetProfile() *ProviderProfile { +func (x *ProviderCredentialRefreshStatus) GetLastRefreshAtMs() int64 { if x != nil { - return x.Profile + return x.LastRefreshAtMs } - return nil + return 0 } -func (x *ProviderProfileImportItem) GetSource() string { +func (x *ProviderCredentialRefreshStatus) GetLastError() string { if x != nil { - return x.Source + return x.LastError } return "" } -// Provider profile validation diagnostic. -type ProviderProfileDiagnostic struct { - state protoimpl.MessageState `protogen:"open.v1"` - Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` - ProfileId string `protobuf:"bytes,2,opt,name=profile_id,json=profileId,proto3" json:"profile_id,omitempty"` - Field string `protobuf:"bytes,3,opt,name=field,proto3" json:"field,omitempty"` - Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` - Severity string `protobuf:"bytes,5,opt,name=severity,proto3" json:"severity,omitempty"` +// Provider profile local discovery declaration. +type ProviderProfileDiscovery struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Credential names from ProviderProfile.credentials eligible for local discovery. + Credentials []string `protobuf:"bytes,1,rep,name=credentials,proto3" json:"credentials,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ProviderProfileDiagnostic) Reset() { - *x = ProviderProfileDiagnostic{} - mi := &file_openshell_proto_msgTypes[71] +func (x *ProviderProfileDiscovery) Reset() { + *x = ProviderProfileDiscovery{} + mi := &file_openshell_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ProviderProfileDiagnostic) String() string { +func (x *ProviderProfileDiscovery) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ProviderProfileDiagnostic) ProtoMessage() {} +func (*ProviderProfileDiscovery) ProtoMessage() {} -func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[71] +func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5118,78 +6521,60 @@ func (x *ProviderProfileDiagnostic) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ProviderProfileDiagnostic.ProtoReflect.Descriptor instead. -func (*ProviderProfileDiagnostic) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{71} -} - -func (x *ProviderProfileDiagnostic) GetSource() string { - if x != nil { - return x.Source - } - return "" -} - -func (x *ProviderProfileDiagnostic) GetProfileId() string { - if x != nil { - return x.ProfileId - } - return "" -} - -func (x *ProviderProfileDiagnostic) GetField() string { - if x != nil { - return x.Field - } - return "" -} - -func (x *ProviderProfileDiagnostic) GetMessage() string { - if x != nil { - return x.Message - } - return "" +// Deprecated: Use ProviderProfileDiscovery.ProtoReflect.Descriptor instead. +func (*ProviderProfileDiscovery) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{89} } -func (x *ProviderProfileDiagnostic) GetSeverity() string { +func (x *ProviderProfileDiscovery) GetCredentials() []string { if x != nil { - return x.Severity + return x.Credentials } - return "" + return nil } -// Endpoint selector for token grant audience overrides. -type ProviderCredentialTokenGrantAudienceOverride struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Optional: endpoint host selector. If omitted, inherits the profile endpoint host. - Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` - // Optional: endpoint port selector. If omitted, matches the expanded profile endpoint port. - Port uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` - // Optional: endpoint path selector. If omitted, inherits the profile endpoint path. - Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` - // Resource audience to request for matching endpoints. - Audience string `protobuf:"bytes,4,opt,name=audience,proto3" json:"audience,omitempty"` - // Optional: OAuth2 scopes to request. If omitted, inherits the token grant scopes. - Scopes []string `protobuf:"bytes,5,rep,name=scopes,proto3" json:"scopes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type StoredProviderCredentialRefreshState struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + ProviderId string `protobuf:"bytes,2,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + ProviderName string `protobuf:"bytes,3,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` + CredentialKey string `protobuf:"bytes,4,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` + Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,5,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` + Material map[string]string `protobuf:"bytes,6,rep,name=material,proto3" json:"material,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + SecretMaterialKeys []string `protobuf:"bytes,7,rep,name=secret_material_keys,json=secretMaterialKeys,proto3" json:"secret_material_keys,omitempty"` + ExpiresAtMs int64 `protobuf:"varint,8,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + NextRefreshAtMs int64 `protobuf:"varint,9,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` + LastRefreshAtMs int64 `protobuf:"varint,10,opt,name=last_refresh_at_ms,json=lastRefreshAtMs,proto3" json:"last_refresh_at_ms,omitempty"` + Status string `protobuf:"bytes,11,opt,name=status,proto3" json:"status,omitempty"` + LastError string `protobuf:"bytes,12,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` + TokenUrl string `protobuf:"bytes,13,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` + Scopes []string `protobuf:"bytes,14,rep,name=scopes,proto3" json:"scopes,omitempty"` + RefreshBeforeSeconds int64 `protobuf:"varint,15,opt,name=refresh_before_seconds,json=refreshBeforeSeconds,proto3" json:"refresh_before_seconds,omitempty"` + MaxLifetimeSeconds int64 `protobuf:"varint,16,opt,name=max_lifetime_seconds,json=maxLifetimeSeconds,proto3" json:"max_lifetime_seconds,omitempty"` + // Resolved mapping of strategy-defined output id -> concrete env key, pinned + // at configure time from the profile's additional_outputs. Read by minting, + // collision reservation, and env-key surfacing so later profile edits cannot + // silently redirect writes. + AdditionalOutputKeys map[string]string `protobuf:"bytes,17,rep,name=additional_output_keys,json=additionalOutputKeys,proto3" json:"additional_output_keys,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ProviderCredentialTokenGrantAudienceOverride) Reset() { - *x = ProviderCredentialTokenGrantAudienceOverride{} - mi := &file_openshell_proto_msgTypes[72] +func (x *StoredProviderCredentialRefreshState) Reset() { + *x = StoredProviderCredentialRefreshState{} + mi := &file_openshell_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ProviderCredentialTokenGrantAudienceOverride) String() string { +func (x *StoredProviderCredentialRefreshState) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ProviderCredentialTokenGrantAudienceOverride) ProtoMessage() {} +func (*StoredProviderCredentialRefreshState) ProtoMessage() {} -func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[72] +func (x *StoredProviderCredentialRefreshState) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5200,182 +6585,162 @@ func (x *ProviderCredentialTokenGrantAudienceOverride) ProtoReflect() protorefle return mi.MessageOf(x) } -// Deprecated: Use ProviderCredentialTokenGrantAudienceOverride.ProtoReflect.Descriptor instead. -func (*ProviderCredentialTokenGrantAudienceOverride) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{72} +// Deprecated: Use StoredProviderCredentialRefreshState.ProtoReflect.Descriptor instead. +func (*StoredProviderCredentialRefreshState) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{90} } -func (x *ProviderCredentialTokenGrantAudienceOverride) GetHost() string { +func (x *StoredProviderCredentialRefreshState) GetMetadata() *datamodelv1.ObjectMeta { if x != nil { - return x.Host + return x.Metadata } - return "" + return nil } -func (x *ProviderCredentialTokenGrantAudienceOverride) GetPort() uint32 { +func (x *StoredProviderCredentialRefreshState) GetProviderId() string { if x != nil { - return x.Port + return x.ProviderId } - return 0 + return "" } -func (x *ProviderCredentialTokenGrantAudienceOverride) GetPath() string { +func (x *StoredProviderCredentialRefreshState) GetProviderName() string { if x != nil { - return x.Path + return x.ProviderName } return "" } -func (x *ProviderCredentialTokenGrantAudienceOverride) GetAudience() string { +func (x *StoredProviderCredentialRefreshState) GetCredentialKey() string { if x != nil { - return x.Audience + return x.CredentialKey } return "" } -func (x *ProviderCredentialTokenGrantAudienceOverride) GetScopes() []string { +func (x *StoredProviderCredentialRefreshState) GetStrategy() ProviderCredentialRefreshStrategy { if x != nil { - return x.Scopes + return x.Strategy } - return nil + return ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED } -// Provider credential token grant configuration. -// When present, the credential is obtained dynamically via OAuth2 grant when needed. -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) - TokenEndpoint string `protobuf:"bytes,1,opt,name=token_endpoint,json=tokenEndpoint,proto3" json:"token_endpoint,omitempty"` - // Optional: default resource audience to request from the token service - Audience string `protobuf:"bytes,2,opt,name=audience,proto3" json:"audience,omitempty"` - // Optional: audience to request when fetching the JWT-SVID from SPIRE. - // If omitted, the sandbox derives this from token_endpoint. - JwtSvidAudience string `protobuf:"bytes,6,opt,name=jwt_svid_audience,json=jwtSvidAudience,proto3" json:"jwt_svid_audience,omitempty"` - // Optional: OAuth2 scopes to request - Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` - // Optional: override token cache TTL (seconds) - // If 0 or omitted, use expires_in from token response - CacheTtlSeconds int64 `protobuf:"varint,4,opt,name=cache_ttl_seconds,json=cacheTtlSeconds,proto3" json:"cache_ttl_seconds,omitempty"` - // Optional: endpoint-specific resource audience overrides. - AudienceOverrides []*ProviderCredentialTokenGrantAudienceOverride `protobuf:"bytes,5,rep,name=audience_overrides,json=audienceOverrides,proto3" json:"audience_overrides,omitempty"` - // 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 +func (x *StoredProviderCredentialRefreshState) GetMaterial() map[string]string { + if x != nil { + return x.Material + } + return nil } -func (x *ProviderCredentialTokenGrant) Reset() { - *x = ProviderCredentialTokenGrant{} - mi := &file_openshell_proto_msgTypes[73] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *StoredProviderCredentialRefreshState) GetSecretMaterialKeys() []string { + if x != nil { + return x.SecretMaterialKeys + } + return nil } -func (x *ProviderCredentialTokenGrant) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *StoredProviderCredentialRefreshState) GetExpiresAtMs() int64 { + if x != nil { + return x.ExpiresAtMs + } + return 0 } -func (*ProviderCredentialTokenGrant) ProtoMessage() {} - -func (x *ProviderCredentialTokenGrant) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[73] +func (x *StoredProviderCredentialRefreshState) GetNextRefreshAtMs() int64 { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.NextRefreshAtMs } - return mi.MessageOf(x) + return 0 } -// Deprecated: Use ProviderCredentialTokenGrant.ProtoReflect.Descriptor instead. -func (*ProviderCredentialTokenGrant) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{73} +func (x *StoredProviderCredentialRefreshState) GetLastRefreshAtMs() int64 { + if x != nil { + return x.LastRefreshAtMs + } + return 0 } -func (x *ProviderCredentialTokenGrant) GetTokenEndpoint() string { +func (x *StoredProviderCredentialRefreshState) GetStatus() string { if x != nil { - return x.TokenEndpoint + return x.Status } return "" } -func (x *ProviderCredentialTokenGrant) GetAudience() string { +func (x *StoredProviderCredentialRefreshState) GetLastError() string { if x != nil { - return x.Audience + return x.LastError } return "" } -func (x *ProviderCredentialTokenGrant) GetJwtSvidAudience() string { +func (x *StoredProviderCredentialRefreshState) GetTokenUrl() string { if x != nil { - return x.JwtSvidAudience + return x.TokenUrl } return "" } -func (x *ProviderCredentialTokenGrant) GetScopes() []string { +func (x *StoredProviderCredentialRefreshState) GetScopes() []string { if x != nil { return x.Scopes } return nil } -func (x *ProviderCredentialTokenGrant) GetCacheTtlSeconds() int64 { +func (x *StoredProviderCredentialRefreshState) GetRefreshBeforeSeconds() int64 { if x != nil { - return x.CacheTtlSeconds + return x.RefreshBeforeSeconds } return 0 } -func (x *ProviderCredentialTokenGrant) GetAudienceOverrides() []*ProviderCredentialTokenGrantAudienceOverride { +func (x *StoredProviderCredentialRefreshState) GetMaxLifetimeSeconds() int64 { if x != nil { - return x.AudienceOverrides + return x.MaxLifetimeSeconds } - return nil + return 0 } -func (x *ProviderCredentialTokenGrant) GetClientAssertionType() string { +func (x *StoredProviderCredentialRefreshState) GetAdditionalOutputKeys() map[string]string { if x != nil { - return x.ClientAssertionType + return x.AdditionalOutputKeys } - return "" -} - -// Provider credential declaration. -type ProviderProfileCredential struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - EnvVars []string `protobuf:"bytes,3,rep,name=env_vars,json=envVars,proto3" json:"env_vars,omitempty"` - Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` - AuthStyle string `protobuf:"bytes,5,opt,name=auth_style,json=authStyle,proto3" json:"auth_style,omitempty"` - HeaderName string `protobuf:"bytes,6,opt,name=header_name,json=headerName,proto3" json:"header_name,omitempty"` - QueryParam string `protobuf:"bytes,7,opt,name=query_param,json=queryParam,proto3" json:"query_param,omitempty"` - Refresh *ProviderCredentialRefresh `protobuf:"bytes,8,opt,name=refresh,proto3" json:"refresh,omitempty"` - PathTemplate string `protobuf:"bytes,9,opt,name=path_template,json=pathTemplate,proto3" json:"path_template,omitempty"` - TokenGrant *ProviderCredentialTokenGrant `protobuf:"bytes,10,opt,name=token_grant,json=tokenGrant,proto3" json:"token_grant,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + return nil } -func (x *ProviderProfileCredential) Reset() { - *x = ProviderProfileCredential{} - mi := &file_openshell_proto_msgTypes[74] +type DelegatedIdentityCredential struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Issuer string `protobuf:"bytes,2,opt,name=issuer,proto3" json:"issuer,omitempty"` + ClientId string `protobuf:"bytes,3,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + PrincipalSubject string `protobuf:"bytes,4,opt,name=principal_subject,json=principalSubject,proto3" json:"principal_subject,omitempty"` + RefreshToken string `protobuf:"bytes,5,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"` + AccessToken string `protobuf:"bytes,6,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + AccessTokenExpiresAtMs int64 `protobuf:"varint,7,opt,name=access_token_expires_at_ms,json=accessTokenExpiresAtMs,proto3" json:"access_token_expires_at_ms,omitempty"` + Scopes string `protobuf:"bytes,8,opt,name=scopes,proto3" json:"scopes,omitempty"` + Audience string `protobuf:"bytes,9,opt,name=audience,proto3" json:"audience,omitempty"` + LastRefreshAtMs int64 `protobuf:"varint,10,opt,name=last_refresh_at_ms,json=lastRefreshAtMs,proto3" json:"last_refresh_at_ms,omitempty"` + RevokedAtMs int64 `protobuf:"varint,11,opt,name=revoked_at_ms,json=revokedAtMs,proto3" json:"revoked_at_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DelegatedIdentityCredential) Reset() { + *x = DelegatedIdentityCredential{} + mi := &file_openshell_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ProviderProfileCredential) String() string { +func (x *DelegatedIdentityCredential) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ProviderProfileCredential) ProtoMessage() {} +func (*DelegatedIdentityCredential) ProtoMessage() {} -func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[74] +func (x *DelegatedIdentityCredential) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5386,106 +6751,120 @@ func (x *ProviderProfileCredential) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ProviderProfileCredential.ProtoReflect.Descriptor instead. -func (*ProviderProfileCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{74} +// Deprecated: Use DelegatedIdentityCredential.ProtoReflect.Descriptor instead. +func (*DelegatedIdentityCredential) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{91} } -func (x *ProviderProfileCredential) GetName() string { +func (x *DelegatedIdentityCredential) GetMetadata() *datamodelv1.ObjectMeta { if x != nil { - return x.Name + return x.Metadata } - return "" + return nil } -func (x *ProviderProfileCredential) GetDescription() string { +func (x *DelegatedIdentityCredential) GetIssuer() string { if x != nil { - return x.Description + return x.Issuer } return "" } -func (x *ProviderProfileCredential) GetEnvVars() []string { +func (x *DelegatedIdentityCredential) GetClientId() string { if x != nil { - return x.EnvVars + return x.ClientId } - return nil + return "" } -func (x *ProviderProfileCredential) GetRequired() bool { +func (x *DelegatedIdentityCredential) GetPrincipalSubject() string { if x != nil { - return x.Required + return x.PrincipalSubject } - return false + return "" } -func (x *ProviderProfileCredential) GetAuthStyle() string { +func (x *DelegatedIdentityCredential) GetRefreshToken() string { if x != nil { - return x.AuthStyle + return x.RefreshToken } return "" } -func (x *ProviderProfileCredential) GetHeaderName() string { +func (x *DelegatedIdentityCredential) GetAccessToken() string { if x != nil { - return x.HeaderName + return x.AccessToken } return "" } -func (x *ProviderProfileCredential) GetQueryParam() string { +func (x *DelegatedIdentityCredential) GetAccessTokenExpiresAtMs() int64 { if x != nil { - return x.QueryParam + return x.AccessTokenExpiresAtMs } - return "" + return 0 } -func (x *ProviderProfileCredential) GetRefresh() *ProviderCredentialRefresh { +func (x *DelegatedIdentityCredential) GetScopes() string { if x != nil { - return x.Refresh + return x.Scopes } - return nil + return "" } -func (x *ProviderProfileCredential) GetPathTemplate() string { +func (x *DelegatedIdentityCredential) GetAudience() string { if x != nil { - return x.PathTemplate + return x.Audience } return "" } -func (x *ProviderProfileCredential) GetTokenGrant() *ProviderCredentialTokenGrant { +func (x *DelegatedIdentityCredential) GetLastRefreshAtMs() int64 { if x != nil { - return x.TokenGrant + return x.LastRefreshAtMs } - return nil + return 0 } -type ProviderCredentialRefreshMaterial struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - Required bool `protobuf:"varint,3,opt,name=required,proto3" json:"required,omitempty"` - Secret bool `protobuf:"varint,4,opt,name=secret,proto3" json:"secret,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *DelegatedIdentityCredential) GetRevokedAtMs() int64 { + if x != nil { + return x.RevokedAtMs + } + return 0 } -func (x *ProviderCredentialRefreshMaterial) Reset() { - *x = ProviderCredentialRefreshMaterial{} - mi := &file_openshell_proto_msgTypes[75] +type DelegatedIdentityCredentialSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Issuer string `protobuf:"bytes,2,opt,name=issuer,proto3" json:"issuer,omitempty"` + ClientId string `protobuf:"bytes,3,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + PrincipalSubject string `protobuf:"bytes,4,opt,name=principal_subject,json=principalSubject,proto3" json:"principal_subject,omitempty"` + RefreshTokenPresent bool `protobuf:"varint,5,opt,name=refresh_token_present,json=refreshTokenPresent,proto3" json:"refresh_token_present,omitempty"` + AccessTokenPresent bool `protobuf:"varint,6,opt,name=access_token_present,json=accessTokenPresent,proto3" json:"access_token_present,omitempty"` + AccessTokenExpiresAtMs int64 `protobuf:"varint,7,opt,name=access_token_expires_at_ms,json=accessTokenExpiresAtMs,proto3" json:"access_token_expires_at_ms,omitempty"` + Scopes string `protobuf:"bytes,8,opt,name=scopes,proto3" json:"scopes,omitempty"` + Audience string `protobuf:"bytes,9,opt,name=audience,proto3" json:"audience,omitempty"` + LastRefreshAtMs int64 `protobuf:"varint,10,opt,name=last_refresh_at_ms,json=lastRefreshAtMs,proto3" json:"last_refresh_at_ms,omitempty"` + RevokedAtMs int64 `protobuf:"varint,11,opt,name=revoked_at_ms,json=revokedAtMs,proto3" json:"revoked_at_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DelegatedIdentityCredentialSummary) Reset() { + *x = DelegatedIdentityCredentialSummary{} + mi := &file_openshell_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ProviderCredentialRefreshMaterial) String() string { +func (x *DelegatedIdentityCredentialSummary) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ProviderCredentialRefreshMaterial) ProtoMessage() {} +func (*DelegatedIdentityCredentialSummary) ProtoMessage() {} -func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[75] +func (x *DelegatedIdentityCredentialSummary) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5496,123 +6875,111 @@ func (x *ProviderCredentialRefreshMaterial) ProtoReflect() protoreflect.Message return mi.MessageOf(x) } -// Deprecated: Use ProviderCredentialRefreshMaterial.ProtoReflect.Descriptor instead. -func (*ProviderCredentialRefreshMaterial) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{75} +// Deprecated: Use DelegatedIdentityCredentialSummary.ProtoReflect.Descriptor instead. +func (*DelegatedIdentityCredentialSummary) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{92} } -func (x *ProviderCredentialRefreshMaterial) GetName() string { +func (x *DelegatedIdentityCredentialSummary) GetMetadata() *datamodelv1.ObjectMeta { if x != nil { - return x.Name + return x.Metadata } - return "" + return nil } -func (x *ProviderCredentialRefreshMaterial) GetDescription() string { +func (x *DelegatedIdentityCredentialSummary) GetIssuer() string { if x != nil { - return x.Description + return x.Issuer } return "" } -func (x *ProviderCredentialRefreshMaterial) GetRequired() bool { +func (x *DelegatedIdentityCredentialSummary) GetClientId() string { if x != nil { - return x.Required + return x.ClientId } - return false + return "" } -func (x *ProviderCredentialRefreshMaterial) GetSecret() bool { +func (x *DelegatedIdentityCredentialSummary) GetPrincipalSubject() string { if x != nil { - return x.Secret + return x.PrincipalSubject } - return false + return "" } -// Declares that a single refresh operation mints more than one credential. -// The refresh is attached to a primary credential; each additional output -// maps a strategy-defined semantic output id to a sibling credential whose -// env_vars receive the minted value. -type ProviderCredentialRefreshOutput struct { - state protoimpl.MessageState `protogen:"open.v1"` - Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` // strategy-defined semantic output id (e.g. "session_token") - Credential string `protobuf:"bytes,2,opt,name=credential,proto3" json:"credential,omitempty"` // sibling credential name whose env_vars receive this output - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *DelegatedIdentityCredentialSummary) GetRefreshTokenPresent() bool { + if x != nil { + return x.RefreshTokenPresent + } + return false } -func (x *ProviderCredentialRefreshOutput) Reset() { - *x = ProviderCredentialRefreshOutput{} - mi := &file_openshell_proto_msgTypes[76] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *DelegatedIdentityCredentialSummary) GetAccessTokenPresent() bool { + if x != nil { + return x.AccessTokenPresent + } + return false } -func (x *ProviderCredentialRefreshOutput) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *DelegatedIdentityCredentialSummary) GetAccessTokenExpiresAtMs() int64 { + if x != nil { + return x.AccessTokenExpiresAtMs + } + return 0 } -func (*ProviderCredentialRefreshOutput) ProtoMessage() {} - -func (x *ProviderCredentialRefreshOutput) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[76] +func (x *DelegatedIdentityCredentialSummary) GetScopes() string { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Scopes } - return mi.MessageOf(x) + return "" } -// Deprecated: Use ProviderCredentialRefreshOutput.ProtoReflect.Descriptor instead. -func (*ProviderCredentialRefreshOutput) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{76} +func (x *DelegatedIdentityCredentialSummary) GetAudience() string { + if x != nil { + return x.Audience + } + return "" } -func (x *ProviderCredentialRefreshOutput) GetOutput() string { +func (x *DelegatedIdentityCredentialSummary) GetLastRefreshAtMs() int64 { if x != nil { - return x.Output + return x.LastRefreshAtMs } - return "" + return 0 } -func (x *ProviderCredentialRefreshOutput) GetCredential() string { +func (x *DelegatedIdentityCredentialSummary) GetRevokedAtMs() int64 { if x != nil { - return x.Credential + return x.RevokedAtMs } - return "" + return 0 } -type ProviderCredentialRefresh struct { - state protoimpl.MessageState `protogen:"open.v1"` - Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,1,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` - TokenUrl string `protobuf:"bytes,2,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` - Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` - RefreshBeforeSeconds int64 `protobuf:"varint,4,opt,name=refresh_before_seconds,json=refreshBeforeSeconds,proto3" json:"refresh_before_seconds,omitempty"` - MaxLifetimeSeconds int64 `protobuf:"varint,5,opt,name=max_lifetime_seconds,json=maxLifetimeSeconds,proto3" json:"max_lifetime_seconds,omitempty"` - Material []*ProviderCredentialRefreshMaterial `protobuf:"bytes,6,rep,name=material,proto3" json:"material,omitempty"` - AdditionalOutputs []*ProviderCredentialRefreshOutput `protobuf:"bytes,7,rep,name=additional_outputs,json=additionalOutputs,proto3" json:"additional_outputs,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type ListDelegatedIdentityCredentialsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit uint32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ProviderCredentialRefresh) Reset() { - *x = ProviderCredentialRefresh{} - mi := &file_openshell_proto_msgTypes[77] +func (x *ListDelegatedIdentityCredentialsRequest) Reset() { + *x = ListDelegatedIdentityCredentialsRequest{} + mi := &file_openshell_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ProviderCredentialRefresh) String() string { +func (x *ListDelegatedIdentityCredentialsRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ProviderCredentialRefresh) ProtoMessage() {} +func (*ListDelegatedIdentityCredentialsRequest) ProtoMessage() {} -func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[77] +func (x *ListDelegatedIdentityCredentialsRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5623,90 +6990,47 @@ func (x *ProviderCredentialRefresh) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ProviderCredentialRefresh.ProtoReflect.Descriptor instead. -func (*ProviderCredentialRefresh) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{77} -} - -func (x *ProviderCredentialRefresh) GetStrategy() ProviderCredentialRefreshStrategy { - if x != nil { - return x.Strategy - } - return ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED -} - -func (x *ProviderCredentialRefresh) GetTokenUrl() string { - if x != nil { - return x.TokenUrl - } - return "" -} - -func (x *ProviderCredentialRefresh) GetScopes() []string { - if x != nil { - return x.Scopes - } - return nil +// Deprecated: Use ListDelegatedIdentityCredentialsRequest.ProtoReflect.Descriptor instead. +func (*ListDelegatedIdentityCredentialsRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{93} } -func (x *ProviderCredentialRefresh) GetRefreshBeforeSeconds() int64 { +func (x *ListDelegatedIdentityCredentialsRequest) GetLimit() uint32 { if x != nil { - return x.RefreshBeforeSeconds + return x.Limit } return 0 } -func (x *ProviderCredentialRefresh) GetMaxLifetimeSeconds() int64 { +func (x *ListDelegatedIdentityCredentialsRequest) GetOffset() uint32 { if x != nil { - return x.MaxLifetimeSeconds + return x.Offset } return 0 } -func (x *ProviderCredentialRefresh) GetMaterial() []*ProviderCredentialRefreshMaterial { - if x != nil { - return x.Material - } - return nil -} - -func (x *ProviderCredentialRefresh) GetAdditionalOutputs() []*ProviderCredentialRefreshOutput { - if x != nil { - return x.AdditionalOutputs - } - return nil -} - -type ProviderCredentialRefreshStatus struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProviderName string `protobuf:"bytes,1,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` - ProviderId string `protobuf:"bytes,2,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` - CredentialKey string `protobuf:"bytes,3,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` - Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,4,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` - Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` - ExpiresAtMs int64 `protobuf:"varint,6,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` - NextRefreshAtMs int64 `protobuf:"varint,7,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` - LastRefreshAtMs int64 `protobuf:"varint,8,opt,name=last_refresh_at_ms,json=lastRefreshAtMs,proto3" json:"last_refresh_at_ms,omitempty"` - LastError string `protobuf:"bytes,9,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type ListDelegatedIdentityCredentialsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Credentials []*DelegatedIdentityCredentialSummary `protobuf:"bytes,1,rep,name=credentials,proto3" json:"credentials,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ProviderCredentialRefreshStatus) Reset() { - *x = ProviderCredentialRefreshStatus{} - mi := &file_openshell_proto_msgTypes[78] +func (x *ListDelegatedIdentityCredentialsResponse) Reset() { + *x = ListDelegatedIdentityCredentialsResponse{} + mi := &file_openshell_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ProviderCredentialRefreshStatus) String() string { +func (x *ListDelegatedIdentityCredentialsResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ProviderCredentialRefreshStatus) ProtoMessage() {} +func (*ListDelegatedIdentityCredentialsResponse) ProtoMessage() {} -func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[78] +func (x *ListDelegatedIdentityCredentialsResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5717,98 +7041,85 @@ func (x *ProviderCredentialRefreshStatus) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ProviderCredentialRefreshStatus.ProtoReflect.Descriptor instead. -func (*ProviderCredentialRefreshStatus) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{78} -} - -func (x *ProviderCredentialRefreshStatus) GetProviderName() string { - if x != nil { - return x.ProviderName - } - return "" +// Deprecated: Use ListDelegatedIdentityCredentialsResponse.ProtoReflect.Descriptor instead. +func (*ListDelegatedIdentityCredentialsResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{94} } -func (x *ProviderCredentialRefreshStatus) GetProviderId() string { +func (x *ListDelegatedIdentityCredentialsResponse) GetCredentials() []*DelegatedIdentityCredentialSummary { if x != nil { - return x.ProviderId + return x.Credentials } - return "" + return nil } -func (x *ProviderCredentialRefreshStatus) GetCredentialKey() string { - if x != nil { - return x.CredentialKey - } - return "" +type GetDelegatedIdentityCredentialStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ProviderCredentialRefreshStatus) GetStrategy() ProviderCredentialRefreshStrategy { - if x != nil { - return x.Strategy - } - return ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED +func (x *GetDelegatedIdentityCredentialStatusRequest) Reset() { + *x = GetDelegatedIdentityCredentialStatusRequest{} + mi := &file_openshell_proto_msgTypes[95] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *ProviderCredentialRefreshStatus) GetStatus() string { - if x != nil { - return x.Status - } - return "" +func (x *GetDelegatedIdentityCredentialStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *ProviderCredentialRefreshStatus) GetExpiresAtMs() int64 { - if x != nil { - return x.ExpiresAtMs - } - return 0 -} +func (*GetDelegatedIdentityCredentialStatusRequest) ProtoMessage() {} -func (x *ProviderCredentialRefreshStatus) GetNextRefreshAtMs() int64 { +func (x *GetDelegatedIdentityCredentialStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[95] if x != nil { - return x.NextRefreshAtMs + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return 0 + return mi.MessageOf(x) } -func (x *ProviderCredentialRefreshStatus) GetLastRefreshAtMs() int64 { - if x != nil { - return x.LastRefreshAtMs - } - return 0 +// Deprecated: Use GetDelegatedIdentityCredentialStatusRequest.ProtoReflect.Descriptor instead. +func (*GetDelegatedIdentityCredentialStatusRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{95} } -func (x *ProviderCredentialRefreshStatus) GetLastError() string { +func (x *GetDelegatedIdentityCredentialStatusRequest) GetId() string { if x != nil { - return x.LastError + return x.Id } return "" } -// Provider profile local discovery declaration. -type ProviderProfileDiscovery struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Credential names from ProviderProfile.credentials eligible for local discovery. - Credentials []string `protobuf:"bytes,1,rep,name=credentials,proto3" json:"credentials,omitempty"` +type GetDelegatedIdentityCredentialStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Credential *DelegatedIdentityCredentialSummary `protobuf:"bytes,1,opt,name=credential,proto3" json:"credential,omitempty"` + NowMs int64 `protobuf:"varint,2,opt,name=now_ms,json=nowMs,proto3" json:"now_ms,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ProviderProfileDiscovery) Reset() { - *x = ProviderProfileDiscovery{} - mi := &file_openshell_proto_msgTypes[79] +func (x *GetDelegatedIdentityCredentialStatusResponse) Reset() { + *x = GetDelegatedIdentityCredentialStatusResponse{} + mi := &file_openshell_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ProviderProfileDiscovery) String() string { +func (x *GetDelegatedIdentityCredentialStatusResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ProviderProfileDiscovery) ProtoMessage() {} +func (*GetDelegatedIdentityCredentialStatusResponse) ProtoMessage() {} -func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[79] +func (x *GetDelegatedIdentityCredentialStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5819,60 +7130,48 @@ func (x *ProviderProfileDiscovery) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ProviderProfileDiscovery.ProtoReflect.Descriptor instead. -func (*ProviderProfileDiscovery) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{79} +// Deprecated: Use GetDelegatedIdentityCredentialStatusResponse.ProtoReflect.Descriptor instead. +func (*GetDelegatedIdentityCredentialStatusResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{96} } -func (x *ProviderProfileDiscovery) GetCredentials() []string { +func (x *GetDelegatedIdentityCredentialStatusResponse) GetCredential() *DelegatedIdentityCredentialSummary { if x != nil { - return x.Credentials + return x.Credential } return nil } -type StoredProviderCredentialRefreshState struct { - state protoimpl.MessageState `protogen:"open.v1"` - Metadata *datamodelv1.ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - ProviderId string `protobuf:"bytes,2,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` - ProviderName string `protobuf:"bytes,3,opt,name=provider_name,json=providerName,proto3" json:"provider_name,omitempty"` - CredentialKey string `protobuf:"bytes,4,opt,name=credential_key,json=credentialKey,proto3" json:"credential_key,omitempty"` - Strategy ProviderCredentialRefreshStrategy `protobuf:"varint,5,opt,name=strategy,proto3,enum=openshell.v1.ProviderCredentialRefreshStrategy" json:"strategy,omitempty"` - Material map[string]string `protobuf:"bytes,6,rep,name=material,proto3" json:"material,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - SecretMaterialKeys []string `protobuf:"bytes,7,rep,name=secret_material_keys,json=secretMaterialKeys,proto3" json:"secret_material_keys,omitempty"` - ExpiresAtMs int64 `protobuf:"varint,8,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` - NextRefreshAtMs int64 `protobuf:"varint,9,opt,name=next_refresh_at_ms,json=nextRefreshAtMs,proto3" json:"next_refresh_at_ms,omitempty"` - LastRefreshAtMs int64 `protobuf:"varint,10,opt,name=last_refresh_at_ms,json=lastRefreshAtMs,proto3" json:"last_refresh_at_ms,omitempty"` - Status string `protobuf:"bytes,11,opt,name=status,proto3" json:"status,omitempty"` - LastError string `protobuf:"bytes,12,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` - TokenUrl string `protobuf:"bytes,13,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` - Scopes []string `protobuf:"bytes,14,rep,name=scopes,proto3" json:"scopes,omitempty"` - RefreshBeforeSeconds int64 `protobuf:"varint,15,opt,name=refresh_before_seconds,json=refreshBeforeSeconds,proto3" json:"refresh_before_seconds,omitempty"` - MaxLifetimeSeconds int64 `protobuf:"varint,16,opt,name=max_lifetime_seconds,json=maxLifetimeSeconds,proto3" json:"max_lifetime_seconds,omitempty"` - // Resolved mapping of strategy-defined output id -> concrete env key, pinned - // at configure time from the profile's additional_outputs. Read by minting, - // collision reservation, and env-key surfacing so later profile edits cannot - // silently redirect writes. - AdditionalOutputKeys map[string]string `protobuf:"bytes,17,rep,name=additional_output_keys,json=additionalOutputKeys,proto3" json:"additional_output_keys,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *GetDelegatedIdentityCredentialStatusResponse) GetNowMs() int64 { + if x != nil { + return x.NowMs + } + return 0 } -func (x *StoredProviderCredentialRefreshState) Reset() { - *x = StoredProviderCredentialRefreshState{} - mi := &file_openshell_proto_msgTypes[80] +type RevokeDelegatedIdentityCredentialRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + ExpectedResourceVersion uint64 `protobuf:"varint,2,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeDelegatedIdentityCredentialRequest) Reset() { + *x = RevokeDelegatedIdentityCredentialRequest{} + mi := &file_openshell_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *StoredProviderCredentialRefreshState) String() string { +func (x *RevokeDelegatedIdentityCredentialRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*StoredProviderCredentialRefreshState) ProtoMessage() {} +func (*RevokeDelegatedIdentityCredentialRequest) ProtoMessage() {} -func (x *StoredProviderCredentialRefreshState) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[80] +func (x *RevokeDelegatedIdentityCredentialRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5883,128 +7182,179 @@ func (x *StoredProviderCredentialRefreshState) ProtoReflect() protoreflect.Messa return mi.MessageOf(x) } -// Deprecated: Use StoredProviderCredentialRefreshState.ProtoReflect.Descriptor instead. -func (*StoredProviderCredentialRefreshState) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{80} +// Deprecated: Use RevokeDelegatedIdentityCredentialRequest.ProtoReflect.Descriptor instead. +func (*RevokeDelegatedIdentityCredentialRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{97} } -func (x *StoredProviderCredentialRefreshState) GetMetadata() *datamodelv1.ObjectMeta { +func (x *RevokeDelegatedIdentityCredentialRequest) GetId() string { if x != nil { - return x.Metadata + return x.Id } - return nil + return "" } -func (x *StoredProviderCredentialRefreshState) GetProviderId() string { +func (x *RevokeDelegatedIdentityCredentialRequest) GetExpectedResourceVersion() uint64 { if x != nil { - return x.ProviderId + return x.ExpectedResourceVersion } - return "" + return 0 } -func (x *StoredProviderCredentialRefreshState) GetProviderName() string { - if x != nil { - return x.ProviderName - } - return "" +type RevokeDelegatedIdentityCredentialResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Revoked bool `protobuf:"varint,2,opt,name=revoked,proto3" json:"revoked,omitempty"` + RevokedAtMs int64 `protobuf:"varint,3,opt,name=revoked_at_ms,json=revokedAtMs,proto3" json:"revoked_at_ms,omitempty"` + ResourceVersion uint64 `protobuf:"varint,4,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *StoredProviderCredentialRefreshState) GetCredentialKey() string { - if x != nil { - return x.CredentialKey - } - return "" +func (x *RevokeDelegatedIdentityCredentialResponse) Reset() { + *x = RevokeDelegatedIdentityCredentialResponse{} + mi := &file_openshell_proto_msgTypes[98] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *StoredProviderCredentialRefreshState) GetStrategy() ProviderCredentialRefreshStrategy { - if x != nil { - return x.Strategy - } - return ProviderCredentialRefreshStrategy_PROVIDER_CREDENTIAL_REFRESH_STRATEGY_UNSPECIFIED +func (x *RevokeDelegatedIdentityCredentialResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *StoredProviderCredentialRefreshState) GetMaterial() map[string]string { +func (*RevokeDelegatedIdentityCredentialResponse) ProtoMessage() {} + +func (x *RevokeDelegatedIdentityCredentialResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[98] if x != nil { - return x.Material + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return nil + return mi.MessageOf(x) } -func (x *StoredProviderCredentialRefreshState) GetSecretMaterialKeys() []string { - if x != nil { - return x.SecretMaterialKeys - } - return nil +// Deprecated: Use RevokeDelegatedIdentityCredentialResponse.ProtoReflect.Descriptor instead. +func (*RevokeDelegatedIdentityCredentialResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{98} } -func (x *StoredProviderCredentialRefreshState) GetExpiresAtMs() int64 { +func (x *RevokeDelegatedIdentityCredentialResponse) GetRevoked() bool { if x != nil { - return x.ExpiresAtMs + return x.Revoked } - return 0 + return false } -func (x *StoredProviderCredentialRefreshState) GetNextRefreshAtMs() int64 { +func (x *RevokeDelegatedIdentityCredentialResponse) GetRevokedAtMs() int64 { if x != nil { - return x.NextRefreshAtMs + return x.RevokedAtMs } return 0 } -func (x *StoredProviderCredentialRefreshState) GetLastRefreshAtMs() int64 { +func (x *RevokeDelegatedIdentityCredentialResponse) GetResourceVersion() uint64 { if x != nil { - return x.LastRefreshAtMs + return x.ResourceVersion } return 0 } -func (x *StoredProviderCredentialRefreshState) GetStatus() string { - if x != nil { - return x.Status - } - return "" +type DeleteDelegatedIdentityCredentialRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + ExpectedResourceVersion uint64 `protobuf:"varint,2,opt,name=expected_resource_version,json=expectedResourceVersion,proto3" json:"expected_resource_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *StoredProviderCredentialRefreshState) GetLastError() string { - if x != nil { - return x.LastError - } - return "" +func (x *DeleteDelegatedIdentityCredentialRequest) Reset() { + *x = DeleteDelegatedIdentityCredentialRequest{} + mi := &file_openshell_proto_msgTypes[99] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *StoredProviderCredentialRefreshState) GetTokenUrl() string { +func (x *DeleteDelegatedIdentityCredentialRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteDelegatedIdentityCredentialRequest) ProtoMessage() {} + +func (x *DeleteDelegatedIdentityCredentialRequest) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[99] if x != nil { - return x.TokenUrl + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return "" + return mi.MessageOf(x) } -func (x *StoredProviderCredentialRefreshState) GetScopes() []string { +// Deprecated: Use DeleteDelegatedIdentityCredentialRequest.ProtoReflect.Descriptor instead. +func (*DeleteDelegatedIdentityCredentialRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{99} +} + +func (x *DeleteDelegatedIdentityCredentialRequest) GetId() string { if x != nil { - return x.Scopes + return x.Id } - return nil + return "" } -func (x *StoredProviderCredentialRefreshState) GetRefreshBeforeSeconds() int64 { +func (x *DeleteDelegatedIdentityCredentialRequest) GetExpectedResourceVersion() uint64 { if x != nil { - return x.RefreshBeforeSeconds + return x.ExpectedResourceVersion } return 0 } -func (x *StoredProviderCredentialRefreshState) GetMaxLifetimeSeconds() int64 { +type DeleteDelegatedIdentityCredentialResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Deleted bool `protobuf:"varint,1,opt,name=deleted,proto3" json:"deleted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteDelegatedIdentityCredentialResponse) Reset() { + *x = DeleteDelegatedIdentityCredentialResponse{} + mi := &file_openshell_proto_msgTypes[100] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteDelegatedIdentityCredentialResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteDelegatedIdentityCredentialResponse) ProtoMessage() {} + +func (x *DeleteDelegatedIdentityCredentialResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[100] if x != nil { - return x.MaxLifetimeSeconds + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return 0 + return mi.MessageOf(x) } -func (x *StoredProviderCredentialRefreshState) GetAdditionalOutputKeys() map[string]string { +// Deprecated: Use DeleteDelegatedIdentityCredentialResponse.ProtoReflect.Descriptor instead. +func (*DeleteDelegatedIdentityCredentialResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{100} +} + +func (x *DeleteDelegatedIdentityCredentialResponse) GetDeleted() bool { if x != nil { - return x.AdditionalOutputKeys + return x.Deleted } - return nil + return false } type GetProviderRefreshStatusRequest struct { @@ -6019,7 +7369,7 @@ type GetProviderRefreshStatusRequest struct { func (x *GetProviderRefreshStatusRequest) Reset() { *x = GetProviderRefreshStatusRequest{} - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6031,7 +7381,7 @@ func (x *GetProviderRefreshStatusRequest) String() string { func (*GetProviderRefreshStatusRequest) ProtoMessage() {} func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[81] + mi := &file_openshell_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6044,7 +7394,7 @@ func (x *GetProviderRefreshStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusRequest.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{81} + return file_openshell_proto_rawDescGZIP(), []int{101} } func (x *GetProviderRefreshStatusRequest) GetProvider() string { @@ -6077,7 +7427,7 @@ type GetProviderRefreshStatusResponse struct { func (x *GetProviderRefreshStatusResponse) Reset() { *x = GetProviderRefreshStatusResponse{} - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6089,7 +7439,7 @@ func (x *GetProviderRefreshStatusResponse) String() string { func (*GetProviderRefreshStatusResponse) ProtoMessage() {} func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[82] + mi := &file_openshell_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6102,7 +7452,7 @@ func (x *GetProviderRefreshStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderRefreshStatusResponse.ProtoReflect.Descriptor instead. func (*GetProviderRefreshStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{82} + return file_openshell_proto_rawDescGZIP(), []int{102} } func (x *GetProviderRefreshStatusResponse) GetCredentials() []*ProviderCredentialRefreshStatus { @@ -6128,7 +7478,7 @@ type ConfigureProviderRefreshRequest struct { func (x *ConfigureProviderRefreshRequest) Reset() { *x = ConfigureProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6140,7 +7490,7 @@ func (x *ConfigureProviderRefreshRequest) String() string { func (*ConfigureProviderRefreshRequest) ProtoMessage() {} func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[83] + mi := &file_openshell_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6153,7 +7503,7 @@ func (x *ConfigureProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{83} + return file_openshell_proto_rawDescGZIP(), []int{103} } func (x *ConfigureProviderRefreshRequest) GetProvider() string { @@ -6214,7 +7564,7 @@ type ConfigureProviderRefreshResponse struct { func (x *ConfigureProviderRefreshResponse) Reset() { *x = ConfigureProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6226,7 +7576,7 @@ func (x *ConfigureProviderRefreshResponse) String() string { func (*ConfigureProviderRefreshResponse) ProtoMessage() {} func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[84] + mi := &file_openshell_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6239,7 +7589,7 @@ func (x *ConfigureProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*ConfigureProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{84} + return file_openshell_proto_rawDescGZIP(), []int{104} } func (x *ConfigureProviderRefreshResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -6261,7 +7611,7 @@ type RotateProviderCredentialRequest struct { func (x *RotateProviderCredentialRequest) Reset() { *x = RotateProviderCredentialRequest{} - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6273,7 +7623,7 @@ func (x *RotateProviderCredentialRequest) String() string { func (*RotateProviderCredentialRequest) ProtoMessage() {} func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[85] + mi := &file_openshell_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6286,7 +7636,7 @@ func (x *RotateProviderCredentialRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialRequest.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{85} + return file_openshell_proto_rawDescGZIP(), []int{105} } func (x *RotateProviderCredentialRequest) GetProvider() string { @@ -6319,7 +7669,7 @@ type RotateProviderCredentialResponse struct { func (x *RotateProviderCredentialResponse) Reset() { *x = RotateProviderCredentialResponse{} - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6331,7 +7681,7 @@ func (x *RotateProviderCredentialResponse) String() string { func (*RotateProviderCredentialResponse) ProtoMessage() {} func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[86] + mi := &file_openshell_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6344,7 +7694,7 @@ func (x *RotateProviderCredentialResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RotateProviderCredentialResponse.ProtoReflect.Descriptor instead. func (*RotateProviderCredentialResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{86} + return file_openshell_proto_rawDescGZIP(), []int{106} } func (x *RotateProviderCredentialResponse) GetStatus() *ProviderCredentialRefreshStatus { @@ -6366,7 +7716,7 @@ type DeleteProviderRefreshRequest struct { func (x *DeleteProviderRefreshRequest) Reset() { *x = DeleteProviderRefreshRequest{} - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6378,7 +7728,7 @@ func (x *DeleteProviderRefreshRequest) String() string { func (*DeleteProviderRefreshRequest) ProtoMessage() {} func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[87] + mi := &file_openshell_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6391,7 +7741,7 @@ func (x *DeleteProviderRefreshRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{87} + return file_openshell_proto_rawDescGZIP(), []int{107} } func (x *DeleteProviderRefreshRequest) GetProvider() string { @@ -6424,7 +7774,7 @@ type DeleteProviderRefreshResponse struct { func (x *DeleteProviderRefreshResponse) Reset() { *x = DeleteProviderRefreshResponse{} - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6436,7 +7786,7 @@ func (x *DeleteProviderRefreshResponse) String() string { func (*DeleteProviderRefreshResponse) ProtoMessage() {} func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[88] + mi := &file_openshell_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6449,7 +7799,7 @@ func (x *DeleteProviderRefreshResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderRefreshResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderRefreshResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{88} + return file_openshell_proto_rawDescGZIP(), []int{108} } func (x *DeleteProviderRefreshResponse) GetDeleted() bool { @@ -6489,7 +7839,7 @@ type ProviderProfile struct { func (x *ProviderProfile) Reset() { *x = ProviderProfile{} - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6501,7 +7851,7 @@ func (x *ProviderProfile) String() string { func (*ProviderProfile) ProtoMessage() {} func (x *ProviderProfile) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[89] + mi := &file_openshell_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6514,7 +7864,7 @@ func (x *ProviderProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfile.ProtoReflect.Descriptor instead. func (*ProviderProfile) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{89} + return file_openshell_proto_rawDescGZIP(), []int{109} } func (x *ProviderProfile) GetId() string { @@ -6619,7 +7969,7 @@ type StoredProviderProfile struct { func (x *StoredProviderProfile) Reset() { *x = StoredProviderProfile{} - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6631,7 +7981,7 @@ func (x *StoredProviderProfile) String() string { func (*StoredProviderProfile) ProtoMessage() {} func (x *StoredProviderProfile) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[90] + mi := &file_openshell_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6644,7 +7994,7 @@ func (x *StoredProviderProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredProviderProfile.ProtoReflect.Descriptor instead. func (*StoredProviderProfile) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{90} + return file_openshell_proto_rawDescGZIP(), []int{110} } func (x *StoredProviderProfile) GetMetadata() *datamodelv1.ObjectMeta { @@ -6671,7 +8021,7 @@ type ProviderProfileResponse struct { func (x *ProviderProfileResponse) Reset() { *x = ProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6683,7 +8033,7 @@ func (x *ProviderProfileResponse) String() string { func (*ProviderProfileResponse) ProtoMessage() {} func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[91] + mi := &file_openshell_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6696,7 +8046,7 @@ func (x *ProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ProviderProfileResponse.ProtoReflect.Descriptor instead. func (*ProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{91} + return file_openshell_proto_rawDescGZIP(), []int{111} } func (x *ProviderProfileResponse) GetProfile() *ProviderProfile { @@ -6716,7 +8066,7 @@ type ListProviderProfilesResponse struct { func (x *ListProviderProfilesResponse) Reset() { *x = ListProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6728,7 +8078,7 @@ func (x *ListProviderProfilesResponse) String() string { func (*ListProviderProfilesResponse) ProtoMessage() {} func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[92] + mi := &file_openshell_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6741,7 +8091,7 @@ func (x *ListProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ListProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{92} + return file_openshell_proto_rawDescGZIP(), []int{112} } func (x *ListProviderProfilesResponse) GetProfiles() []*ProviderProfile { @@ -6764,7 +8114,7 @@ type ImportProviderProfilesRequest struct { func (x *ImportProviderProfilesRequest) Reset() { *x = ImportProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6776,7 +8126,7 @@ func (x *ImportProviderProfilesRequest) String() string { func (*ImportProviderProfilesRequest) ProtoMessage() {} func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[93] + mi := &file_openshell_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6789,7 +8139,7 @@ func (x *ImportProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{93} + return file_openshell_proto_rawDescGZIP(), []int{113} } func (x *ImportProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { @@ -6818,7 +8168,7 @@ type ImportProviderProfilesResponse struct { func (x *ImportProviderProfilesResponse) Reset() { *x = ImportProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6830,7 +8180,7 @@ func (x *ImportProviderProfilesResponse) String() string { func (*ImportProviderProfilesResponse) ProtoMessage() {} func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[94] + mi := &file_openshell_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6843,7 +8193,7 @@ func (x *ImportProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*ImportProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{94} + return file_openshell_proto_rawDescGZIP(), []int{114} } func (x *ImportProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -6887,7 +8237,7 @@ type UpdateProviderProfilesRequest struct { func (x *UpdateProviderProfilesRequest) Reset() { *x = UpdateProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6899,7 +8249,7 @@ func (x *UpdateProviderProfilesRequest) String() string { func (*UpdateProviderProfilesRequest) ProtoMessage() {} func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[95] + mi := &file_openshell_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6912,7 +8262,7 @@ func (x *UpdateProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{95} + return file_openshell_proto_rawDescGZIP(), []int{115} } func (x *UpdateProviderProfilesRequest) GetProfile() *ProviderProfileImportItem { @@ -6955,7 +8305,7 @@ type UpdateProviderProfilesResponse struct { func (x *UpdateProviderProfilesResponse) Reset() { *x = UpdateProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6967,7 +8317,7 @@ func (x *UpdateProviderProfilesResponse) String() string { func (*UpdateProviderProfilesResponse) ProtoMessage() {} func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[96] + mi := &file_openshell_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6980,7 +8330,7 @@ func (x *UpdateProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*UpdateProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{96} + return file_openshell_proto_rawDescGZIP(), []int{116} } func (x *UpdateProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -7017,7 +8367,7 @@ type LintProviderProfilesRequest struct { func (x *LintProviderProfilesRequest) Reset() { *x = LintProviderProfilesRequest{} - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7029,7 +8379,7 @@ func (x *LintProviderProfilesRequest) String() string { func (*LintProviderProfilesRequest) ProtoMessage() {} func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[97] + mi := &file_openshell_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7042,7 +8392,7 @@ func (x *LintProviderProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesRequest.ProtoReflect.Descriptor instead. func (*LintProviderProfilesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{97} + return file_openshell_proto_rawDescGZIP(), []int{117} } func (x *LintProviderProfilesRequest) GetProfiles() []*ProviderProfileImportItem { @@ -7070,7 +8420,7 @@ type LintProviderProfilesResponse struct { func (x *LintProviderProfilesResponse) Reset() { *x = LintProviderProfilesResponse{} - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7082,7 +8432,7 @@ func (x *LintProviderProfilesResponse) String() string { func (*LintProviderProfilesResponse) ProtoMessage() {} func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[98] + mi := &file_openshell_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7095,7 +8445,7 @@ func (x *LintProviderProfilesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LintProviderProfilesResponse.ProtoReflect.Descriptor instead. func (*LintProviderProfilesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{98} + return file_openshell_proto_rawDescGZIP(), []int{118} } func (x *LintProviderProfilesResponse) GetDiagnostics() []*ProviderProfileDiagnostic { @@ -7122,7 +8472,7 @@ type DeleteProviderResponse struct { func (x *DeleteProviderResponse) Reset() { *x = DeleteProviderResponse{} - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7134,7 +8484,7 @@ func (x *DeleteProviderResponse) String() string { func (*DeleteProviderResponse) ProtoMessage() {} func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[99] + mi := &file_openshell_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7147,7 +8497,7 @@ func (x *DeleteProviderResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{99} + return file_openshell_proto_rawDescGZIP(), []int{119} } func (x *DeleteProviderResponse) GetDeleted() bool { @@ -7170,7 +8520,7 @@ type DeleteProviderProfileRequest struct { func (x *DeleteProviderProfileRequest) Reset() { *x = DeleteProviderProfileRequest{} - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7182,7 +8532,7 @@ func (x *DeleteProviderProfileRequest) String() string { func (*DeleteProviderProfileRequest) ProtoMessage() {} func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[100] + mi := &file_openshell_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7195,7 +8545,7 @@ func (x *DeleteProviderProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileRequest.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{100} + return file_openshell_proto_rawDescGZIP(), []int{120} } func (x *DeleteProviderProfileRequest) GetId() string { @@ -7222,7 +8572,7 @@ type DeleteProviderProfileResponse struct { func (x *DeleteProviderProfileResponse) Reset() { *x = DeleteProviderProfileResponse{} - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7234,7 +8584,7 @@ func (x *DeleteProviderProfileResponse) String() string { func (*DeleteProviderProfileResponse) ProtoMessage() {} func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[101] + mi := &file_openshell_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7247,7 +8597,7 @@ func (x *DeleteProviderProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteProviderProfileResponse.ProtoReflect.Descriptor instead. func (*DeleteProviderProfileResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{101} + return file_openshell_proto_rawDescGZIP(), []int{121} } func (x *DeleteProviderProfileResponse) GetDeleted() bool { @@ -7272,7 +8622,7 @@ type GetSandboxProviderEnvironmentRequest struct { func (x *GetSandboxProviderEnvironmentRequest) Reset() { *x = GetSandboxProviderEnvironmentRequest{} - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7284,7 +8634,7 @@ func (x *GetSandboxProviderEnvironmentRequest) String() string { func (*GetSandboxProviderEnvironmentRequest) ProtoMessage() {} func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[102] + mi := &file_openshell_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7297,7 +8647,7 @@ func (x *GetSandboxProviderEnvironmentRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use GetSandboxProviderEnvironmentRequest.ProtoReflect.Descriptor instead. func (*GetSandboxProviderEnvironmentRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{102} + return file_openshell_proto_rawDescGZIP(), []int{122} } func (x *GetSandboxProviderEnvironmentRequest) GetSandboxId() string { @@ -7326,7 +8676,7 @@ type StaticCredentialEndpointBinding struct { func (x *StaticCredentialEndpointBinding) Reset() { *x = StaticCredentialEndpointBinding{} - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7338,7 +8688,7 @@ func (x *StaticCredentialEndpointBinding) String() string { func (*StaticCredentialEndpointBinding) ProtoMessage() {} func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[103] + mi := &file_openshell_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7351,7 +8701,7 @@ func (x *StaticCredentialEndpointBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticCredentialEndpointBinding.ProtoReflect.Descriptor instead. func (*StaticCredentialEndpointBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{103} + return file_openshell_proto_rawDescGZIP(), []int{123} } func (x *StaticCredentialEndpointBinding) GetHost() string { @@ -7389,7 +8739,7 @@ type StaticCredentialBinding struct { func (x *StaticCredentialBinding) Reset() { *x = StaticCredentialBinding{} - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7401,7 +8751,163 @@ func (x *StaticCredentialBinding) String() string { func (*StaticCredentialBinding) ProtoMessage() {} func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[104] + mi := &file_openshell_proto_msgTypes[124] + 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 StaticCredentialBinding.ProtoReflect.Descriptor instead. +func (*StaticCredentialBinding) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{124} +} + +func (x *StaticCredentialBinding) GetEndpoints() []*StaticCredentialEndpointBinding { + if x != nil { + return x.Endpoints + } + return nil +} + +func (x *StaticCredentialBinding) GetCredentialIdentity() string { + if x != nil { + return x.CredentialIdentity + } + return "" +} + +// Get sandbox provider environment response. +type GetSandboxProviderEnvironmentResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Provider credential environment variables. + Environment map[string]string `protobuf:"bytes,1,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Fingerprint for the provider credential inputs that produced environment. + ProviderEnvRevision uint64 `protobuf:"varint,2,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` + // Expiration timestamps for returned environment variables. + CredentialExpiresAtMs map[string]int64 `protobuf:"bytes,3,rep,name=credential_expires_at_ms,json=credentialExpiresAtMs,proto3" json:"credential_expires_at_ms,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` + // Dynamic credentials that require token grants or other runtime injection. + // Maps endpoint-bound provider metadata to credential metadata. + // Supervisor uses this to inject Authorization headers for token grant credentials. + DynamicCredentials map[string]*ProviderProfileCredential `protobuf:"bytes,4,rep,name=dynamic_credentials,json=dynamicCredentials,proto3" json:"dynamic_credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Endpoint allowlists for static credential environment variables. Metadata + // can be incomplete when a provider profile is invalid; capable supervisors + // reject all static material in that snapshot while preserving independently + // endpoint-bound dynamic credentials. + StaticCredentialBindings map[string]*StaticCredentialBinding `protobuf:"bytes,5,rep,name=static_credential_bindings,json=staticCredentialBindings,proto3" json:"static_credential_bindings,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Environment variables that contain provider configuration rather than + // credentials and therefore do not require endpoint-scoped resolution. + NonSecretEnvironmentKeys []string `protobuf:"bytes,6,rep,name=non_secret_environment_keys,json=nonSecretEnvironmentKeys,proto3" json:"non_secret_environment_keys,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetSandboxProviderEnvironmentResponse) Reset() { + *x = GetSandboxProviderEnvironmentResponse{} + mi := &file_openshell_proto_msgTypes[125] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetSandboxProviderEnvironmentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetSandboxProviderEnvironmentResponse) ProtoMessage() {} + +func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[125] + 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 GetSandboxProviderEnvironmentResponse.ProtoReflect.Descriptor instead. +func (*GetSandboxProviderEnvironmentResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{125} +} + +func (x *GetSandboxProviderEnvironmentResponse) GetEnvironment() map[string]string { + if x != nil { + return x.Environment + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetProviderEnvRevision() uint64 { + if x != nil { + return x.ProviderEnvRevision + } + return 0 +} + +func (x *GetSandboxProviderEnvironmentResponse) GetCredentialExpiresAtMs() map[string]int64 { + if x != nil { + return x.CredentialExpiresAtMs + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetDynamicCredentials() map[string]*ProviderProfileCredential { + if x != nil { + return x.DynamicCredentials + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetStaticCredentialBindings() map[string]*StaticCredentialBinding { + if x != nil { + return x.StaticCredentialBindings + } + return nil +} + +func (x *GetSandboxProviderEnvironmentResponse) GetNonSecretEnvironmentKeys() []string { + if x != nil { + return x.NonSecretEnvironmentKeys + } + 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[126] + 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[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7409,68 +8915,66 @@ func (x *StaticCredentialBinding) ProtoReflect() protoreflect.Message { } return ms } - return mi.MessageOf(x) + return mi.MessageOf(x) +} + +// Deprecated: Use ExchangeProviderSubjectTokenRequest.ProtoReflect.Descriptor instead. +func (*ExchangeProviderSubjectTokenRequest) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{126} +} + +func (x *ExchangeProviderSubjectTokenRequest) GetSandboxId() string { + if x != nil { + return x.SandboxId + } + return "" } -// Deprecated: Use StaticCredentialBinding.ProtoReflect.Descriptor instead. -func (*StaticCredentialBinding) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{104} +func (x *ExchangeProviderSubjectTokenRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" } -func (x *StaticCredentialBinding) GetEndpoints() []*StaticCredentialEndpointBinding { +func (x *ExchangeProviderSubjectTokenRequest) GetCredentialKey() string { if x != nil { - return x.Endpoints + return x.CredentialKey } - return nil + return "" } -func (x *StaticCredentialBinding) GetCredentialIdentity() string { +func (x *ExchangeProviderSubjectTokenRequest) GetSupervisorJwtSvid() string { if x != nil { - return x.CredentialIdentity + return x.SupervisorJwtSvid } return "" } -// Get sandbox provider environment response. -type GetSandboxProviderEnvironmentResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Provider credential environment variables. - Environment map[string]string `protobuf:"bytes,1,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Fingerprint for the provider credential inputs that produced environment. - ProviderEnvRevision uint64 `protobuf:"varint,2,opt,name=provider_env_revision,json=providerEnvRevision,proto3" json:"provider_env_revision,omitempty"` - // Expiration timestamps for returned environment variables. - CredentialExpiresAtMs map[string]int64 `protobuf:"bytes,3,rep,name=credential_expires_at_ms,json=credentialExpiresAtMs,proto3" json:"credential_expires_at_ms,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` - // Dynamic credentials that require token grants or other runtime injection. - // Maps endpoint-bound provider metadata to credential metadata. - // Supervisor uses this to inject Authorization headers for token grant credentials. - DynamicCredentials map[string]*ProviderProfileCredential `protobuf:"bytes,4,rep,name=dynamic_credentials,json=dynamicCredentials,proto3" json:"dynamic_credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Endpoint allowlists for static credential environment variables. Metadata - // can be incomplete when a provider profile is invalid; capable supervisors - // reject all static material in that snapshot while preserving independently - // endpoint-bound dynamic credentials. - StaticCredentialBindings map[string]*StaticCredentialBinding `protobuf:"bytes,5,rep,name=static_credential_bindings,json=staticCredentialBindings,proto3" json:"static_credential_bindings,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // Environment variables that contain provider configuration rather than - // credentials and therefore do not require endpoint-scoped resolution. - NonSecretEnvironmentKeys []string `protobuf:"bytes,6,rep,name=non_secret_environment_keys,json=nonSecretEnvironmentKeys,proto3" json:"non_secret_environment_keys,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +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 *GetSandboxProviderEnvironmentResponse) Reset() { - *x = GetSandboxProviderEnvironmentResponse{} - mi := &file_openshell_proto_msgTypes[105] +func (x *ExchangeProviderSubjectTokenResponse) Reset() { + *x = ExchangeProviderSubjectTokenResponse{} + mi := &file_openshell_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *GetSandboxProviderEnvironmentResponse) String() string { +func (x *ExchangeProviderSubjectTokenResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GetSandboxProviderEnvironmentResponse) ProtoMessage() {} +func (*ExchangeProviderSubjectTokenResponse) ProtoMessage() {} -func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[105] +func (x *ExchangeProviderSubjectTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_openshell_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7481,51 +8985,30 @@ func (x *GetSandboxProviderEnvironmentResponse) ProtoReflect() protoreflect.Mess return mi.MessageOf(x) } -// Deprecated: Use GetSandboxProviderEnvironmentResponse.ProtoReflect.Descriptor instead. -func (*GetSandboxProviderEnvironmentResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{105} +// Deprecated: Use ExchangeProviderSubjectTokenResponse.ProtoReflect.Descriptor instead. +func (*ExchangeProviderSubjectTokenResponse) Descriptor() ([]byte, []int) { + return file_openshell_proto_rawDescGZIP(), []int{127} } -func (x *GetSandboxProviderEnvironmentResponse) GetEnvironment() map[string]string { +func (x *ExchangeProviderSubjectTokenResponse) GetAccessToken() string { if x != nil { - return x.Environment + return x.AccessToken } - return nil + return "" } -func (x *GetSandboxProviderEnvironmentResponse) GetProviderEnvRevision() uint64 { +func (x *ExchangeProviderSubjectTokenResponse) GetExpiresIn() int64 { if x != nil { - return x.ProviderEnvRevision + return x.ExpiresIn } return 0 } -func (x *GetSandboxProviderEnvironmentResponse) GetCredentialExpiresAtMs() map[string]int64 { - if x != nil { - return x.CredentialExpiresAtMs - } - return nil -} - -func (x *GetSandboxProviderEnvironmentResponse) GetDynamicCredentials() map[string]*ProviderProfileCredential { - if x != nil { - return x.DynamicCredentials - } - return nil -} - -func (x *GetSandboxProviderEnvironmentResponse) GetStaticCredentialBindings() map[string]*StaticCredentialBinding { - if x != nil { - return x.StaticCredentialBindings - } - return nil -} - -func (x *GetSandboxProviderEnvironmentResponse) GetNonSecretEnvironmentKeys() []string { +func (x *ExchangeProviderSubjectTokenResponse) GetTokenType() string { if x != nil { - return x.NonSecretEnvironmentKeys + return x.TokenType } - return nil + return "" } // Update sandbox policy request. @@ -7575,7 +9058,7 @@ type UpdateConfigRequest struct { func (x *UpdateConfigRequest) Reset() { *x = UpdateConfigRequest{} - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7587,7 +9070,7 @@ func (x *UpdateConfigRequest) String() string { func (*UpdateConfigRequest) ProtoMessage() {} func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[106] + mi := &file_openshell_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7600,7 +9083,7 @@ func (x *UpdateConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigRequest.ProtoReflect.Descriptor instead. func (*UpdateConfigRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{106} + return file_openshell_proto_rawDescGZIP(), []int{128} } func (x *UpdateConfigRequest) GetName() string { @@ -7690,7 +9173,7 @@ type PolicyMergeOperation struct { func (x *PolicyMergeOperation) Reset() { *x = PolicyMergeOperation{} - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7702,7 +9185,7 @@ func (x *PolicyMergeOperation) String() string { func (*PolicyMergeOperation) ProtoMessage() {} func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[107] + mi := &file_openshell_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7715,7 +9198,7 @@ func (x *PolicyMergeOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyMergeOperation.ProtoReflect.Descriptor instead. func (*PolicyMergeOperation) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{107} + return file_openshell_proto_rawDescGZIP(), []int{129} } func (x *PolicyMergeOperation) GetOperation() isPolicyMergeOperation_Operation { @@ -7829,7 +9312,7 @@ type AddNetworkRule struct { func (x *AddNetworkRule) Reset() { *x = AddNetworkRule{} - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7841,7 +9324,7 @@ func (x *AddNetworkRule) String() string { func (*AddNetworkRule) ProtoMessage() {} func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[108] + mi := &file_openshell_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7854,7 +9337,7 @@ func (x *AddNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use AddNetworkRule.ProtoReflect.Descriptor instead. func (*AddNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{108} + return file_openshell_proto_rawDescGZIP(), []int{130} } func (x *AddNetworkRule) GetRuleName() string { @@ -7882,7 +9365,7 @@ type RemoveNetworkEndpoint struct { func (x *RemoveNetworkEndpoint) Reset() { *x = RemoveNetworkEndpoint{} - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7894,7 +9377,7 @@ func (x *RemoveNetworkEndpoint) String() string { func (*RemoveNetworkEndpoint) ProtoMessage() {} func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[109] + mi := &file_openshell_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7907,7 +9390,7 @@ func (x *RemoveNetworkEndpoint) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkEndpoint.ProtoReflect.Descriptor instead. func (*RemoveNetworkEndpoint) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{109} + return file_openshell_proto_rawDescGZIP(), []int{131} } func (x *RemoveNetworkEndpoint) GetRuleName() string { @@ -7940,7 +9423,7 @@ type RemoveNetworkRule struct { func (x *RemoveNetworkRule) Reset() { *x = RemoveNetworkRule{} - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7952,7 +9435,7 @@ func (x *RemoveNetworkRule) String() string { func (*RemoveNetworkRule) ProtoMessage() {} func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[110] + mi := &file_openshell_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7965,7 +9448,7 @@ func (x *RemoveNetworkRule) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkRule.ProtoReflect.Descriptor instead. func (*RemoveNetworkRule) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{110} + return file_openshell_proto_rawDescGZIP(), []int{132} } func (x *RemoveNetworkRule) GetRuleName() string { @@ -7986,7 +9469,7 @@ type AddDenyRules struct { func (x *AddDenyRules) Reset() { *x = AddDenyRules{} - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7998,7 +9481,7 @@ func (x *AddDenyRules) String() string { func (*AddDenyRules) ProtoMessage() {} func (x *AddDenyRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[111] + mi := &file_openshell_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8011,7 +9494,7 @@ func (x *AddDenyRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddDenyRules.ProtoReflect.Descriptor instead. func (*AddDenyRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{111} + return file_openshell_proto_rawDescGZIP(), []int{133} } func (x *AddDenyRules) GetHost() string { @@ -8046,7 +9529,7 @@ type AddAllowRules struct { func (x *AddAllowRules) Reset() { *x = AddAllowRules{} - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8058,7 +9541,7 @@ func (x *AddAllowRules) String() string { func (*AddAllowRules) ProtoMessage() {} func (x *AddAllowRules) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[112] + mi := &file_openshell_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8071,7 +9554,7 @@ func (x *AddAllowRules) ProtoReflect() protoreflect.Message { // Deprecated: Use AddAllowRules.ProtoReflect.Descriptor instead. func (*AddAllowRules) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{112} + return file_openshell_proto_rawDescGZIP(), []int{134} } func (x *AddAllowRules) GetHost() string { @@ -8105,7 +9588,7 @@ type RemoveNetworkBinary struct { func (x *RemoveNetworkBinary) Reset() { *x = RemoveNetworkBinary{} - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8117,7 +9600,7 @@ func (x *RemoveNetworkBinary) String() string { func (*RemoveNetworkBinary) ProtoMessage() {} func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[113] + mi := &file_openshell_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8130,7 +9613,7 @@ func (x *RemoveNetworkBinary) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveNetworkBinary.ProtoReflect.Descriptor instead. func (*RemoveNetworkBinary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{113} + return file_openshell_proto_rawDescGZIP(), []int{135} } func (x *RemoveNetworkBinary) GetRuleName() string { @@ -8166,7 +9649,7 @@ type UpdateConfigResponse struct { func (x *UpdateConfigResponse) Reset() { *x = UpdateConfigResponse{} - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[136] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8178,7 +9661,7 @@ func (x *UpdateConfigResponse) String() string { func (*UpdateConfigResponse) ProtoMessage() {} func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[114] + mi := &file_openshell_proto_msgTypes[136] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8191,7 +9674,7 @@ func (x *UpdateConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConfigResponse.ProtoReflect.Descriptor instead. func (*UpdateConfigResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{114} + return file_openshell_proto_rawDescGZIP(), []int{136} } func (x *UpdateConfigResponse) GetVersion() uint32 { @@ -8246,7 +9729,7 @@ type GetSandboxPolicyStatusRequest struct { func (x *GetSandboxPolicyStatusRequest) Reset() { *x = GetSandboxPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[137] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8258,7 +9741,7 @@ func (x *GetSandboxPolicyStatusRequest) String() string { func (*GetSandboxPolicyStatusRequest) ProtoMessage() {} func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[115] + mi := &file_openshell_proto_msgTypes[137] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8271,7 +9754,7 @@ func (x *GetSandboxPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{115} + return file_openshell_proto_rawDescGZIP(), []int{137} } func (x *GetSandboxPolicyStatusRequest) GetName() string { @@ -8315,7 +9798,7 @@ type GetSandboxPolicyStatusResponse struct { func (x *GetSandboxPolicyStatusResponse) Reset() { *x = GetSandboxPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[138] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8327,7 +9810,7 @@ func (x *GetSandboxPolicyStatusResponse) String() string { func (*GetSandboxPolicyStatusResponse) ProtoMessage() {} func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[116] + mi := &file_openshell_proto_msgTypes[138] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8340,7 +9823,7 @@ func (x *GetSandboxPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*GetSandboxPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{116} + return file_openshell_proto_rawDescGZIP(), []int{138} } func (x *GetSandboxPolicyStatusResponse) GetRevision() *SandboxPolicyRevision { @@ -8374,7 +9857,7 @@ type ListSandboxPoliciesRequest struct { func (x *ListSandboxPoliciesRequest) Reset() { *x = ListSandboxPoliciesRequest{} - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[139] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8386,7 +9869,7 @@ func (x *ListSandboxPoliciesRequest) String() string { func (*ListSandboxPoliciesRequest) ProtoMessage() {} func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[117] + mi := &file_openshell_proto_msgTypes[139] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8399,7 +9882,7 @@ func (x *ListSandboxPoliciesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesRequest.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{117} + return file_openshell_proto_rawDescGZIP(), []int{139} } func (x *ListSandboxPoliciesRequest) GetName() string { @@ -8447,7 +9930,7 @@ type ListSandboxPoliciesResponse struct { func (x *ListSandboxPoliciesResponse) Reset() { *x = ListSandboxPoliciesResponse{} - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[140] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8459,7 +9942,7 @@ func (x *ListSandboxPoliciesResponse) String() string { func (*ListSandboxPoliciesResponse) ProtoMessage() {} func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[118] + mi := &file_openshell_proto_msgTypes[140] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8472,7 +9955,7 @@ func (x *ListSandboxPoliciesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSandboxPoliciesResponse.ProtoReflect.Descriptor instead. func (*ListSandboxPoliciesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{118} + return file_openshell_proto_rawDescGZIP(), []int{140} } func (x *ListSandboxPoliciesResponse) GetRevisions() []*SandboxPolicyRevision { @@ -8499,7 +9982,7 @@ type ReportPolicyStatusRequest struct { func (x *ReportPolicyStatusRequest) Reset() { *x = ReportPolicyStatusRequest{} - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8511,7 +9994,7 @@ func (x *ReportPolicyStatusRequest) String() string { func (*ReportPolicyStatusRequest) ProtoMessage() {} func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[119] + mi := &file_openshell_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8524,7 +10007,7 @@ func (x *ReportPolicyStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusRequest.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{119} + return file_openshell_proto_rawDescGZIP(), []int{141} } func (x *ReportPolicyStatusRequest) GetSandboxId() string { @@ -8564,7 +10047,7 @@ type ReportPolicyStatusResponse struct { func (x *ReportPolicyStatusResponse) Reset() { *x = ReportPolicyStatusResponse{} - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8576,7 +10059,7 @@ func (x *ReportPolicyStatusResponse) String() string { func (*ReportPolicyStatusResponse) ProtoMessage() {} func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[120] + mi := &file_openshell_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8589,7 +10072,7 @@ func (x *ReportPolicyStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ReportPolicyStatusResponse.ProtoReflect.Descriptor instead. func (*ReportPolicyStatusResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{120} + return file_openshell_proto_rawDescGZIP(), []int{142} } // A versioned policy revision with metadata. @@ -8617,7 +10100,7 @@ type SandboxPolicyRevision struct { func (x *SandboxPolicyRevision) Reset() { *x = SandboxPolicyRevision{} - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[143] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8629,7 +10112,7 @@ func (x *SandboxPolicyRevision) String() string { func (*SandboxPolicyRevision) ProtoMessage() {} func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[121] + mi := &file_openshell_proto_msgTypes[143] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8642,7 +10125,7 @@ func (x *SandboxPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use SandboxPolicyRevision.ProtoReflect.Descriptor instead. func (*SandboxPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{121} + return file_openshell_proto_rawDescGZIP(), []int{143} } func (x *SandboxPolicyRevision) GetVersion() uint32 { @@ -8722,7 +10205,7 @@ type GetSandboxLogsRequest struct { func (x *GetSandboxLogsRequest) Reset() { *x = GetSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8734,7 +10217,7 @@ func (x *GetSandboxLogsRequest) String() string { func (*GetSandboxLogsRequest) ProtoMessage() {} func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[122] + mi := &file_openshell_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8747,7 +10230,7 @@ func (x *GetSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*GetSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{122} + return file_openshell_proto_rawDescGZIP(), []int{144} } func (x *GetSandboxLogsRequest) GetSandboxId() string { @@ -8805,7 +10288,7 @@ type PushSandboxLogsRequest struct { func (x *PushSandboxLogsRequest) Reset() { *x = PushSandboxLogsRequest{} - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8817,7 +10300,7 @@ func (x *PushSandboxLogsRequest) String() string { func (*PushSandboxLogsRequest) ProtoMessage() {} func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[123] + mi := &file_openshell_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8830,7 +10313,7 @@ func (x *PushSandboxLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsRequest.ProtoReflect.Descriptor instead. func (*PushSandboxLogsRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{123} + return file_openshell_proto_rawDescGZIP(), []int{145} } func (x *PushSandboxLogsRequest) GetSandboxId() string { @@ -8856,7 +10339,7 @@ type PushSandboxLogsResponse struct { func (x *PushSandboxLogsResponse) Reset() { *x = PushSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8868,7 +10351,7 @@ func (x *PushSandboxLogsResponse) String() string { func (*PushSandboxLogsResponse) ProtoMessage() {} func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[124] + mi := &file_openshell_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8881,7 +10364,7 @@ func (x *PushSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PushSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*PushSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{124} + return file_openshell_proto_rawDescGZIP(), []int{146} } // Get sandbox logs response. @@ -8897,7 +10380,7 @@ type GetSandboxLogsResponse struct { func (x *GetSandboxLogsResponse) Reset() { *x = GetSandboxLogsResponse{} - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[147] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8909,7 +10392,7 @@ func (x *GetSandboxLogsResponse) String() string { func (*GetSandboxLogsResponse) ProtoMessage() {} func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[125] + mi := &file_openshell_proto_msgTypes[147] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8922,7 +10405,7 @@ func (x *GetSandboxLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSandboxLogsResponse.ProtoReflect.Descriptor instead. func (*GetSandboxLogsResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{125} + return file_openshell_proto_rawDescGZIP(), []int{147} } func (x *GetSandboxLogsResponse) GetLogs() []*SandboxLogLine { @@ -8955,7 +10438,7 @@ type SupervisorMessage struct { func (x *SupervisorMessage) Reset() { *x = SupervisorMessage{} - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[148] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8967,7 +10450,7 @@ func (x *SupervisorMessage) String() string { func (*SupervisorMessage) ProtoMessage() {} func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[126] + mi := &file_openshell_proto_msgTypes[148] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8980,7 +10463,7 @@ func (x *SupervisorMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorMessage.ProtoReflect.Descriptor instead. func (*SupervisorMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{126} + return file_openshell_proto_rawDescGZIP(), []int{148} } func (x *SupervisorMessage) GetPayload() isSupervisorMessage_Payload { @@ -9071,7 +10554,7 @@ type GatewayMessage struct { func (x *GatewayMessage) Reset() { *x = GatewayMessage{} - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9083,7 +10566,7 @@ func (x *GatewayMessage) String() string { func (*GatewayMessage) ProtoMessage() {} func (x *GatewayMessage) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[127] + mi := &file_openshell_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9096,7 +10579,7 @@ func (x *GatewayMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayMessage.ProtoReflect.Descriptor instead. func (*GatewayMessage) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{127} + return file_openshell_proto_rawDescGZIP(), []int{149} } func (x *GatewayMessage) GetPayload() isGatewayMessage_Payload { @@ -9198,7 +10681,7 @@ type SupervisorHello struct { func (x *SupervisorHello) Reset() { *x = SupervisorHello{} - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[150] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9210,7 +10693,7 @@ func (x *SupervisorHello) String() string { func (*SupervisorHello) ProtoMessage() {} func (x *SupervisorHello) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[128] + mi := &file_openshell_proto_msgTypes[150] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9223,7 +10706,7 @@ func (x *SupervisorHello) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHello.ProtoReflect.Descriptor instead. func (*SupervisorHello) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{128} + return file_openshell_proto_rawDescGZIP(), []int{150} } func (x *SupervisorHello) GetSandboxId() string { @@ -9253,7 +10736,7 @@ type SessionAccepted struct { func (x *SessionAccepted) Reset() { *x = SessionAccepted{} - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[151] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9265,7 +10748,7 @@ func (x *SessionAccepted) String() string { func (*SessionAccepted) ProtoMessage() {} func (x *SessionAccepted) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[129] + mi := &file_openshell_proto_msgTypes[151] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9278,7 +10761,7 @@ func (x *SessionAccepted) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionAccepted.ProtoReflect.Descriptor instead. func (*SessionAccepted) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{129} + return file_openshell_proto_rawDescGZIP(), []int{151} } func (x *SessionAccepted) GetSessionId() string { @@ -9306,7 +10789,7 @@ type SessionRejected struct { func (x *SessionRejected) Reset() { *x = SessionRejected{} - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[152] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9318,7 +10801,7 @@ func (x *SessionRejected) String() string { func (*SessionRejected) ProtoMessage() {} func (x *SessionRejected) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[130] + mi := &file_openshell_proto_msgTypes[152] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9331,7 +10814,7 @@ func (x *SessionRejected) ProtoReflect() protoreflect.Message { // Deprecated: Use SessionRejected.ProtoReflect.Descriptor instead. func (*SessionRejected) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{130} + return file_openshell_proto_rawDescGZIP(), []int{152} } func (x *SessionRejected) GetReason() string { @@ -9350,7 +10833,7 @@ type SupervisorHeartbeat struct { func (x *SupervisorHeartbeat) Reset() { *x = SupervisorHeartbeat{} - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[153] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9362,7 +10845,7 @@ func (x *SupervisorHeartbeat) String() string { func (*SupervisorHeartbeat) ProtoMessage() {} func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[131] + mi := &file_openshell_proto_msgTypes[153] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9375,7 +10858,7 @@ func (x *SupervisorHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use SupervisorHeartbeat.ProtoReflect.Descriptor instead. func (*SupervisorHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{131} + return file_openshell_proto_rawDescGZIP(), []int{153} } // Gateway heartbeat. @@ -9387,7 +10870,7 @@ type GatewayHeartbeat struct { func (x *GatewayHeartbeat) Reset() { *x = GatewayHeartbeat{} - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[154] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9399,7 +10882,7 @@ func (x *GatewayHeartbeat) String() string { func (*GatewayHeartbeat) ProtoMessage() {} func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[132] + mi := &file_openshell_proto_msgTypes[154] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9412,7 +10895,7 @@ func (x *GatewayHeartbeat) ProtoReflect() protoreflect.Message { // Deprecated: Use GatewayHeartbeat.ProtoReflect.Descriptor instead. func (*GatewayHeartbeat) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{132} + return file_openshell_proto_rawDescGZIP(), []int{154} } // Gateway requests the supervisor to open a relay channel. @@ -9441,7 +10924,7 @@ type RelayOpen struct { func (x *RelayOpen) Reset() { *x = RelayOpen{} - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[155] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9453,7 +10936,7 @@ func (x *RelayOpen) String() string { func (*RelayOpen) ProtoMessage() {} func (x *RelayOpen) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[133] + mi := &file_openshell_proto_msgTypes[155] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9466,7 +10949,7 @@ func (x *RelayOpen) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpen.ProtoReflect.Descriptor instead. func (*RelayOpen) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{133} + return file_openshell_proto_rawDescGZIP(), []int{155} } func (x *RelayOpen) GetChannelId() string { @@ -9533,7 +11016,7 @@ type SshRelayTarget struct { func (x *SshRelayTarget) Reset() { *x = SshRelayTarget{} - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[156] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9545,7 +11028,7 @@ func (x *SshRelayTarget) String() string { func (*SshRelayTarget) ProtoMessage() {} func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[134] + mi := &file_openshell_proto_msgTypes[156] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9558,7 +11041,7 @@ func (x *SshRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use SshRelayTarget.ProtoReflect.Descriptor instead. func (*SshRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{134} + return file_openshell_proto_rawDescGZIP(), []int{156} } // TCP target dialed by the supervisor from inside the sandbox. @@ -9574,7 +11057,7 @@ type TcpRelayTarget struct { func (x *TcpRelayTarget) Reset() { *x = TcpRelayTarget{} - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[157] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9586,7 +11069,7 @@ func (x *TcpRelayTarget) String() string { func (*TcpRelayTarget) ProtoMessage() {} func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[135] + mi := &file_openshell_proto_msgTypes[157] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9599,7 +11082,7 @@ func (x *TcpRelayTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use TcpRelayTarget.ProtoReflect.Descriptor instead. func (*TcpRelayTarget) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{135} + return file_openshell_proto_rawDescGZIP(), []int{157} } func (x *TcpRelayTarget) GetHost() string { @@ -9627,7 +11110,7 @@ type RelayInit struct { func (x *RelayInit) Reset() { *x = RelayInit{} - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9639,7 +11122,7 @@ func (x *RelayInit) String() string { func (*RelayInit) ProtoMessage() {} func (x *RelayInit) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[136] + mi := &file_openshell_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9652,7 +11135,7 @@ func (x *RelayInit) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayInit.ProtoReflect.Descriptor instead. func (*RelayInit) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{136} + return file_openshell_proto_rawDescGZIP(), []int{158} } func (x *RelayInit) GetChannelId() string { @@ -9679,7 +11162,7 @@ type RelayFrame struct { func (x *RelayFrame) Reset() { *x = RelayFrame{} - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9691,7 +11174,7 @@ func (x *RelayFrame) String() string { func (*RelayFrame) ProtoMessage() {} func (x *RelayFrame) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[137] + mi := &file_openshell_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9704,7 +11187,7 @@ func (x *RelayFrame) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayFrame.ProtoReflect.Descriptor instead. func (*RelayFrame) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{137} + return file_openshell_proto_rawDescGZIP(), []int{159} } func (x *RelayFrame) GetPayload() isRelayFrame_Payload { @@ -9763,7 +11246,7 @@ type RelayOpenResult struct { func (x *RelayOpenResult) Reset() { *x = RelayOpenResult{} - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9775,7 +11258,7 @@ func (x *RelayOpenResult) String() string { func (*RelayOpenResult) ProtoMessage() {} func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[138] + mi := &file_openshell_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9788,7 +11271,7 @@ func (x *RelayOpenResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayOpenResult.ProtoReflect.Descriptor instead. func (*RelayOpenResult) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{138} + return file_openshell_proto_rawDescGZIP(), []int{160} } func (x *RelayOpenResult) GetChannelId() string { @@ -9825,7 +11308,7 @@ type RelayClose struct { func (x *RelayClose) Reset() { *x = RelayClose{} - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9837,7 +11320,7 @@ func (x *RelayClose) String() string { func (*RelayClose) ProtoMessage() {} func (x *RelayClose) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[139] + mi := &file_openshell_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9850,7 +11333,7 @@ func (x *RelayClose) ProtoReflect() protoreflect.Message { // Deprecated: Use RelayClose.ProtoReflect.Descriptor instead. func (*RelayClose) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{139} + return file_openshell_proto_rawDescGZIP(), []int{161} } func (x *RelayClose) GetChannelId() string { @@ -9884,7 +11367,7 @@ type L7RequestSample struct { func (x *L7RequestSample) Reset() { *x = L7RequestSample{} - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9896,7 +11379,7 @@ func (x *L7RequestSample) String() string { func (*L7RequestSample) ProtoMessage() {} func (x *L7RequestSample) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[140] + mi := &file_openshell_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9909,7 +11392,7 @@ func (x *L7RequestSample) ProtoReflect() protoreflect.Message { // Deprecated: Use L7RequestSample.ProtoReflect.Descriptor instead. func (*L7RequestSample) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{140} + return file_openshell_proto_rawDescGZIP(), []int{162} } func (x *L7RequestSample) GetMethod() string { @@ -9983,7 +11466,7 @@ type DenialSummary struct { func (x *DenialSummary) Reset() { *x = DenialSummary{} - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9995,7 +11478,7 @@ func (x *DenialSummary) String() string { func (*DenialSummary) ProtoMessage() {} func (x *DenialSummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[141] + mi := &file_openshell_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10008,7 +11491,7 @@ func (x *DenialSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialSummary.ProtoReflect.Descriptor instead. func (*DenialSummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{141} + return file_openshell_proto_rawDescGZIP(), []int{163} } func (x *DenialSummary) GetSandboxId() string { @@ -10143,7 +11626,7 @@ type DenialGroupCount struct { func (x *DenialGroupCount) Reset() { *x = DenialGroupCount{} - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10155,7 +11638,7 @@ func (x *DenialGroupCount) String() string { func (*DenialGroupCount) ProtoMessage() {} func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[142] + mi := &file_openshell_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10168,7 +11651,7 @@ func (x *DenialGroupCount) ProtoReflect() protoreflect.Message { // Deprecated: Use DenialGroupCount.ProtoReflect.Descriptor instead. func (*DenialGroupCount) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{142} + return file_openshell_proto_rawDescGZIP(), []int{164} } func (x *DenialGroupCount) GetDenyGroup() string { @@ -10201,7 +11684,7 @@ type NetworkActivitySummary struct { func (x *NetworkActivitySummary) Reset() { *x = NetworkActivitySummary{} - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10213,7 +11696,7 @@ func (x *NetworkActivitySummary) String() string { func (*NetworkActivitySummary) ProtoMessage() {} func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[143] + mi := &file_openshell_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10226,7 +11709,7 @@ func (x *NetworkActivitySummary) ProtoReflect() protoreflect.Message { // Deprecated: Use NetworkActivitySummary.ProtoReflect.Descriptor instead. func (*NetworkActivitySummary) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{143} + return file_openshell_proto_rawDescGZIP(), []int{165} } func (x *NetworkActivitySummary) GetNetworkActivityCount() uint32 { @@ -10300,7 +11783,7 @@ type PolicyChunk struct { func (x *PolicyChunk) Reset() { *x = PolicyChunk{} - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10312,7 +11795,7 @@ func (x *PolicyChunk) String() string { func (*PolicyChunk) ProtoMessage() {} func (x *PolicyChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[144] + mi := &file_openshell_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10325,7 +11808,7 @@ func (x *PolicyChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyChunk.ProtoReflect.Descriptor instead. func (*PolicyChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{144} + return file_openshell_proto_rawDescGZIP(), []int{166} } func (x *PolicyChunk) GetId() string { @@ -10471,7 +11954,7 @@ type DraftPolicyUpdate struct { func (x *DraftPolicyUpdate) Reset() { *x = DraftPolicyUpdate{} - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10483,7 +11966,7 @@ func (x *DraftPolicyUpdate) String() string { func (*DraftPolicyUpdate) ProtoMessage() {} func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[145] + mi := &file_openshell_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10496,7 +11979,7 @@ func (x *DraftPolicyUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftPolicyUpdate.ProtoReflect.Descriptor instead. func (*DraftPolicyUpdate) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{145} + return file_openshell_proto_rawDescGZIP(), []int{167} } func (x *DraftPolicyUpdate) GetDraftVersion() uint64 { @@ -10554,7 +12037,7 @@ type SubmitPolicyAnalysisRequest struct { func (x *SubmitPolicyAnalysisRequest) Reset() { *x = SubmitPolicyAnalysisRequest{} - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10566,7 +12049,7 @@ func (x *SubmitPolicyAnalysisRequest) String() string { func (*SubmitPolicyAnalysisRequest) ProtoMessage() {} func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[146] + mi := &file_openshell_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10579,7 +12062,7 @@ func (x *SubmitPolicyAnalysisRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisRequest.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{146} + return file_openshell_proto_rawDescGZIP(), []int{168} } func (x *SubmitPolicyAnalysisRequest) GetSummaries() []*DenialSummary { @@ -10642,7 +12125,7 @@ type SubmitPolicyAnalysisResponse struct { func (x *SubmitPolicyAnalysisResponse) Reset() { *x = SubmitPolicyAnalysisResponse{} - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10654,7 +12137,7 @@ func (x *SubmitPolicyAnalysisResponse) String() string { func (*SubmitPolicyAnalysisResponse) ProtoMessage() {} func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[147] + mi := &file_openshell_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10667,7 +12150,7 @@ func (x *SubmitPolicyAnalysisResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SubmitPolicyAnalysisResponse.ProtoReflect.Descriptor instead. func (*SubmitPolicyAnalysisResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{147} + return file_openshell_proto_rawDescGZIP(), []int{169} } func (x *SubmitPolicyAnalysisResponse) GetAcceptedChunks() uint32 { @@ -10713,7 +12196,7 @@ type GetDraftPolicyRequest struct { func (x *GetDraftPolicyRequest) Reset() { *x = GetDraftPolicyRequest{} - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10725,7 +12208,7 @@ func (x *GetDraftPolicyRequest) String() string { func (*GetDraftPolicyRequest) ProtoMessage() {} func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[148] + mi := &file_openshell_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10738,7 +12221,7 @@ func (x *GetDraftPolicyRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyRequest.ProtoReflect.Descriptor instead. func (*GetDraftPolicyRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{148} + return file_openshell_proto_rawDescGZIP(), []int{170} } func (x *GetDraftPolicyRequest) GetName() string { @@ -10778,7 +12261,7 @@ type GetDraftPolicyResponse struct { func (x *GetDraftPolicyResponse) Reset() { *x = GetDraftPolicyResponse{} - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10790,7 +12273,7 @@ func (x *GetDraftPolicyResponse) String() string { func (*GetDraftPolicyResponse) ProtoMessage() {} func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[149] + mi := &file_openshell_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10803,7 +12286,7 @@ func (x *GetDraftPolicyResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftPolicyResponse.ProtoReflect.Descriptor instead. func (*GetDraftPolicyResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{149} + return file_openshell_proto_rawDescGZIP(), []int{171} } func (x *GetDraftPolicyResponse) GetChunks() []*PolicyChunk { @@ -10849,7 +12332,7 @@ type ApproveDraftChunkRequest struct { func (x *ApproveDraftChunkRequest) Reset() { *x = ApproveDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[172] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10861,7 +12344,7 @@ func (x *ApproveDraftChunkRequest) String() string { func (*ApproveDraftChunkRequest) ProtoMessage() {} func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[150] + mi := &file_openshell_proto_msgTypes[172] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10874,7 +12357,7 @@ func (x *ApproveDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkRequest.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{150} + return file_openshell_proto_rawDescGZIP(), []int{172} } func (x *ApproveDraftChunkRequest) GetName() string { @@ -10910,7 +12393,7 @@ type ApproveDraftChunkResponse struct { func (x *ApproveDraftChunkResponse) Reset() { *x = ApproveDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[173] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10922,7 +12405,7 @@ func (x *ApproveDraftChunkResponse) String() string { func (*ApproveDraftChunkResponse) ProtoMessage() {} func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[151] + mi := &file_openshell_proto_msgTypes[173] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10935,7 +12418,7 @@ func (x *ApproveDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveDraftChunkResponse.ProtoReflect.Descriptor instead. func (*ApproveDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{151} + return file_openshell_proto_rawDescGZIP(), []int{173} } func (x *ApproveDraftChunkResponse) GetPolicyVersion() uint32 { @@ -10969,7 +12452,7 @@ type RejectDraftChunkRequest struct { func (x *RejectDraftChunkRequest) Reset() { *x = RejectDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[174] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10981,7 +12464,7 @@ func (x *RejectDraftChunkRequest) String() string { func (*RejectDraftChunkRequest) ProtoMessage() {} func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[152] + mi := &file_openshell_proto_msgTypes[174] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10994,7 +12477,7 @@ func (x *RejectDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkRequest.ProtoReflect.Descriptor instead. func (*RejectDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{152} + return file_openshell_proto_rawDescGZIP(), []int{174} } func (x *RejectDraftChunkRequest) GetName() string { @@ -11033,7 +12516,7 @@ type RejectDraftChunkResponse struct { func (x *RejectDraftChunkResponse) Reset() { *x = RejectDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[175] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11045,7 +12528,7 @@ func (x *RejectDraftChunkResponse) String() string { func (*RejectDraftChunkResponse) ProtoMessage() {} func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[153] + mi := &file_openshell_proto_msgTypes[175] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11058,7 +12541,7 @@ func (x *RejectDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RejectDraftChunkResponse.ProtoReflect.Descriptor instead. func (*RejectDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{153} + return file_openshell_proto_rawDescGZIP(), []int{175} } // Approve all pending chunks. @@ -11076,7 +12559,7 @@ type ApproveAllDraftChunksRequest struct { func (x *ApproveAllDraftChunksRequest) Reset() { *x = ApproveAllDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[176] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11088,7 +12571,7 @@ func (x *ApproveAllDraftChunksRequest) String() string { func (*ApproveAllDraftChunksRequest) ProtoMessage() {} func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[154] + mi := &file_openshell_proto_msgTypes[176] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11101,7 +12584,7 @@ func (x *ApproveAllDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{154} + return file_openshell_proto_rawDescGZIP(), []int{176} } func (x *ApproveAllDraftChunksRequest) GetName() string { @@ -11141,7 +12624,7 @@ type ApproveAllDraftChunksResponse struct { func (x *ApproveAllDraftChunksResponse) Reset() { *x = ApproveAllDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[177] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11153,7 +12636,7 @@ func (x *ApproveAllDraftChunksResponse) String() string { func (*ApproveAllDraftChunksResponse) ProtoMessage() {} func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[155] + mi := &file_openshell_proto_msgTypes[177] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11166,7 +12649,7 @@ func (x *ApproveAllDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApproveAllDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ApproveAllDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{155} + return file_openshell_proto_rawDescGZIP(), []int{177} } func (x *ApproveAllDraftChunksResponse) GetPolicyVersion() uint32 { @@ -11214,7 +12697,7 @@ type EditDraftChunkRequest struct { func (x *EditDraftChunkRequest) Reset() { *x = EditDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[178] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11226,7 +12709,7 @@ func (x *EditDraftChunkRequest) String() string { func (*EditDraftChunkRequest) ProtoMessage() {} func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[156] + mi := &file_openshell_proto_msgTypes[178] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11239,7 +12722,7 @@ func (x *EditDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkRequest.ProtoReflect.Descriptor instead. func (*EditDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{156} + return file_openshell_proto_rawDescGZIP(), []int{178} } func (x *EditDraftChunkRequest) GetName() string { @@ -11278,7 +12761,7 @@ type EditDraftChunkResponse struct { func (x *EditDraftChunkResponse) Reset() { *x = EditDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[179] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11290,7 +12773,7 @@ func (x *EditDraftChunkResponse) String() string { func (*EditDraftChunkResponse) ProtoMessage() {} func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[157] + mi := &file_openshell_proto_msgTypes[179] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11303,7 +12786,7 @@ func (x *EditDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EditDraftChunkResponse.ProtoReflect.Descriptor instead. func (*EditDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{157} + return file_openshell_proto_rawDescGZIP(), []int{179} } // Reverse an approval (remove merged rule from active policy). @@ -11321,7 +12804,7 @@ type UndoDraftChunkRequest struct { func (x *UndoDraftChunkRequest) Reset() { *x = UndoDraftChunkRequest{} - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[180] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11333,7 +12816,7 @@ func (x *UndoDraftChunkRequest) String() string { func (*UndoDraftChunkRequest) ProtoMessage() {} func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[158] + mi := &file_openshell_proto_msgTypes[180] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11346,7 +12829,7 @@ func (x *UndoDraftChunkRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkRequest.ProtoReflect.Descriptor instead. func (*UndoDraftChunkRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{158} + return file_openshell_proto_rawDescGZIP(), []int{180} } func (x *UndoDraftChunkRequest) GetName() string { @@ -11382,7 +12865,7 @@ type UndoDraftChunkResponse struct { func (x *UndoDraftChunkResponse) Reset() { *x = UndoDraftChunkResponse{} - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[181] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11394,7 +12877,7 @@ func (x *UndoDraftChunkResponse) String() string { func (*UndoDraftChunkResponse) ProtoMessage() {} func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[159] + mi := &file_openshell_proto_msgTypes[181] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11407,7 +12890,7 @@ func (x *UndoDraftChunkResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UndoDraftChunkResponse.ProtoReflect.Descriptor instead. func (*UndoDraftChunkResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{159} + return file_openshell_proto_rawDescGZIP(), []int{181} } func (x *UndoDraftChunkResponse) GetPolicyVersion() uint32 { @@ -11437,7 +12920,7 @@ type ClearDraftChunksRequest struct { func (x *ClearDraftChunksRequest) Reset() { *x = ClearDraftChunksRequest{} - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[182] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11449,7 +12932,7 @@ func (x *ClearDraftChunksRequest) String() string { func (*ClearDraftChunksRequest) ProtoMessage() {} func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[160] + mi := &file_openshell_proto_msgTypes[182] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11462,7 +12945,7 @@ func (x *ClearDraftChunksRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksRequest.ProtoReflect.Descriptor instead. func (*ClearDraftChunksRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{160} + return file_openshell_proto_rawDescGZIP(), []int{182} } func (x *ClearDraftChunksRequest) GetName() string { @@ -11489,7 +12972,7 @@ type ClearDraftChunksResponse struct { func (x *ClearDraftChunksResponse) Reset() { *x = ClearDraftChunksResponse{} - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[183] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11501,7 +12984,7 @@ func (x *ClearDraftChunksResponse) String() string { func (*ClearDraftChunksResponse) ProtoMessage() {} func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[161] + mi := &file_openshell_proto_msgTypes[183] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11514,7 +12997,7 @@ func (x *ClearDraftChunksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClearDraftChunksResponse.ProtoReflect.Descriptor instead. func (*ClearDraftChunksResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{161} + return file_openshell_proto_rawDescGZIP(), []int{183} } func (x *ClearDraftChunksResponse) GetChunksCleared() uint32 { @@ -11537,7 +13020,7 @@ type GetDraftHistoryRequest struct { func (x *GetDraftHistoryRequest) Reset() { *x = GetDraftHistoryRequest{} - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[184] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11549,7 +13032,7 @@ func (x *GetDraftHistoryRequest) String() string { func (*GetDraftHistoryRequest) ProtoMessage() {} func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[162] + mi := &file_openshell_proto_msgTypes[184] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11562,7 +13045,7 @@ func (x *GetDraftHistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryRequest.ProtoReflect.Descriptor instead. func (*GetDraftHistoryRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{162} + return file_openshell_proto_rawDescGZIP(), []int{184} } func (x *GetDraftHistoryRequest) GetName() string { @@ -11596,7 +13079,7 @@ type DraftHistoryEntry struct { func (x *DraftHistoryEntry) Reset() { *x = DraftHistoryEntry{} - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[185] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11608,7 +13091,7 @@ func (x *DraftHistoryEntry) String() string { func (*DraftHistoryEntry) ProtoMessage() {} func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[163] + mi := &file_openshell_proto_msgTypes[185] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11621,7 +13104,7 @@ func (x *DraftHistoryEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftHistoryEntry.ProtoReflect.Descriptor instead. func (*DraftHistoryEntry) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{163} + return file_openshell_proto_rawDescGZIP(), []int{185} } func (x *DraftHistoryEntry) GetTimestampMs() int64 { @@ -11662,7 +13145,7 @@ type GetDraftHistoryResponse struct { func (x *GetDraftHistoryResponse) Reset() { *x = GetDraftHistoryResponse{} - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[186] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11674,7 +13157,7 @@ func (x *GetDraftHistoryResponse) String() string { func (*GetDraftHistoryResponse) ProtoMessage() {} func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[164] + mi := &file_openshell_proto_msgTypes[186] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11687,7 +13170,7 @@ func (x *GetDraftHistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDraftHistoryResponse.ProtoReflect.Descriptor instead. func (*GetDraftHistoryResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{164} + return file_openshell_proto_rawDescGZIP(), []int{186} } func (x *GetDraftHistoryResponse) GetEntries() []*DraftHistoryEntry { @@ -11716,7 +13199,7 @@ type PolicyRevisionPayload struct { func (x *PolicyRevisionPayload) Reset() { *x = PolicyRevisionPayload{} - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[187] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11728,7 +13211,7 @@ func (x *PolicyRevisionPayload) String() string { func (*PolicyRevisionPayload) ProtoMessage() {} func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[165] + mi := &file_openshell_proto_msgTypes[187] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11741,7 +13224,7 @@ func (x *PolicyRevisionPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use PolicyRevisionPayload.ProtoReflect.Descriptor instead. func (*PolicyRevisionPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{165} + return file_openshell_proto_rawDescGZIP(), []int{187} } func (x *PolicyRevisionPayload) GetPolicy() *sandboxv1.SandboxPolicy { @@ -11814,7 +13297,7 @@ type DraftChunkPayload struct { func (x *DraftChunkPayload) Reset() { *x = DraftChunkPayload{} - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[188] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11826,7 +13309,7 @@ func (x *DraftChunkPayload) String() string { func (*DraftChunkPayload) ProtoMessage() {} func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[166] + mi := &file_openshell_proto_msgTypes[188] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11839,7 +13322,7 @@ func (x *DraftChunkPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use DraftChunkPayload.ProtoReflect.Descriptor instead. func (*DraftChunkPayload) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{166} + return file_openshell_proto_rawDescGZIP(), []int{188} } func (x *DraftChunkPayload) GetRuleName() string { @@ -11945,7 +13428,7 @@ type StoredPolicyRevision struct { func (x *StoredPolicyRevision) Reset() { *x = StoredPolicyRevision{} - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[189] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11957,7 +13440,7 @@ func (x *StoredPolicyRevision) String() string { func (*StoredPolicyRevision) ProtoMessage() {} func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[167] + mi := &file_openshell_proto_msgTypes[189] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11970,7 +13453,7 @@ func (x *StoredPolicyRevision) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredPolicyRevision.ProtoReflect.Descriptor instead. func (*StoredPolicyRevision) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{167} + return file_openshell_proto_rawDescGZIP(), []int{189} } func (x *StoredPolicyRevision) GetId() string { @@ -12073,7 +13556,7 @@ type StoredDraftChunk struct { func (x *StoredDraftChunk) Reset() { *x = StoredDraftChunk{} - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[190] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12085,7 +13568,7 @@ func (x *StoredDraftChunk) String() string { func (*StoredDraftChunk) ProtoMessage() {} func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[168] + mi := &file_openshell_proto_msgTypes[190] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12098,7 +13581,7 @@ func (x *StoredDraftChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredDraftChunk.ProtoReflect.Descriptor instead. func (*StoredDraftChunk) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{168} + return file_openshell_proto_rawDescGZIP(), []int{190} } func (x *StoredDraftChunk) GetId() string { @@ -12247,7 +13730,7 @@ type CreateWorkspaceRequest struct { func (x *CreateWorkspaceRequest) Reset() { *x = CreateWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[191] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12259,7 +13742,7 @@ func (x *CreateWorkspaceRequest) String() string { func (*CreateWorkspaceRequest) ProtoMessage() {} func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[169] + mi := &file_openshell_proto_msgTypes[191] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12272,7 +13755,7 @@ func (x *CreateWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceRequest.ProtoReflect.Descriptor instead. func (*CreateWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{169} + return file_openshell_proto_rawDescGZIP(), []int{191} } func (x *CreateWorkspaceRequest) GetName() string { @@ -12299,7 +13782,7 @@ type CreateWorkspaceResponse struct { func (x *CreateWorkspaceResponse) Reset() { *x = CreateWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[192] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12311,7 +13794,7 @@ func (x *CreateWorkspaceResponse) String() string { func (*CreateWorkspaceResponse) ProtoMessage() {} func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[170] + mi := &file_openshell_proto_msgTypes[192] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12324,7 +13807,7 @@ func (x *CreateWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateWorkspaceResponse.ProtoReflect.Descriptor instead. func (*CreateWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{170} + return file_openshell_proto_rawDescGZIP(), []int{192} } func (x *CreateWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -12345,7 +13828,7 @@ type GetWorkspaceRequest struct { func (x *GetWorkspaceRequest) Reset() { *x = GetWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[193] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12357,7 +13840,7 @@ func (x *GetWorkspaceRequest) String() string { func (*GetWorkspaceRequest) ProtoMessage() {} func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[171] + mi := &file_openshell_proto_msgTypes[193] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12370,7 +13853,7 @@ func (x *GetWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceRequest.ProtoReflect.Descriptor instead. func (*GetWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{171} + return file_openshell_proto_rawDescGZIP(), []int{193} } func (x *GetWorkspaceRequest) GetName() string { @@ -12390,7 +13873,7 @@ type GetWorkspaceResponse struct { func (x *GetWorkspaceResponse) Reset() { *x = GetWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[194] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12402,7 +13885,7 @@ func (x *GetWorkspaceResponse) String() string { func (*GetWorkspaceResponse) ProtoMessage() {} func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[172] + mi := &file_openshell_proto_msgTypes[194] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12415,7 +13898,7 @@ func (x *GetWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetWorkspaceResponse.ProtoReflect.Descriptor instead. func (*GetWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{172} + return file_openshell_proto_rawDescGZIP(), []int{194} } func (x *GetWorkspaceResponse) GetWorkspace() *datamodelv1.Workspace { @@ -12438,7 +13921,7 @@ type ListWorkspacesRequest struct { func (x *ListWorkspacesRequest) Reset() { *x = ListWorkspacesRequest{} - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[195] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12450,7 +13933,7 @@ func (x *ListWorkspacesRequest) String() string { func (*ListWorkspacesRequest) ProtoMessage() {} func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[173] + mi := &file_openshell_proto_msgTypes[195] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12463,7 +13946,7 @@ func (x *ListWorkspacesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesRequest.ProtoReflect.Descriptor instead. func (*ListWorkspacesRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{173} + return file_openshell_proto_rawDescGZIP(), []int{195} } func (x *ListWorkspacesRequest) GetLimit() uint32 { @@ -12497,7 +13980,7 @@ type ListWorkspacesResponse struct { func (x *ListWorkspacesResponse) Reset() { *x = ListWorkspacesResponse{} - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[196] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12509,7 +13992,7 @@ func (x *ListWorkspacesResponse) String() string { func (*ListWorkspacesResponse) ProtoMessage() {} func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[174] + mi := &file_openshell_proto_msgTypes[196] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12522,7 +14005,7 @@ func (x *ListWorkspacesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspacesResponse.ProtoReflect.Descriptor instead. func (*ListWorkspacesResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{174} + return file_openshell_proto_rawDescGZIP(), []int{196} } func (x *ListWorkspacesResponse) GetWorkspaces() []*datamodelv1.Workspace { @@ -12543,7 +14026,7 @@ type DeleteWorkspaceRequest struct { func (x *DeleteWorkspaceRequest) Reset() { *x = DeleteWorkspaceRequest{} - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[197] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12555,7 +14038,7 @@ func (x *DeleteWorkspaceRequest) String() string { func (*DeleteWorkspaceRequest) ProtoMessage() {} func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[175] + mi := &file_openshell_proto_msgTypes[197] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12568,7 +14051,7 @@ func (x *DeleteWorkspaceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceRequest.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{175} + return file_openshell_proto_rawDescGZIP(), []int{197} } func (x *DeleteWorkspaceRequest) GetName() string { @@ -12588,7 +14071,7 @@ type DeleteWorkspaceResponse struct { func (x *DeleteWorkspaceResponse) Reset() { *x = DeleteWorkspaceResponse{} - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[198] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12600,7 +14083,7 @@ func (x *DeleteWorkspaceResponse) String() string { func (*DeleteWorkspaceResponse) ProtoMessage() {} func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[176] + mi := &file_openshell_proto_msgTypes[198] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12613,7 +14096,7 @@ func (x *DeleteWorkspaceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteWorkspaceResponse.ProtoReflect.Descriptor instead. func (*DeleteWorkspaceResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{176} + return file_openshell_proto_rawDescGZIP(), []int{198} } func (x *DeleteWorkspaceResponse) GetDeleted() bool { @@ -12637,7 +14120,7 @@ type WorkspaceMember struct { func (x *WorkspaceMember) Reset() { *x = WorkspaceMember{} - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[199] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12649,7 +14132,7 @@ func (x *WorkspaceMember) String() string { func (*WorkspaceMember) ProtoMessage() {} func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[177] + mi := &file_openshell_proto_msgTypes[199] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12662,7 +14145,7 @@ func (x *WorkspaceMember) ProtoReflect() protoreflect.Message { // Deprecated: Use WorkspaceMember.ProtoReflect.Descriptor instead. func (*WorkspaceMember) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{177} + return file_openshell_proto_rawDescGZIP(), []int{199} } func (x *WorkspaceMember) GetMetadata() *datamodelv1.ObjectMeta { @@ -12701,7 +14184,7 @@ type AddWorkspaceMemberRequest struct { func (x *AddWorkspaceMemberRequest) Reset() { *x = AddWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[200] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12713,7 +14196,7 @@ func (x *AddWorkspaceMemberRequest) String() string { func (*AddWorkspaceMemberRequest) ProtoMessage() {} func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[178] + mi := &file_openshell_proto_msgTypes[200] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12726,7 +14209,7 @@ func (x *AddWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{178} + return file_openshell_proto_rawDescGZIP(), []int{200} } func (x *AddWorkspaceMemberRequest) GetWorkspace() string { @@ -12760,7 +14243,7 @@ type AddWorkspaceMemberResponse struct { func (x *AddWorkspaceMemberResponse) Reset() { *x = AddWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[201] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12772,7 +14255,7 @@ func (x *AddWorkspaceMemberResponse) String() string { func (*AddWorkspaceMemberResponse) ProtoMessage() {} func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[179] + mi := &file_openshell_proto_msgTypes[201] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12785,7 +14268,7 @@ func (x *AddWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*AddWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{179} + return file_openshell_proto_rawDescGZIP(), []int{201} } func (x *AddWorkspaceMemberResponse) GetMember() *WorkspaceMember { @@ -12808,7 +14291,7 @@ type RemoveWorkspaceMemberRequest struct { func (x *RemoveWorkspaceMemberRequest) Reset() { *x = RemoveWorkspaceMemberRequest{} - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[202] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12820,7 +14303,7 @@ func (x *RemoveWorkspaceMemberRequest) String() string { func (*RemoveWorkspaceMemberRequest) ProtoMessage() {} func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[180] + mi := &file_openshell_proto_msgTypes[202] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12833,7 +14316,7 @@ func (x *RemoveWorkspaceMemberRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberRequest.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{180} + return file_openshell_proto_rawDescGZIP(), []int{202} } func (x *RemoveWorkspaceMemberRequest) GetWorkspace() string { @@ -12860,7 +14343,7 @@ type RemoveWorkspaceMemberResponse struct { func (x *RemoveWorkspaceMemberResponse) Reset() { *x = RemoveWorkspaceMemberResponse{} - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[203] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12872,7 +14355,7 @@ func (x *RemoveWorkspaceMemberResponse) String() string { func (*RemoveWorkspaceMemberResponse) ProtoMessage() {} func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[181] + mi := &file_openshell_proto_msgTypes[203] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12885,7 +14368,7 @@ func (x *RemoveWorkspaceMemberResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveWorkspaceMemberResponse.ProtoReflect.Descriptor instead. func (*RemoveWorkspaceMemberResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{181} + return file_openshell_proto_rawDescGZIP(), []int{203} } func (x *RemoveWorkspaceMemberResponse) GetRemoved() bool { @@ -12908,7 +14391,7 @@ type ListWorkspaceMembersRequest struct { func (x *ListWorkspaceMembersRequest) Reset() { *x = ListWorkspaceMembersRequest{} - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[204] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12920,7 +14403,7 @@ func (x *ListWorkspaceMembersRequest) String() string { func (*ListWorkspaceMembersRequest) ProtoMessage() {} func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[182] + mi := &file_openshell_proto_msgTypes[204] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12933,7 +14416,7 @@ func (x *ListWorkspaceMembersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersRequest.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersRequest) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{182} + return file_openshell_proto_rawDescGZIP(), []int{204} } func (x *ListWorkspaceMembersRequest) GetWorkspace() string { @@ -12967,7 +14450,7 @@ type ListWorkspaceMembersResponse struct { func (x *ListWorkspaceMembersResponse) Reset() { *x = ListWorkspaceMembersResponse{} - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[205] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12979,7 +14462,7 @@ func (x *ListWorkspaceMembersResponse) String() string { func (*ListWorkspaceMembersResponse) ProtoMessage() {} func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[183] + mi := &file_openshell_proto_msgTypes[205] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12992,7 +14475,7 @@ func (x *ListWorkspaceMembersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListWorkspaceMembersResponse.ProtoReflect.Descriptor instead. func (*ListWorkspaceMembersResponse) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{183} + return file_openshell_proto_rawDescGZIP(), []int{205} } func (x *ListWorkspaceMembersResponse) GetMembers() []*WorkspaceMember { @@ -13020,7 +14503,7 @@ type ExtensionServiceCredential struct { func (x *ExtensionServiceCredential) Reset() { *x = ExtensionServiceCredential{} - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[206] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -13032,7 +14515,7 @@ func (x *ExtensionServiceCredential) String() string { func (*ExtensionServiceCredential) ProtoMessage() {} func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { - mi := &file_openshell_proto_msgTypes[184] + mi := &file_openshell_proto_msgTypes[206] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -13045,7 +14528,7 @@ func (x *ExtensionServiceCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use ExtensionServiceCredential.ProtoReflect.Descriptor instead. func (*ExtensionServiceCredential) Descriptor() ([]byte, []int) { - return file_openshell_proto_rawDescGZIP(), []int{184} + return file_openshell_proto_rawDescGZIP(), []int{206} } func (x *ExtensionServiceCredential) GetServiceName() string { @@ -13106,11 +14589,21 @@ const file_openshell_proto_rawDesc = "" + "\x19ComputeDriverCapabilities\x12\x1f\n" + "\vdriver_name\x18\x01 \x01(\tR\n" + "driverName\x12%\n" + - "\x0edriver_version\x18\x02 \x01(\tR\rdriverVersion\"\xd8\x01\n" + + "\x0edriver_version\x18\x02 \x01(\tR\rdriverVersion\"\xf2\x01\n" + "\aSandbox\x12>\n" + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12-\n" + "\x04spec\x18\x02 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x123\n" + - "\x06status\x18\x03 \x01(\v2\x1b.openshell.v1.SandboxStatusR\x06statusJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06R\x05phaseR\x16current_policy_version\"\xd7\x03\n" + + "\x06status\x18\x03 \x01(\v2\x1b.openshell.v1.SandboxStatusR\x06statusJ\x04\b\x04\x10\x05J\x04\b\x05\x10\x06J\x04\b\x06\x10\aR\x05phaseR\x16current_policy_versionR\x12delegated_identity\"\xc2\x01\n" + + "\x18SandboxDelegatedIdentity\x12#\n" + + "\rcredential_id\x18\x01 \x01(\tR\fcredentialId\x12+\n" + + "\x11principal_subject\x18\x02 \x01(\tR\x10principalSubject\x12,\n" + + "\x12delegated_until_ms\x18\x03 \x01(\x03R\x10delegatedUntilMs\x12&\n" + + "\x0fwithdrawn_at_ms\x18\x04 \x01(\x03R\rwithdrawnAtMs\"\xd6\x01\n" + + "\x1eSandboxDelegatedIdentityRecord\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x1d\n" + + "\n" + + "sandbox_id\x18\x02 \x01(\tR\tsandboxId\x12U\n" + + "\x12delegated_identity\x18\x03 \x01(\v2&.openshell.v1.SandboxDelegatedIdentityR\x11delegatedIdentity\"\xd7\x03\n" + "\vSandboxSpec\x12\x1b\n" + "\tlog_level\x18\x01 \x01(\tR\blogLevel\x12L\n" + "\venvironment\x18\x05 \x03(\v2*.openshell.v1.SandboxSpec.EnvironmentEntryR\venvironment\x129\n" + @@ -13176,19 +14669,48 @@ const file_openshell_proto_rawDesc = "" + "\bmetadata\x18\x06 \x03(\v2).openshell.v1.PlatformEvent.MetadataEntryR\bmetadata\x1a;\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x91\x03\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe8\x03\n" + "\x14CreateSandboxRequest\x12-\n" + "\x04spec\x18\x01 \x01(\v2\x19.openshell.v1.SandboxSpecR\x04spec\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12F\n" + "\x06labels\x18\x03 \x03(\v2..openshell.v1.CreateSandboxRequest.LabelsEntryR\x06labels\x12U\n" + "\vannotations\x18\x04 \x03(\v23.openshell.v1.CreateSandboxRequest.AnnotationsEntryR\vannotations\x12\x1c\n" + - "\tworkspace\x18\x05 \x01(\tR\tworkspace\x1a9\n" + + "\tworkspace\x18\x05 \x01(\tR\tworkspace\x12U\n" + + "\x12delegated_identity\x18\x06 \x01(\v2&.openshell.v1.DelegatedIdentityRequestR\x11delegatedIdentity\x1a9\n" + "\vLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"E\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa7\x02\n" + + "\x18DelegatedIdentityRequest\x12,\n" + + "\x12delegated_until_ms\x18\x01 \x01(\x03R\x10delegatedUntilMs\x12\x16\n" + + "\x06issuer\x18\x02 \x01(\tR\x06issuer\x12\x1b\n" + + "\tclient_id\x18\x03 \x01(\tR\bclientId\x12)\n" + + "\rrefresh_token\x18\x04 \x01(\tB\x04\x88\xb5\x18\x01R\frefreshToken\x12'\n" + + "\faccess_token\x18\x05 \x01(\tB\x04\x88\xb5\x18\x01R\vaccessToken\x12\x16\n" + + "\x06scopes\x18\a \x01(\tR\x06scopes\x12\x1a\n" + + "\baudience\x18\b \x01(\tR\baudienceJ\x04\b\x06\x10\aR\x1aaccess_token_expires_at_ms\"\\\n" + + "(GetSandboxDelegatedIdentityStatusRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + + "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\x81\x02\n" + + ")GetSandboxDelegatedIdentityStatusResponse\x12U\n" + + "\x12delegated_identity\x18\x01 \x01(\v2&.openshell.v1.SandboxDelegatedIdentityR\x11delegatedIdentity\x12\x15\n" + + "\x06now_ms\x18\x02 \x01(\x03R\x05nowMs\x127\n" + + "\x18credential_revoked_at_ms\x18\x03 \x01(\x03R\x15credentialRevokedAtMs\x12-\n" + + "\x12credential_missing\x18\x04 \x01(\bR\x11credentialMissing\"[\n" + + "'WithdrawSandboxDelegatedIdentityRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + + "\tworkspace\x18\x02 \x01(\tR\tworkspace\"y\n" + + "(WithdrawSandboxDelegatedIdentityResponse\x12/\n" + + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\x12\x1c\n" + + "\twithdrawn\x18\x02 \x01(\bR\twithdrawn\"\xb0\x01\n" + + "%ExtendSandboxDelegatedIdentityRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + + "\tworkspace\x18\x02 \x01(\tR\tworkspace\x12U\n" + + "\x12delegated_identity\x18\x03 \x01(\v2&.openshell.v1.DelegatedIdentityRequestR\x11delegatedIdentity\"Y\n" + + "&ExtendSandboxDelegatedIdentityResponse\x12/\n" + + "\asandbox\x18\x01 \x01(\v2\x15.openshell.v1.SandboxR\asandbox\"E\n" + "\x11GetSandboxRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1c\n" + "\tworkspace\x18\x02 \x01(\tR\tworkspace\"\xb0\x01\n" + @@ -13425,7 +14947,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" + @@ -13433,7 +14961,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" + @@ -13508,7 +15041,57 @@ const file_openshell_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aG\n" + "\x19AdditionalOutputKeysEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x82\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd4\x03\n" + + "\x1bDelegatedIdentityCredential\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x16\n" + + "\x06issuer\x18\x02 \x01(\tR\x06issuer\x12\x1b\n" + + "\tclient_id\x18\x03 \x01(\tR\bclientId\x12+\n" + + "\x11principal_subject\x18\x04 \x01(\tR\x10principalSubject\x12)\n" + + "\rrefresh_token\x18\x05 \x01(\tB\x04\x88\xb5\x18\x01R\frefreshToken\x12'\n" + + "\faccess_token\x18\x06 \x01(\tB\x04\x88\xb5\x18\x01R\vaccessToken\x12:\n" + + "\x1aaccess_token_expires_at_ms\x18\a \x01(\x03R\x16accessTokenExpiresAtMs\x12\x16\n" + + "\x06scopes\x18\b \x01(\tR\x06scopes\x12\x1a\n" + + "\baudience\x18\t \x01(\tR\baudience\x12+\n" + + "\x12last_refresh_at_ms\x18\n" + + " \x01(\x03R\x0flastRefreshAtMs\x12\"\n" + + "\rrevoked_at_ms\x18\v \x01(\x03R\vrevokedAtMs\"\xed\x03\n" + + "\"DelegatedIdentityCredentialSummary\x12>\n" + + "\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x16\n" + + "\x06issuer\x18\x02 \x01(\tR\x06issuer\x12\x1b\n" + + "\tclient_id\x18\x03 \x01(\tR\bclientId\x12+\n" + + "\x11principal_subject\x18\x04 \x01(\tR\x10principalSubject\x122\n" + + "\x15refresh_token_present\x18\x05 \x01(\bR\x13refreshTokenPresent\x120\n" + + "\x14access_token_present\x18\x06 \x01(\bR\x12accessTokenPresent\x12:\n" + + "\x1aaccess_token_expires_at_ms\x18\a \x01(\x03R\x16accessTokenExpiresAtMs\x12\x16\n" + + "\x06scopes\x18\b \x01(\tR\x06scopes\x12\x1a\n" + + "\baudience\x18\t \x01(\tR\baudience\x12+\n" + + "\x12last_refresh_at_ms\x18\n" + + " \x01(\x03R\x0flastRefreshAtMs\x12\"\n" + + "\rrevoked_at_ms\x18\v \x01(\x03R\vrevokedAtMs\"W\n" + + "'ListDelegatedIdentityCredentialsRequest\x12\x14\n" + + "\x05limit\x18\x01 \x01(\rR\x05limit\x12\x16\n" + + "\x06offset\x18\x02 \x01(\rR\x06offset\"~\n" + + "(ListDelegatedIdentityCredentialsResponse\x12R\n" + + "\vcredentials\x18\x01 \x03(\v20.openshell.v1.DelegatedIdentityCredentialSummaryR\vcredentials\"=\n" + + "+GetDelegatedIdentityCredentialStatusRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"\x97\x01\n" + + ",GetDelegatedIdentityCredentialStatusResponse\x12P\n" + + "\n" + + "credential\x18\x01 \x01(\v20.openshell.v1.DelegatedIdentityCredentialSummaryR\n" + + "credential\x12\x15\n" + + "\x06now_ms\x18\x02 \x01(\x03R\x05nowMs\"v\n" + + "(RevokeDelegatedIdentityCredentialRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12:\n" + + "\x19expected_resource_version\x18\x02 \x01(\x04R\x17expectedResourceVersion\"\x9a\x01\n" + + ")RevokeDelegatedIdentityCredentialResponse\x12\x18\n" + + "\arevoked\x18\x02 \x01(\bR\arevoked\x12\"\n" + + "\rrevoked_at_ms\x18\x03 \x01(\x03R\vrevokedAtMs\x12)\n" + + "\x10resource_version\x18\x04 \x01(\x04R\x0fresourceVersionJ\x04\b\x01\x10\x02\"v\n" + + "(DeleteDelegatedIdentityCredentialRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12:\n" + + "\x19expected_resource_version\x18\x02 \x01(\x04R\x17expectedResourceVersion\"E\n" + + ")DeleteDelegatedIdentityCredentialResponse\x12\x18\n" + + "\adeleted\x18\x01 \x01(\bR\adeleted\"\x82\x01\n" + "\x1fGetProviderRefreshStatusRequest\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x12%\n" + "\x0ecredential_key\x18\x02 \x01(\tR\rcredentialKey\x12\x1c\n" + @@ -13624,7 +15207,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" + @@ -14076,7 +15671,11 @@ const file_openshell_proto_rawDesc = "" + "\x15SANDBOX_PHASE_UNKNOWN\x10\x05\x12\x1a\n" + "\x16SANDBOX_PHASE_STOPPING\x10\x06\x12\x19\n" + "\x15SANDBOX_PHASE_STOPPED\x10\a\x12\x1a\n" + - "\x16SANDBOX_PHASE_STARTING\x10\b*\xc3\x03\n" + + "\x16SANDBOX_PHASE_STARTING\x10\b*\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" + @@ -14108,7 +15707,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\x94D\n" + + "\x14WORKSPACE_ROLE_ADMIN\x10\x022\xe4O\n" + "\tOpenShell\x12Z\n" + "\x06Health\x12\x1b.openshell.v1.HealthRequest\x1a\x1c.openshell.v1.HealthResponse\"\x15\x82\xb5\x18\x11\n" + "\x0funauthenticated\x12i\n" + @@ -14117,6 +15716,12 @@ const file_openshell_proto_rawDesc = "" + "\x0eGetGatewayInfo\x12#.openshell.v1.GetGatewayInfoRequest\x1a$.openshell.v1.GetGatewayInfoResponse\")\x82\xb5\x18%\n" + "\x06bearer\x1a\x0eplatform_admin\"\vconfig:read\x12u\n" + "\rCreateSandbox\x12\".openshell.v1.CreateSandboxRequest\x1a\x1d.openshell.v1.SandboxResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12\xb6\x01\n" + + "!GetSandboxDelegatedIdentityStatus\x126.openshell.v1.GetSandboxDelegatedIdentityStatusRequest\x1a7.openshell.v1.GetSandboxDelegatedIdentityStatusResponse\" \x82\xb5\x18\x1c\n" + + "\x06bearer\x12\x04user\"\fsandbox:read\x12\xb4\x01\n" + + " WithdrawSandboxDelegatedIdentity\x125.openshell.v1.WithdrawSandboxDelegatedIdentityRequest\x1a6.openshell.v1.WithdrawSandboxDelegatedIdentityResponse\"!\x82\xb5\x18\x1d\n" + + "\x06bearer\x12\x04user\"\rsandbox:write\x12\xae\x01\n" + + "\x1eExtendSandboxDelegatedIdentity\x123.openshell.v1.ExtendSandboxDelegatedIdentityRequest\x1a4.openshell.v1.ExtendSandboxDelegatedIdentityResponse\"!\x82\xb5\x18\x1d\n" + "\x06bearer\x12\x04user\"\rsandbox:write\x12n\n" + "\n" + "GetSandbox\x12\x1f.openshell.v1.GetSandboxRequest\x1a\x1d.openshell.v1.SandboxResponse\" \x82\xb5\x18\x1c\n" + @@ -14178,7 +15783,15 @@ const file_openshell_proto_rawDesc = "" + "\x18ConfigureProviderRefresh\x12-.openshell.v1.ConfigureProviderRefreshRequest\x1a..openshell.v1.ConfigureProviderRefreshResponse\"#\x82\xb5\x18\x1f\n" + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\x9e\x01\n" + "\x18RotateProviderCredential\x12-.openshell.v1.RotateProviderCredentialRequest\x1a..openshell.v1.RotateProviderCredentialResponse\"#\x82\xb5\x18\x1f\n" + - "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\x95\x01\n" + + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\xbe\x01\n" + + " ListDelegatedIdentityCredentials\x125.openshell.v1.ListDelegatedIdentityCredentialsRequest\x1a6.openshell.v1.ListDelegatedIdentityCredentialsResponse\"+\x82\xb5\x18'\n" + + "\x06bearer\x1a\x0eplatform_admin\"\rprovider:read\x12\xca\x01\n" + + "$GetDelegatedIdentityCredentialStatus\x129.openshell.v1.GetDelegatedIdentityCredentialStatusRequest\x1a:.openshell.v1.GetDelegatedIdentityCredentialStatusResponse\"+\x82\xb5\x18'\n" + + "\x06bearer\x1a\x0eplatform_admin\"\rprovider:read\x12\xc2\x01\n" + + "!RevokeDelegatedIdentityCredential\x126.openshell.v1.RevokeDelegatedIdentityCredentialRequest\x1a7.openshell.v1.RevokeDelegatedIdentityCredentialResponse\",\x82\xb5\x18(\n" + + "\x06bearer\x1a\x0eplatform_admin\"\x0eprovider:write\x12\xc2\x01\n" + + "!DeleteDelegatedIdentityCredential\x126.openshell.v1.DeleteDelegatedIdentityCredentialRequest\x1a7.openshell.v1.DeleteDelegatedIdentityCredentialResponse\",\x82\xb5\x18(\n" + + "\x06bearer\x1a\x0eplatform_admin\"\x0eprovider:write\x12\x95\x01\n" + "\x15DeleteProviderRefresh\x12*.openshell.v1.DeleteProviderRefreshRequest\x1a+.openshell.v1.DeleteProviderRefreshResponse\"#\x82\xb5\x18\x1f\n" + "\x06bearer\x12\x05admin\"\x0eprovider:write\x12\x80\x01\n" + "\x0eDeleteProvider\x12#.openshell.v1.DeleteProviderRequest\x1a$.openshell.v1.DeleteProviderResponse\"#\x82\xb5\x18\x1f\n" + @@ -14198,6 +15811,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" + @@ -14258,529 +15873,581 @@ 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, 209) +var file_openshell_proto_enumTypes = make([]protoimpl.EnumInfo, 7) +var file_openshell_proto_msgTypes = make([]protoimpl.MessageInfo, 231) 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 - (*StopSandboxRequest)(nil), // 33: openshell.v1.StopSandboxRequest - (*StartSandboxRequest)(nil), // 34: openshell.v1.StartSandboxRequest - (*SandboxResponse)(nil), // 35: openshell.v1.SandboxResponse - (*ListSandboxesResponse)(nil), // 36: openshell.v1.ListSandboxesResponse - (*ListSandboxProvidersResponse)(nil), // 37: openshell.v1.ListSandboxProvidersResponse - (*AttachSandboxProviderResponse)(nil), // 38: openshell.v1.AttachSandboxProviderResponse - (*DetachSandboxProviderResponse)(nil), // 39: openshell.v1.DetachSandboxProviderResponse - (*DeleteSandboxResponse)(nil), // 40: openshell.v1.DeleteSandboxResponse - (*CreateSshSessionRequest)(nil), // 41: openshell.v1.CreateSshSessionRequest - (*CreateSshSessionResponse)(nil), // 42: openshell.v1.CreateSshSessionResponse - (*ExposeServiceRequest)(nil), // 43: openshell.v1.ExposeServiceRequest - (*GetServiceRequest)(nil), // 44: openshell.v1.GetServiceRequest - (*ListServicesRequest)(nil), // 45: openshell.v1.ListServicesRequest - (*ListServicesResponse)(nil), // 46: openshell.v1.ListServicesResponse - (*DeleteServiceRequest)(nil), // 47: openshell.v1.DeleteServiceRequest - (*DeleteServiceResponse)(nil), // 48: openshell.v1.DeleteServiceResponse - (*ServiceEndpoint)(nil), // 49: openshell.v1.ServiceEndpoint - (*ServiceEndpointResponse)(nil), // 50: openshell.v1.ServiceEndpointResponse - (*RevokeSshSessionRequest)(nil), // 51: openshell.v1.RevokeSshSessionRequest - (*RevokeSshSessionResponse)(nil), // 52: openshell.v1.RevokeSshSessionResponse - (*ExecSandboxRequest)(nil), // 53: openshell.v1.ExecSandboxRequest - (*ExecSandboxStdout)(nil), // 54: openshell.v1.ExecSandboxStdout - (*ExecSandboxStderr)(nil), // 55: openshell.v1.ExecSandboxStderr - (*ExecSandboxExit)(nil), // 56: openshell.v1.ExecSandboxExit - (*ExecSandboxEvent)(nil), // 57: openshell.v1.ExecSandboxEvent - (*TcpForwardInit)(nil), // 58: openshell.v1.TcpForwardInit - (*TcpForwardFrame)(nil), // 59: openshell.v1.TcpForwardFrame - (*ExecSandboxInput)(nil), // 60: openshell.v1.ExecSandboxInput - (*ExecSandboxWindowResize)(nil), // 61: openshell.v1.ExecSandboxWindowResize - (*SshSession)(nil), // 62: openshell.v1.SshSession - (*WatchSandboxRequest)(nil), // 63: openshell.v1.WatchSandboxRequest - (*SandboxStreamEvent)(nil), // 64: openshell.v1.SandboxStreamEvent - (*SandboxLogLine)(nil), // 65: openshell.v1.SandboxLogLine - (*SandboxStreamWarning)(nil), // 66: openshell.v1.SandboxStreamWarning - (*CreateProviderRequest)(nil), // 67: openshell.v1.CreateProviderRequest - (*GetProviderRequest)(nil), // 68: openshell.v1.GetProviderRequest - (*ListProvidersRequest)(nil), // 69: openshell.v1.ListProvidersRequest - (*UpdateProviderRequest)(nil), // 70: openshell.v1.UpdateProviderRequest - (*DeleteProviderRequest)(nil), // 71: openshell.v1.DeleteProviderRequest - (*ProviderResponse)(nil), // 72: openshell.v1.ProviderResponse - (*ListProvidersResponse)(nil), // 73: openshell.v1.ListProvidersResponse - (*ListProviderProfilesRequest)(nil), // 74: openshell.v1.ListProviderProfilesRequest - (*GetProviderProfileRequest)(nil), // 75: openshell.v1.GetProviderProfileRequest - (*ProviderProfileImportItem)(nil), // 76: openshell.v1.ProviderProfileImportItem - (*ProviderProfileDiagnostic)(nil), // 77: openshell.v1.ProviderProfileDiagnostic - (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 78: openshell.v1.ProviderCredentialTokenGrantAudienceOverride - (*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 - (*UpdateConfigRequest)(nil), // 112: openshell.v1.UpdateConfigRequest - (*PolicyMergeOperation)(nil), // 113: openshell.v1.PolicyMergeOperation - (*AddNetworkRule)(nil), // 114: openshell.v1.AddNetworkRule - (*RemoveNetworkEndpoint)(nil), // 115: openshell.v1.RemoveNetworkEndpoint - (*RemoveNetworkRule)(nil), // 116: openshell.v1.RemoveNetworkRule - (*AddDenyRules)(nil), // 117: openshell.v1.AddDenyRules - (*AddAllowRules)(nil), // 118: openshell.v1.AddAllowRules - (*RemoveNetworkBinary)(nil), // 119: openshell.v1.RemoveNetworkBinary - (*UpdateConfigResponse)(nil), // 120: openshell.v1.UpdateConfigResponse - (*GetSandboxPolicyStatusRequest)(nil), // 121: openshell.v1.GetSandboxPolicyStatusRequest - (*GetSandboxPolicyStatusResponse)(nil), // 122: openshell.v1.GetSandboxPolicyStatusResponse - (*ListSandboxPoliciesRequest)(nil), // 123: openshell.v1.ListSandboxPoliciesRequest - (*ListSandboxPoliciesResponse)(nil), // 124: openshell.v1.ListSandboxPoliciesResponse - (*ReportPolicyStatusRequest)(nil), // 125: openshell.v1.ReportPolicyStatusRequest - (*ReportPolicyStatusResponse)(nil), // 126: openshell.v1.ReportPolicyStatusResponse - (*SandboxPolicyRevision)(nil), // 127: openshell.v1.SandboxPolicyRevision - (*GetSandboxLogsRequest)(nil), // 128: openshell.v1.GetSandboxLogsRequest - (*PushSandboxLogsRequest)(nil), // 129: openshell.v1.PushSandboxLogsRequest - (*PushSandboxLogsResponse)(nil), // 130: openshell.v1.PushSandboxLogsResponse - (*GetSandboxLogsResponse)(nil), // 131: openshell.v1.GetSandboxLogsResponse - (*SupervisorMessage)(nil), // 132: openshell.v1.SupervisorMessage - (*GatewayMessage)(nil), // 133: openshell.v1.GatewayMessage - (*SupervisorHello)(nil), // 134: openshell.v1.SupervisorHello - (*SessionAccepted)(nil), // 135: openshell.v1.SessionAccepted - (*SessionRejected)(nil), // 136: openshell.v1.SessionRejected - (*SupervisorHeartbeat)(nil), // 137: openshell.v1.SupervisorHeartbeat - (*GatewayHeartbeat)(nil), // 138: openshell.v1.GatewayHeartbeat - (*RelayOpen)(nil), // 139: openshell.v1.RelayOpen - (*SshRelayTarget)(nil), // 140: openshell.v1.SshRelayTarget - (*TcpRelayTarget)(nil), // 141: openshell.v1.TcpRelayTarget - (*RelayInit)(nil), // 142: openshell.v1.RelayInit - (*RelayFrame)(nil), // 143: openshell.v1.RelayFrame - (*RelayOpenResult)(nil), // 144: openshell.v1.RelayOpenResult - (*RelayClose)(nil), // 145: openshell.v1.RelayClose - (*L7RequestSample)(nil), // 146: openshell.v1.L7RequestSample - (*DenialSummary)(nil), // 147: openshell.v1.DenialSummary - (*DenialGroupCount)(nil), // 148: openshell.v1.DenialGroupCount - (*NetworkActivitySummary)(nil), // 149: openshell.v1.NetworkActivitySummary - (*PolicyChunk)(nil), // 150: openshell.v1.PolicyChunk - (*DraftPolicyUpdate)(nil), // 151: openshell.v1.DraftPolicyUpdate - (*SubmitPolicyAnalysisRequest)(nil), // 152: openshell.v1.SubmitPolicyAnalysisRequest - (*SubmitPolicyAnalysisResponse)(nil), // 153: openshell.v1.SubmitPolicyAnalysisResponse - (*GetDraftPolicyRequest)(nil), // 154: openshell.v1.GetDraftPolicyRequest - (*GetDraftPolicyResponse)(nil), // 155: openshell.v1.GetDraftPolicyResponse - (*ApproveDraftChunkRequest)(nil), // 156: openshell.v1.ApproveDraftChunkRequest - (*ApproveDraftChunkResponse)(nil), // 157: openshell.v1.ApproveDraftChunkResponse - (*RejectDraftChunkRequest)(nil), // 158: openshell.v1.RejectDraftChunkRequest - (*RejectDraftChunkResponse)(nil), // 159: openshell.v1.RejectDraftChunkResponse - (*ApproveAllDraftChunksRequest)(nil), // 160: openshell.v1.ApproveAllDraftChunksRequest - (*ApproveAllDraftChunksResponse)(nil), // 161: openshell.v1.ApproveAllDraftChunksResponse - (*EditDraftChunkRequest)(nil), // 162: openshell.v1.EditDraftChunkRequest - (*EditDraftChunkResponse)(nil), // 163: openshell.v1.EditDraftChunkResponse - (*UndoDraftChunkRequest)(nil), // 164: openshell.v1.UndoDraftChunkRequest - (*UndoDraftChunkResponse)(nil), // 165: openshell.v1.UndoDraftChunkResponse - (*ClearDraftChunksRequest)(nil), // 166: openshell.v1.ClearDraftChunksRequest - (*ClearDraftChunksResponse)(nil), // 167: openshell.v1.ClearDraftChunksResponse - (*GetDraftHistoryRequest)(nil), // 168: openshell.v1.GetDraftHistoryRequest - (*DraftHistoryEntry)(nil), // 169: openshell.v1.DraftHistoryEntry - (*GetDraftHistoryResponse)(nil), // 170: openshell.v1.GetDraftHistoryResponse - (*PolicyRevisionPayload)(nil), // 171: openshell.v1.PolicyRevisionPayload - (*DraftChunkPayload)(nil), // 172: openshell.v1.DraftChunkPayload - (*StoredPolicyRevision)(nil), // 173: openshell.v1.StoredPolicyRevision - (*StoredDraftChunk)(nil), // 174: openshell.v1.StoredDraftChunk - (*CreateWorkspaceRequest)(nil), // 175: openshell.v1.CreateWorkspaceRequest - (*CreateWorkspaceResponse)(nil), // 176: openshell.v1.CreateWorkspaceResponse - (*GetWorkspaceRequest)(nil), // 177: openshell.v1.GetWorkspaceRequest - (*GetWorkspaceResponse)(nil), // 178: openshell.v1.GetWorkspaceResponse - (*ListWorkspacesRequest)(nil), // 179: openshell.v1.ListWorkspacesRequest - (*ListWorkspacesResponse)(nil), // 180: openshell.v1.ListWorkspacesResponse - (*DeleteWorkspaceRequest)(nil), // 181: openshell.v1.DeleteWorkspaceRequest - (*DeleteWorkspaceResponse)(nil), // 182: openshell.v1.DeleteWorkspaceResponse - (*WorkspaceMember)(nil), // 183: openshell.v1.WorkspaceMember - (*AddWorkspaceMemberRequest)(nil), // 184: openshell.v1.AddWorkspaceMemberRequest - (*AddWorkspaceMemberResponse)(nil), // 185: openshell.v1.AddWorkspaceMemberResponse - (*RemoveWorkspaceMemberRequest)(nil), // 186: openshell.v1.RemoveWorkspaceMemberRequest - (*RemoveWorkspaceMemberResponse)(nil), // 187: openshell.v1.RemoveWorkspaceMemberResponse - (*ListWorkspaceMembersRequest)(nil), // 188: openshell.v1.ListWorkspaceMembersRequest - (*ListWorkspaceMembersResponse)(nil), // 189: openshell.v1.ListWorkspaceMembersResponse - (*ExtensionServiceCredential)(nil), // 190: openshell.v1.ExtensionServiceCredential - nil, // 191: openshell.v1.SandboxSpec.EnvironmentEntry - nil, // 192: openshell.v1.SandboxTemplate.LabelsEntry - nil, // 193: openshell.v1.SandboxTemplate.AnnotationsEntry - nil, // 194: openshell.v1.SandboxTemplate.EnvironmentEntry - nil, // 195: openshell.v1.PlatformEvent.MetadataEntry - nil, // 196: openshell.v1.CreateSandboxRequest.LabelsEntry - nil, // 197: openshell.v1.CreateSandboxRequest.AnnotationsEntry - nil, // 198: openshell.v1.ExecSandboxRequest.EnvironmentEntry - nil, // 199: openshell.v1.SandboxLogLine.FieldsEntry - nil, // 200: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - nil, // 201: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - nil, // 202: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - nil, // 203: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - nil, // 204: openshell.v1.ProviderProfile.AnnotationsEntry - nil, // 205: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - nil, // 206: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - nil, // 207: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - nil, // 208: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - nil, // 209: openshell.v1.UpdateConfigRequest.AnnotationsEntry - nil, // 210: openshell.v1.UpdateConfigResponse.AnnotationsEntry - nil, // 211: openshell.v1.SandboxPolicyRevision.ProvenanceEntry - nil, // 212: openshell.v1.PolicyRevisionPayload.ProvenanceEntry - nil, // 213: openshell.v1.StoredPolicyRevision.ProvenanceEntry - nil, // 214: openshell.v1.CreateWorkspaceRequest.LabelsEntry - (*datamodelv1.ObjectMeta)(nil), // 215: openshell.datamodel.v1.ObjectMeta - (*sandboxv1.SandboxPolicy)(nil), // 216: openshell.sandbox.v1.SandboxPolicy - (*structpb.Struct)(nil), // 217: google.protobuf.Struct - (*datamodelv1.Provider)(nil), // 218: openshell.datamodel.v1.Provider - (*sandboxv1.NetworkEndpoint)(nil), // 219: openshell.sandbox.v1.NetworkEndpoint - (*sandboxv1.NetworkBinary)(nil), // 220: openshell.sandbox.v1.NetworkBinary - (*sandboxv1.SettingValue)(nil), // 221: openshell.sandbox.v1.SettingValue - (*sandboxv1.NetworkPolicyRule)(nil), // 222: openshell.sandbox.v1.NetworkPolicyRule - (*sandboxv1.L7DenyRule)(nil), // 223: openshell.sandbox.v1.L7DenyRule - (*sandboxv1.L7Rule)(nil), // 224: openshell.sandbox.v1.L7Rule - (*datamodelv1.Workspace)(nil), // 225: openshell.datamodel.v1.Workspace - (*sandboxv1.GetSandboxConfigRequest)(nil), // 226: openshell.sandbox.v1.GetSandboxConfigRequest - (*sandboxv1.GetGatewayConfigRequest)(nil), // 227: openshell.sandbox.v1.GetGatewayConfigRequest - (*sandboxv1.GetSandboxConfigResponse)(nil), // 228: openshell.sandbox.v1.GetSandboxConfigResponse - (*sandboxv1.GetGatewayConfigResponse)(nil), // 229: 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 + (*SandboxDelegatedIdentity)(nil), // 20: openshell.v1.SandboxDelegatedIdentity + (*SandboxDelegatedIdentityRecord)(nil), // 21: openshell.v1.SandboxDelegatedIdentityRecord + (*SandboxSpec)(nil), // 22: openshell.v1.SandboxSpec + (*ResourceRequirements)(nil), // 23: openshell.v1.ResourceRequirements + (*GpuResourceRequirements)(nil), // 24: openshell.v1.GpuResourceRequirements + (*SandboxTemplate)(nil), // 25: openshell.v1.SandboxTemplate + (*SandboxStatus)(nil), // 26: openshell.v1.SandboxStatus + (*SandboxCondition)(nil), // 27: openshell.v1.SandboxCondition + (*PlatformEvent)(nil), // 28: openshell.v1.PlatformEvent + (*CreateSandboxRequest)(nil), // 29: openshell.v1.CreateSandboxRequest + (*DelegatedIdentityRequest)(nil), // 30: openshell.v1.DelegatedIdentityRequest + (*GetSandboxDelegatedIdentityStatusRequest)(nil), // 31: openshell.v1.GetSandboxDelegatedIdentityStatusRequest + (*GetSandboxDelegatedIdentityStatusResponse)(nil), // 32: openshell.v1.GetSandboxDelegatedIdentityStatusResponse + (*WithdrawSandboxDelegatedIdentityRequest)(nil), // 33: openshell.v1.WithdrawSandboxDelegatedIdentityRequest + (*WithdrawSandboxDelegatedIdentityResponse)(nil), // 34: openshell.v1.WithdrawSandboxDelegatedIdentityResponse + (*ExtendSandboxDelegatedIdentityRequest)(nil), // 35: openshell.v1.ExtendSandboxDelegatedIdentityRequest + (*ExtendSandboxDelegatedIdentityResponse)(nil), // 36: openshell.v1.ExtendSandboxDelegatedIdentityResponse + (*GetSandboxRequest)(nil), // 37: openshell.v1.GetSandboxRequest + (*ListSandboxesRequest)(nil), // 38: openshell.v1.ListSandboxesRequest + (*ListSandboxProvidersRequest)(nil), // 39: openshell.v1.ListSandboxProvidersRequest + (*AttachSandboxProviderRequest)(nil), // 40: openshell.v1.AttachSandboxProviderRequest + (*DetachSandboxProviderRequest)(nil), // 41: openshell.v1.DetachSandboxProviderRequest + (*DeleteSandboxRequest)(nil), // 42: openshell.v1.DeleteSandboxRequest + (*StopSandboxRequest)(nil), // 43: openshell.v1.StopSandboxRequest + (*StartSandboxRequest)(nil), // 44: openshell.v1.StartSandboxRequest + (*SandboxResponse)(nil), // 45: openshell.v1.SandboxResponse + (*ListSandboxesResponse)(nil), // 46: openshell.v1.ListSandboxesResponse + (*ListSandboxProvidersResponse)(nil), // 47: openshell.v1.ListSandboxProvidersResponse + (*AttachSandboxProviderResponse)(nil), // 48: openshell.v1.AttachSandboxProviderResponse + (*DetachSandboxProviderResponse)(nil), // 49: openshell.v1.DetachSandboxProviderResponse + (*DeleteSandboxResponse)(nil), // 50: openshell.v1.DeleteSandboxResponse + (*CreateSshSessionRequest)(nil), // 51: openshell.v1.CreateSshSessionRequest + (*CreateSshSessionResponse)(nil), // 52: openshell.v1.CreateSshSessionResponse + (*ExposeServiceRequest)(nil), // 53: openshell.v1.ExposeServiceRequest + (*GetServiceRequest)(nil), // 54: openshell.v1.GetServiceRequest + (*ListServicesRequest)(nil), // 55: openshell.v1.ListServicesRequest + (*ListServicesResponse)(nil), // 56: openshell.v1.ListServicesResponse + (*DeleteServiceRequest)(nil), // 57: openshell.v1.DeleteServiceRequest + (*DeleteServiceResponse)(nil), // 58: openshell.v1.DeleteServiceResponse + (*ServiceEndpoint)(nil), // 59: openshell.v1.ServiceEndpoint + (*ServiceEndpointResponse)(nil), // 60: openshell.v1.ServiceEndpointResponse + (*RevokeSshSessionRequest)(nil), // 61: openshell.v1.RevokeSshSessionRequest + (*RevokeSshSessionResponse)(nil), // 62: openshell.v1.RevokeSshSessionResponse + (*ExecSandboxRequest)(nil), // 63: openshell.v1.ExecSandboxRequest + (*ExecSandboxStdout)(nil), // 64: openshell.v1.ExecSandboxStdout + (*ExecSandboxStderr)(nil), // 65: openshell.v1.ExecSandboxStderr + (*ExecSandboxExit)(nil), // 66: openshell.v1.ExecSandboxExit + (*ExecSandboxEvent)(nil), // 67: openshell.v1.ExecSandboxEvent + (*TcpForwardInit)(nil), // 68: openshell.v1.TcpForwardInit + (*TcpForwardFrame)(nil), // 69: openshell.v1.TcpForwardFrame + (*ExecSandboxInput)(nil), // 70: openshell.v1.ExecSandboxInput + (*ExecSandboxWindowResize)(nil), // 71: openshell.v1.ExecSandboxWindowResize + (*SshSession)(nil), // 72: openshell.v1.SshSession + (*WatchSandboxRequest)(nil), // 73: openshell.v1.WatchSandboxRequest + (*SandboxStreamEvent)(nil), // 74: openshell.v1.SandboxStreamEvent + (*SandboxLogLine)(nil), // 75: openshell.v1.SandboxLogLine + (*SandboxStreamWarning)(nil), // 76: openshell.v1.SandboxStreamWarning + (*CreateProviderRequest)(nil), // 77: openshell.v1.CreateProviderRequest + (*GetProviderRequest)(nil), // 78: openshell.v1.GetProviderRequest + (*ListProvidersRequest)(nil), // 79: openshell.v1.ListProvidersRequest + (*UpdateProviderRequest)(nil), // 80: openshell.v1.UpdateProviderRequest + (*DeleteProviderRequest)(nil), // 81: openshell.v1.DeleteProviderRequest + (*ProviderResponse)(nil), // 82: openshell.v1.ProviderResponse + (*ListProvidersResponse)(nil), // 83: openshell.v1.ListProvidersResponse + (*ListProviderProfilesRequest)(nil), // 84: openshell.v1.ListProviderProfilesRequest + (*GetProviderProfileRequest)(nil), // 85: openshell.v1.GetProviderProfileRequest + (*ProviderProfileImportItem)(nil), // 86: openshell.v1.ProviderProfileImportItem + (*ProviderProfileDiagnostic)(nil), // 87: openshell.v1.ProviderProfileDiagnostic + (*ProviderCredentialTokenGrantAudienceOverride)(nil), // 88: openshell.v1.ProviderCredentialTokenGrantAudienceOverride + (*ProviderCredentialTokenGrantSubjectToken)(nil), // 89: openshell.v1.ProviderCredentialTokenGrantSubjectToken + (*ProviderCredentialTokenGrant)(nil), // 90: openshell.v1.ProviderCredentialTokenGrant + (*ProviderProfileCredential)(nil), // 91: openshell.v1.ProviderProfileCredential + (*ProviderCredentialRefreshMaterial)(nil), // 92: openshell.v1.ProviderCredentialRefreshMaterial + (*ProviderCredentialRefreshOutput)(nil), // 93: openshell.v1.ProviderCredentialRefreshOutput + (*ProviderCredentialRefresh)(nil), // 94: openshell.v1.ProviderCredentialRefresh + (*ProviderCredentialRefreshStatus)(nil), // 95: openshell.v1.ProviderCredentialRefreshStatus + (*ProviderProfileDiscovery)(nil), // 96: openshell.v1.ProviderProfileDiscovery + (*StoredProviderCredentialRefreshState)(nil), // 97: openshell.v1.StoredProviderCredentialRefreshState + (*DelegatedIdentityCredential)(nil), // 98: openshell.v1.DelegatedIdentityCredential + (*DelegatedIdentityCredentialSummary)(nil), // 99: openshell.v1.DelegatedIdentityCredentialSummary + (*ListDelegatedIdentityCredentialsRequest)(nil), // 100: openshell.v1.ListDelegatedIdentityCredentialsRequest + (*ListDelegatedIdentityCredentialsResponse)(nil), // 101: openshell.v1.ListDelegatedIdentityCredentialsResponse + (*GetDelegatedIdentityCredentialStatusRequest)(nil), // 102: openshell.v1.GetDelegatedIdentityCredentialStatusRequest + (*GetDelegatedIdentityCredentialStatusResponse)(nil), // 103: openshell.v1.GetDelegatedIdentityCredentialStatusResponse + (*RevokeDelegatedIdentityCredentialRequest)(nil), // 104: openshell.v1.RevokeDelegatedIdentityCredentialRequest + (*RevokeDelegatedIdentityCredentialResponse)(nil), // 105: openshell.v1.RevokeDelegatedIdentityCredentialResponse + (*DeleteDelegatedIdentityCredentialRequest)(nil), // 106: openshell.v1.DeleteDelegatedIdentityCredentialRequest + (*DeleteDelegatedIdentityCredentialResponse)(nil), // 107: openshell.v1.DeleteDelegatedIdentityCredentialResponse + (*GetProviderRefreshStatusRequest)(nil), // 108: openshell.v1.GetProviderRefreshStatusRequest + (*GetProviderRefreshStatusResponse)(nil), // 109: openshell.v1.GetProviderRefreshStatusResponse + (*ConfigureProviderRefreshRequest)(nil), // 110: openshell.v1.ConfigureProviderRefreshRequest + (*ConfigureProviderRefreshResponse)(nil), // 111: openshell.v1.ConfigureProviderRefreshResponse + (*RotateProviderCredentialRequest)(nil), // 112: openshell.v1.RotateProviderCredentialRequest + (*RotateProviderCredentialResponse)(nil), // 113: openshell.v1.RotateProviderCredentialResponse + (*DeleteProviderRefreshRequest)(nil), // 114: openshell.v1.DeleteProviderRefreshRequest + (*DeleteProviderRefreshResponse)(nil), // 115: openshell.v1.DeleteProviderRefreshResponse + (*ProviderProfile)(nil), // 116: openshell.v1.ProviderProfile + (*StoredProviderProfile)(nil), // 117: openshell.v1.StoredProviderProfile + (*ProviderProfileResponse)(nil), // 118: openshell.v1.ProviderProfileResponse + (*ListProviderProfilesResponse)(nil), // 119: openshell.v1.ListProviderProfilesResponse + (*ImportProviderProfilesRequest)(nil), // 120: openshell.v1.ImportProviderProfilesRequest + (*ImportProviderProfilesResponse)(nil), // 121: openshell.v1.ImportProviderProfilesResponse + (*UpdateProviderProfilesRequest)(nil), // 122: openshell.v1.UpdateProviderProfilesRequest + (*UpdateProviderProfilesResponse)(nil), // 123: openshell.v1.UpdateProviderProfilesResponse + (*LintProviderProfilesRequest)(nil), // 124: openshell.v1.LintProviderProfilesRequest + (*LintProviderProfilesResponse)(nil), // 125: openshell.v1.LintProviderProfilesResponse + (*DeleteProviderResponse)(nil), // 126: openshell.v1.DeleteProviderResponse + (*DeleteProviderProfileRequest)(nil), // 127: openshell.v1.DeleteProviderProfileRequest + (*DeleteProviderProfileResponse)(nil), // 128: openshell.v1.DeleteProviderProfileResponse + (*GetSandboxProviderEnvironmentRequest)(nil), // 129: openshell.v1.GetSandboxProviderEnvironmentRequest + (*StaticCredentialEndpointBinding)(nil), // 130: openshell.v1.StaticCredentialEndpointBinding + (*StaticCredentialBinding)(nil), // 131: openshell.v1.StaticCredentialBinding + (*GetSandboxProviderEnvironmentResponse)(nil), // 132: openshell.v1.GetSandboxProviderEnvironmentResponse + (*ExchangeProviderSubjectTokenRequest)(nil), // 133: openshell.v1.ExchangeProviderSubjectTokenRequest + (*ExchangeProviderSubjectTokenResponse)(nil), // 134: openshell.v1.ExchangeProviderSubjectTokenResponse + (*UpdateConfigRequest)(nil), // 135: openshell.v1.UpdateConfigRequest + (*PolicyMergeOperation)(nil), // 136: openshell.v1.PolicyMergeOperation + (*AddNetworkRule)(nil), // 137: openshell.v1.AddNetworkRule + (*RemoveNetworkEndpoint)(nil), // 138: openshell.v1.RemoveNetworkEndpoint + (*RemoveNetworkRule)(nil), // 139: openshell.v1.RemoveNetworkRule + (*AddDenyRules)(nil), // 140: openshell.v1.AddDenyRules + (*AddAllowRules)(nil), // 141: openshell.v1.AddAllowRules + (*RemoveNetworkBinary)(nil), // 142: openshell.v1.RemoveNetworkBinary + (*UpdateConfigResponse)(nil), // 143: openshell.v1.UpdateConfigResponse + (*GetSandboxPolicyStatusRequest)(nil), // 144: openshell.v1.GetSandboxPolicyStatusRequest + (*GetSandboxPolicyStatusResponse)(nil), // 145: openshell.v1.GetSandboxPolicyStatusResponse + (*ListSandboxPoliciesRequest)(nil), // 146: openshell.v1.ListSandboxPoliciesRequest + (*ListSandboxPoliciesResponse)(nil), // 147: openshell.v1.ListSandboxPoliciesResponse + (*ReportPolicyStatusRequest)(nil), // 148: openshell.v1.ReportPolicyStatusRequest + (*ReportPolicyStatusResponse)(nil), // 149: openshell.v1.ReportPolicyStatusResponse + (*SandboxPolicyRevision)(nil), // 150: openshell.v1.SandboxPolicyRevision + (*GetSandboxLogsRequest)(nil), // 151: openshell.v1.GetSandboxLogsRequest + (*PushSandboxLogsRequest)(nil), // 152: openshell.v1.PushSandboxLogsRequest + (*PushSandboxLogsResponse)(nil), // 153: openshell.v1.PushSandboxLogsResponse + (*GetSandboxLogsResponse)(nil), // 154: openshell.v1.GetSandboxLogsResponse + (*SupervisorMessage)(nil), // 155: openshell.v1.SupervisorMessage + (*GatewayMessage)(nil), // 156: openshell.v1.GatewayMessage + (*SupervisorHello)(nil), // 157: openshell.v1.SupervisorHello + (*SessionAccepted)(nil), // 158: openshell.v1.SessionAccepted + (*SessionRejected)(nil), // 159: openshell.v1.SessionRejected + (*SupervisorHeartbeat)(nil), // 160: openshell.v1.SupervisorHeartbeat + (*GatewayHeartbeat)(nil), // 161: openshell.v1.GatewayHeartbeat + (*RelayOpen)(nil), // 162: openshell.v1.RelayOpen + (*SshRelayTarget)(nil), // 163: openshell.v1.SshRelayTarget + (*TcpRelayTarget)(nil), // 164: openshell.v1.TcpRelayTarget + (*RelayInit)(nil), // 165: openshell.v1.RelayInit + (*RelayFrame)(nil), // 166: openshell.v1.RelayFrame + (*RelayOpenResult)(nil), // 167: openshell.v1.RelayOpenResult + (*RelayClose)(nil), // 168: openshell.v1.RelayClose + (*L7RequestSample)(nil), // 169: openshell.v1.L7RequestSample + (*DenialSummary)(nil), // 170: openshell.v1.DenialSummary + (*DenialGroupCount)(nil), // 171: openshell.v1.DenialGroupCount + (*NetworkActivitySummary)(nil), // 172: openshell.v1.NetworkActivitySummary + (*PolicyChunk)(nil), // 173: openshell.v1.PolicyChunk + (*DraftPolicyUpdate)(nil), // 174: openshell.v1.DraftPolicyUpdate + (*SubmitPolicyAnalysisRequest)(nil), // 175: openshell.v1.SubmitPolicyAnalysisRequest + (*SubmitPolicyAnalysisResponse)(nil), // 176: openshell.v1.SubmitPolicyAnalysisResponse + (*GetDraftPolicyRequest)(nil), // 177: openshell.v1.GetDraftPolicyRequest + (*GetDraftPolicyResponse)(nil), // 178: openshell.v1.GetDraftPolicyResponse + (*ApproveDraftChunkRequest)(nil), // 179: openshell.v1.ApproveDraftChunkRequest + (*ApproveDraftChunkResponse)(nil), // 180: openshell.v1.ApproveDraftChunkResponse + (*RejectDraftChunkRequest)(nil), // 181: openshell.v1.RejectDraftChunkRequest + (*RejectDraftChunkResponse)(nil), // 182: openshell.v1.RejectDraftChunkResponse + (*ApproveAllDraftChunksRequest)(nil), // 183: openshell.v1.ApproveAllDraftChunksRequest + (*ApproveAllDraftChunksResponse)(nil), // 184: openshell.v1.ApproveAllDraftChunksResponse + (*EditDraftChunkRequest)(nil), // 185: openshell.v1.EditDraftChunkRequest + (*EditDraftChunkResponse)(nil), // 186: openshell.v1.EditDraftChunkResponse + (*UndoDraftChunkRequest)(nil), // 187: openshell.v1.UndoDraftChunkRequest + (*UndoDraftChunkResponse)(nil), // 188: openshell.v1.UndoDraftChunkResponse + (*ClearDraftChunksRequest)(nil), // 189: openshell.v1.ClearDraftChunksRequest + (*ClearDraftChunksResponse)(nil), // 190: openshell.v1.ClearDraftChunksResponse + (*GetDraftHistoryRequest)(nil), // 191: openshell.v1.GetDraftHistoryRequest + (*DraftHistoryEntry)(nil), // 192: openshell.v1.DraftHistoryEntry + (*GetDraftHistoryResponse)(nil), // 193: openshell.v1.GetDraftHistoryResponse + (*PolicyRevisionPayload)(nil), // 194: openshell.v1.PolicyRevisionPayload + (*DraftChunkPayload)(nil), // 195: openshell.v1.DraftChunkPayload + (*StoredPolicyRevision)(nil), // 196: openshell.v1.StoredPolicyRevision + (*StoredDraftChunk)(nil), // 197: openshell.v1.StoredDraftChunk + (*CreateWorkspaceRequest)(nil), // 198: openshell.v1.CreateWorkspaceRequest + (*CreateWorkspaceResponse)(nil), // 199: openshell.v1.CreateWorkspaceResponse + (*GetWorkspaceRequest)(nil), // 200: openshell.v1.GetWorkspaceRequest + (*GetWorkspaceResponse)(nil), // 201: openshell.v1.GetWorkspaceResponse + (*ListWorkspacesRequest)(nil), // 202: openshell.v1.ListWorkspacesRequest + (*ListWorkspacesResponse)(nil), // 203: openshell.v1.ListWorkspacesResponse + (*DeleteWorkspaceRequest)(nil), // 204: openshell.v1.DeleteWorkspaceRequest + (*DeleteWorkspaceResponse)(nil), // 205: openshell.v1.DeleteWorkspaceResponse + (*WorkspaceMember)(nil), // 206: openshell.v1.WorkspaceMember + (*AddWorkspaceMemberRequest)(nil), // 207: openshell.v1.AddWorkspaceMemberRequest + (*AddWorkspaceMemberResponse)(nil), // 208: openshell.v1.AddWorkspaceMemberResponse + (*RemoveWorkspaceMemberRequest)(nil), // 209: openshell.v1.RemoveWorkspaceMemberRequest + (*RemoveWorkspaceMemberResponse)(nil), // 210: openshell.v1.RemoveWorkspaceMemberResponse + (*ListWorkspaceMembersRequest)(nil), // 211: openshell.v1.ListWorkspaceMembersRequest + (*ListWorkspaceMembersResponse)(nil), // 212: openshell.v1.ListWorkspaceMembersResponse + (*ExtensionServiceCredential)(nil), // 213: openshell.v1.ExtensionServiceCredential + nil, // 214: openshell.v1.SandboxSpec.EnvironmentEntry + nil, // 215: openshell.v1.SandboxTemplate.LabelsEntry + nil, // 216: openshell.v1.SandboxTemplate.AnnotationsEntry + nil, // 217: openshell.v1.SandboxTemplate.EnvironmentEntry + nil, // 218: openshell.v1.PlatformEvent.MetadataEntry + nil, // 219: openshell.v1.CreateSandboxRequest.LabelsEntry + nil, // 220: openshell.v1.CreateSandboxRequest.AnnotationsEntry + nil, // 221: openshell.v1.ExecSandboxRequest.EnvironmentEntry + nil, // 222: openshell.v1.SandboxLogLine.FieldsEntry + nil, // 223: openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + nil, // 224: openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + nil, // 225: openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + nil, // 226: openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + nil, // 227: openshell.v1.ProviderProfile.AnnotationsEntry + nil, // 228: openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + nil, // 229: openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + nil, // 230: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + nil, // 231: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + nil, // 232: openshell.v1.UpdateConfigRequest.AnnotationsEntry + nil, // 233: openshell.v1.UpdateConfigResponse.AnnotationsEntry + nil, // 234: openshell.v1.SandboxPolicyRevision.ProvenanceEntry + nil, // 235: openshell.v1.PolicyRevisionPayload.ProvenanceEntry + nil, // 236: openshell.v1.StoredPolicyRevision.ProvenanceEntry + nil, // 237: openshell.v1.CreateWorkspaceRequest.LabelsEntry + (*datamodelv1.ObjectMeta)(nil), // 238: openshell.datamodel.v1.ObjectMeta + (*sandboxv1.SandboxPolicy)(nil), // 239: openshell.sandbox.v1.SandboxPolicy + (*structpb.Struct)(nil), // 240: google.protobuf.Struct + (*datamodelv1.Provider)(nil), // 241: openshell.datamodel.v1.Provider + (*sandboxv1.NetworkEndpoint)(nil), // 242: openshell.sandbox.v1.NetworkEndpoint + (*sandboxv1.NetworkBinary)(nil), // 243: openshell.sandbox.v1.NetworkBinary + (*sandboxv1.SettingValue)(nil), // 244: openshell.sandbox.v1.SettingValue + (*sandboxv1.NetworkPolicyRule)(nil), // 245: openshell.sandbox.v1.NetworkPolicyRule + (*sandboxv1.L7DenyRule)(nil), // 246: openshell.sandbox.v1.L7DenyRule + (*sandboxv1.L7Rule)(nil), // 247: openshell.sandbox.v1.L7Rule + (*datamodelv1.Workspace)(nil), // 248: openshell.datamodel.v1.Workspace + (*sandboxv1.GetSandboxConfigRequest)(nil), // 249: openshell.sandbox.v1.GetSandboxConfigRequest + (*sandboxv1.GetGatewayConfigRequest)(nil), // 250: openshell.sandbox.v1.GetGatewayConfigRequest + (*sandboxv1.GetSandboxConfigResponse)(nil), // 251: openshell.sandbox.v1.GetSandboxConfigResponse + (*sandboxv1.GetGatewayConfigResponse)(nil), // 252: openshell.sandbox.v1.GetGatewayConfigResponse } var file_openshell_proto_depIdxs = []int32{ - 190, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential - 4, // 1: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus - 4, // 2: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus - 16, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo - 17, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities - 215, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 19, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec - 23, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus - 191, // 8: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry - 22, // 9: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate - 216, // 10: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 20, // 11: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements - 21, // 12: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements - 192, // 13: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry - 193, // 14: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry - 194, // 15: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry - 217, // 16: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct - 217, // 17: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct - 24, // 18: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition - 0, // 19: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase - 195, // 20: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry - 19, // 21: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec - 196, // 22: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry - 197, // 23: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry - 18, // 24: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox - 18, // 25: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox - 218, // 26: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 18, // 27: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 18, // 28: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox - 50, // 29: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse - 215, // 30: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 49, // 31: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint - 198, // 32: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry - 54, // 33: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout - 55, // 34: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr - 56, // 35: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit - 140, // 36: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget - 141, // 37: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget - 58, // 38: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit - 53, // 39: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest - 61, // 40: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize - 215, // 41: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 18, // 42: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox - 65, // 43: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine - 25, // 44: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent - 66, // 45: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning - 151, // 46: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate - 199, // 47: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry - 218, // 48: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 218, // 49: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider - 200, // 50: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry - 218, // 51: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider - 218, // 52: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider - 95, // 53: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile - 78, // 54: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride - 83, // 55: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh - 79, // 56: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant - 1, // 57: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 81, // 58: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial - 82, // 59: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput - 1, // 60: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 215, // 61: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 1, // 62: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 201, // 63: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry - 202, // 64: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry - 84, // 65: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 1, // 66: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy - 203, // 67: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry - 84, // 68: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 84, // 69: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus - 2, // 70: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory - 80, // 71: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential - 219, // 72: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 220, // 73: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 85, // 74: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery - 204, // 75: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry - 215, // 76: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 95, // 77: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile - 95, // 78: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile - 95, // 79: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 76, // 80: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 77, // 81: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 95, // 82: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile - 76, // 83: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem - 77, // 84: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 95, // 85: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile - 76, // 86: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem - 77, // 87: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic - 109, // 88: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding - 205, // 89: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry - 206, // 90: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry - 207, // 91: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry - 208, // 92: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry - 216, // 93: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 221, // 94: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue - 113, // 95: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation - 209, // 96: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry - 114, // 97: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule - 115, // 98: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint - 116, // 99: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule - 117, // 100: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules - 118, // 101: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules - 119, // 102: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary - 222, // 103: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 223, // 104: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 224, // 105: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule - 210, // 106: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry - 127, // 107: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision - 127, // 108: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision - 3, // 109: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus - 3, // 110: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus - 216, // 111: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 211, // 112: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry - 65, // 113: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine - 65, // 114: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine - 134, // 115: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello - 137, // 116: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat - 144, // 117: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult - 145, // 118: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose - 135, // 119: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted - 136, // 120: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected - 138, // 121: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat - 139, // 122: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen - 145, // 123: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose - 140, // 124: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget - 141, // 125: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget - 142, // 126: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit - 146, // 127: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample - 148, // 128: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount - 222, // 129: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 147, // 130: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary - 150, // 131: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk - 149, // 132: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary - 150, // 133: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk - 222, // 134: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 169, // 135: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry - 216, // 136: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 212, // 137: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry - 222, // 138: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 213, // 139: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry - 214, // 140: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry - 225, // 141: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 225, // 142: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace - 225, // 143: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace - 215, // 144: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta - 5, // 145: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole - 5, // 146: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole - 183, // 147: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember - 183, // 148: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember - 80, // 149: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential - 110, // 150: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding - 10, // 151: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest - 12, // 152: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest - 14, // 153: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest - 26, // 154: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest - 27, // 155: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest - 28, // 156: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest - 29, // 157: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest - 30, // 158: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest - 31, // 159: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest - 32, // 160: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest - 33, // 161: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest - 34, // 162: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest - 41, // 163: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest - 43, // 164: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest - 44, // 165: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest - 45, // 166: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest - 47, // 167: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest - 51, // 168: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest - 53, // 169: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest - 59, // 170: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame - 60, // 171: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput - 67, // 172: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest - 68, // 173: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest - 69, // 174: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest - 74, // 175: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest - 75, // 176: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest - 99, // 177: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest - 101, // 178: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest - 103, // 179: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest - 70, // 180: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest - 87, // 181: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest - 89, // 182: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest - 91, // 183: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest - 93, // 184: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest - 71, // 185: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest - 106, // 186: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest - 226, // 187: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest - 227, // 188: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest - 112, // 189: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest - 121, // 190: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest - 123, // 191: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest - 125, // 192: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest - 108, // 193: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest - 128, // 194: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest - 129, // 195: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest - 132, // 196: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage - 143, // 197: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame - 63, // 198: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest - 152, // 199: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest - 154, // 200: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest - 156, // 201: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest - 158, // 202: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest - 160, // 203: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest - 162, // 204: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest - 164, // 205: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest - 166, // 206: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest - 168, // 207: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest - 6, // 208: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest - 8, // 209: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest - 175, // 210: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest - 177, // 211: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest - 179, // 212: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest - 181, // 213: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest - 184, // 214: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest - 186, // 215: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest - 188, // 216: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest - 11, // 217: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse - 13, // 218: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse - 15, // 219: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse - 35, // 220: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse - 35, // 221: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse - 36, // 222: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse - 37, // 223: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse - 38, // 224: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse - 39, // 225: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse - 40, // 226: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse - 35, // 227: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse - 35, // 228: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse - 42, // 229: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse - 50, // 230: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse - 50, // 231: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse - 46, // 232: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse - 48, // 233: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse - 52, // 234: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse - 57, // 235: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent - 59, // 236: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame - 57, // 237: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent - 72, // 238: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse - 72, // 239: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse - 73, // 240: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse - 98, // 241: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse - 97, // 242: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse - 100, // 243: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse - 102, // 244: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse - 104, // 245: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse - 72, // 246: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse - 88, // 247: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse - 90, // 248: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse - 92, // 249: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse - 94, // 250: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse - 105, // 251: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse - 107, // 252: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse - 228, // 253: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse - 229, // 254: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse - 120, // 255: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse - 122, // 256: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse - 124, // 257: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse - 126, // 258: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse - 111, // 259: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse - 131, // 260: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse - 130, // 261: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse - 133, // 262: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage - 143, // 263: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame - 64, // 264: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent - 153, // 265: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse - 155, // 266: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse - 157, // 267: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse - 159, // 268: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse - 161, // 269: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse - 163, // 270: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse - 165, // 271: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse - 167, // 272: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse - 170, // 273: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse - 7, // 274: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse - 9, // 275: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse - 176, // 276: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse - 178, // 277: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse - 180, // 278: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse - 182, // 279: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse - 185, // 280: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse - 187, // 281: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse - 189, // 282: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse - 217, // [217:283] is the sub-list for method output_type - 151, // [151:217] is the sub-list for method input_type - 151, // [151:151] is the sub-list for extension type_name - 151, // [151:151] is the sub-list for extension extendee - 0, // [0:151] is the sub-list for field type_name + 213, // 0: openshell.v1.RefreshSandboxTokenResponse.extension_credentials:type_name -> openshell.v1.ExtensionServiceCredential + 5, // 1: openshell.v1.HealthResponse.status:type_name -> openshell.v1.ServiceStatus + 5, // 2: openshell.v1.GetGatewayInfoResponse.status:type_name -> openshell.v1.ServiceStatus + 17, // 3: openshell.v1.GetGatewayInfoResponse.compute_drivers:type_name -> openshell.v1.ComputeDriverInfo + 18, // 4: openshell.v1.ComputeDriverInfo.capabilities:type_name -> openshell.v1.ComputeDriverCapabilities + 238, // 5: openshell.v1.Sandbox.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 22, // 6: openshell.v1.Sandbox.spec:type_name -> openshell.v1.SandboxSpec + 26, // 7: openshell.v1.Sandbox.status:type_name -> openshell.v1.SandboxStatus + 238, // 8: openshell.v1.SandboxDelegatedIdentityRecord.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 20, // 9: openshell.v1.SandboxDelegatedIdentityRecord.delegated_identity:type_name -> openshell.v1.SandboxDelegatedIdentity + 214, // 10: openshell.v1.SandboxSpec.environment:type_name -> openshell.v1.SandboxSpec.EnvironmentEntry + 25, // 11: openshell.v1.SandboxSpec.template:type_name -> openshell.v1.SandboxTemplate + 239, // 12: openshell.v1.SandboxSpec.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 23, // 13: openshell.v1.SandboxSpec.resource_requirements:type_name -> openshell.v1.ResourceRequirements + 24, // 14: openshell.v1.ResourceRequirements.gpu:type_name -> openshell.v1.GpuResourceRequirements + 215, // 15: openshell.v1.SandboxTemplate.labels:type_name -> openshell.v1.SandboxTemplate.LabelsEntry + 216, // 16: openshell.v1.SandboxTemplate.annotations:type_name -> openshell.v1.SandboxTemplate.AnnotationsEntry + 217, // 17: openshell.v1.SandboxTemplate.environment:type_name -> openshell.v1.SandboxTemplate.EnvironmentEntry + 240, // 18: openshell.v1.SandboxTemplate.resources:type_name -> google.protobuf.Struct + 240, // 19: openshell.v1.SandboxTemplate.driver_config:type_name -> google.protobuf.Struct + 27, // 20: openshell.v1.SandboxStatus.conditions:type_name -> openshell.v1.SandboxCondition + 0, // 21: openshell.v1.SandboxStatus.phase:type_name -> openshell.v1.SandboxPhase + 218, // 22: openshell.v1.PlatformEvent.metadata:type_name -> openshell.v1.PlatformEvent.MetadataEntry + 22, // 23: openshell.v1.CreateSandboxRequest.spec:type_name -> openshell.v1.SandboxSpec + 219, // 24: openshell.v1.CreateSandboxRequest.labels:type_name -> openshell.v1.CreateSandboxRequest.LabelsEntry + 220, // 25: openshell.v1.CreateSandboxRequest.annotations:type_name -> openshell.v1.CreateSandboxRequest.AnnotationsEntry + 30, // 26: openshell.v1.CreateSandboxRequest.delegated_identity:type_name -> openshell.v1.DelegatedIdentityRequest + 20, // 27: openshell.v1.GetSandboxDelegatedIdentityStatusResponse.delegated_identity:type_name -> openshell.v1.SandboxDelegatedIdentity + 19, // 28: openshell.v1.WithdrawSandboxDelegatedIdentityResponse.sandbox:type_name -> openshell.v1.Sandbox + 30, // 29: openshell.v1.ExtendSandboxDelegatedIdentityRequest.delegated_identity:type_name -> openshell.v1.DelegatedIdentityRequest + 19, // 30: openshell.v1.ExtendSandboxDelegatedIdentityResponse.sandbox:type_name -> openshell.v1.Sandbox + 19, // 31: openshell.v1.SandboxResponse.sandbox:type_name -> openshell.v1.Sandbox + 19, // 32: openshell.v1.ListSandboxesResponse.sandboxes:type_name -> openshell.v1.Sandbox + 241, // 33: openshell.v1.ListSandboxProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 19, // 34: openshell.v1.AttachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 19, // 35: openshell.v1.DetachSandboxProviderResponse.sandbox:type_name -> openshell.v1.Sandbox + 60, // 36: openshell.v1.ListServicesResponse.services:type_name -> openshell.v1.ServiceEndpointResponse + 238, // 37: openshell.v1.ServiceEndpoint.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 59, // 38: openshell.v1.ServiceEndpointResponse.endpoint:type_name -> openshell.v1.ServiceEndpoint + 221, // 39: openshell.v1.ExecSandboxRequest.environment:type_name -> openshell.v1.ExecSandboxRequest.EnvironmentEntry + 64, // 40: openshell.v1.ExecSandboxEvent.stdout:type_name -> openshell.v1.ExecSandboxStdout + 65, // 41: openshell.v1.ExecSandboxEvent.stderr:type_name -> openshell.v1.ExecSandboxStderr + 66, // 42: openshell.v1.ExecSandboxEvent.exit:type_name -> openshell.v1.ExecSandboxExit + 163, // 43: openshell.v1.TcpForwardInit.ssh:type_name -> openshell.v1.SshRelayTarget + 164, // 44: openshell.v1.TcpForwardInit.tcp:type_name -> openshell.v1.TcpRelayTarget + 68, // 45: openshell.v1.TcpForwardFrame.init:type_name -> openshell.v1.TcpForwardInit + 63, // 46: openshell.v1.ExecSandboxInput.start:type_name -> openshell.v1.ExecSandboxRequest + 71, // 47: openshell.v1.ExecSandboxInput.resize:type_name -> openshell.v1.ExecSandboxWindowResize + 238, // 48: openshell.v1.SshSession.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 19, // 49: openshell.v1.SandboxStreamEvent.sandbox:type_name -> openshell.v1.Sandbox + 75, // 50: openshell.v1.SandboxStreamEvent.log:type_name -> openshell.v1.SandboxLogLine + 28, // 51: openshell.v1.SandboxStreamEvent.event:type_name -> openshell.v1.PlatformEvent + 76, // 52: openshell.v1.SandboxStreamEvent.warning:type_name -> openshell.v1.SandboxStreamWarning + 174, // 53: openshell.v1.SandboxStreamEvent.draft_policy_update:type_name -> openshell.v1.DraftPolicyUpdate + 222, // 54: openshell.v1.SandboxLogLine.fields:type_name -> openshell.v1.SandboxLogLine.FieldsEntry + 241, // 55: openshell.v1.CreateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 241, // 56: openshell.v1.UpdateProviderRequest.provider:type_name -> openshell.datamodel.v1.Provider + 223, // 57: openshell.v1.UpdateProviderRequest.credential_expires_at_ms:type_name -> openshell.v1.UpdateProviderRequest.CredentialExpiresAtMsEntry + 241, // 58: openshell.v1.ProviderResponse.provider:type_name -> openshell.datamodel.v1.Provider + 241, // 59: openshell.v1.ListProvidersResponse.providers:type_name -> openshell.datamodel.v1.Provider + 116, // 60: openshell.v1.ProviderProfileImportItem.profile:type_name -> openshell.v1.ProviderProfile + 88, // 61: openshell.v1.ProviderCredentialTokenGrant.audience_overrides:type_name -> openshell.v1.ProviderCredentialTokenGrantAudienceOverride + 1, // 62: openshell.v1.ProviderCredentialTokenGrant.grant_type:type_name -> openshell.v1.ProviderCredentialTokenGrantType + 89, // 63: openshell.v1.ProviderCredentialTokenGrant.subject_token:type_name -> openshell.v1.ProviderCredentialTokenGrantSubjectToken + 94, // 64: openshell.v1.ProviderProfileCredential.refresh:type_name -> openshell.v1.ProviderCredentialRefresh + 90, // 65: openshell.v1.ProviderProfileCredential.token_grant:type_name -> openshell.v1.ProviderCredentialTokenGrant + 2, // 66: openshell.v1.ProviderCredentialRefresh.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 92, // 67: openshell.v1.ProviderCredentialRefresh.material:type_name -> openshell.v1.ProviderCredentialRefreshMaterial + 93, // 68: openshell.v1.ProviderCredentialRefresh.additional_outputs:type_name -> openshell.v1.ProviderCredentialRefreshOutput + 2, // 69: openshell.v1.ProviderCredentialRefreshStatus.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 238, // 70: openshell.v1.StoredProviderCredentialRefreshState.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 2, // 71: openshell.v1.StoredProviderCredentialRefreshState.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 224, // 72: openshell.v1.StoredProviderCredentialRefreshState.material:type_name -> openshell.v1.StoredProviderCredentialRefreshState.MaterialEntry + 225, // 73: openshell.v1.StoredProviderCredentialRefreshState.additional_output_keys:type_name -> openshell.v1.StoredProviderCredentialRefreshState.AdditionalOutputKeysEntry + 238, // 74: openshell.v1.DelegatedIdentityCredential.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 238, // 75: openshell.v1.DelegatedIdentityCredentialSummary.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 99, // 76: openshell.v1.ListDelegatedIdentityCredentialsResponse.credentials:type_name -> openshell.v1.DelegatedIdentityCredentialSummary + 99, // 77: openshell.v1.GetDelegatedIdentityCredentialStatusResponse.credential:type_name -> openshell.v1.DelegatedIdentityCredentialSummary + 95, // 78: openshell.v1.GetProviderRefreshStatusResponse.credentials:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 2, // 79: openshell.v1.ConfigureProviderRefreshRequest.strategy:type_name -> openshell.v1.ProviderCredentialRefreshStrategy + 226, // 80: openshell.v1.ConfigureProviderRefreshRequest.material:type_name -> openshell.v1.ConfigureProviderRefreshRequest.MaterialEntry + 95, // 81: openshell.v1.ConfigureProviderRefreshResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 95, // 82: openshell.v1.RotateProviderCredentialResponse.status:type_name -> openshell.v1.ProviderCredentialRefreshStatus + 3, // 83: openshell.v1.ProviderProfile.category:type_name -> openshell.v1.ProviderProfileCategory + 91, // 84: openshell.v1.ProviderProfile.credentials:type_name -> openshell.v1.ProviderProfileCredential + 242, // 85: openshell.v1.ProviderProfile.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 243, // 86: openshell.v1.ProviderProfile.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 96, // 87: openshell.v1.ProviderProfile.discovery:type_name -> openshell.v1.ProviderProfileDiscovery + 227, // 88: openshell.v1.ProviderProfile.annotations:type_name -> openshell.v1.ProviderProfile.AnnotationsEntry + 238, // 89: openshell.v1.StoredProviderProfile.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 116, // 90: openshell.v1.StoredProviderProfile.profile:type_name -> openshell.v1.ProviderProfile + 116, // 91: openshell.v1.ProviderProfileResponse.profile:type_name -> openshell.v1.ProviderProfile + 116, // 92: openshell.v1.ListProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 86, // 93: openshell.v1.ImportProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 87, // 94: openshell.v1.ImportProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 116, // 95: openshell.v1.ImportProviderProfilesResponse.profiles:type_name -> openshell.v1.ProviderProfile + 86, // 96: openshell.v1.UpdateProviderProfilesRequest.profile:type_name -> openshell.v1.ProviderProfileImportItem + 87, // 97: openshell.v1.UpdateProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 116, // 98: openshell.v1.UpdateProviderProfilesResponse.profile:type_name -> openshell.v1.ProviderProfile + 86, // 99: openshell.v1.LintProviderProfilesRequest.profiles:type_name -> openshell.v1.ProviderProfileImportItem + 87, // 100: openshell.v1.LintProviderProfilesResponse.diagnostics:type_name -> openshell.v1.ProviderProfileDiagnostic + 130, // 101: openshell.v1.StaticCredentialBinding.endpoints:type_name -> openshell.v1.StaticCredentialEndpointBinding + 228, // 102: openshell.v1.GetSandboxProviderEnvironmentResponse.environment:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.EnvironmentEntry + 229, // 103: openshell.v1.GetSandboxProviderEnvironmentResponse.credential_expires_at_ms:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.CredentialExpiresAtMsEntry + 230, // 104: openshell.v1.GetSandboxProviderEnvironmentResponse.dynamic_credentials:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry + 231, // 105: openshell.v1.GetSandboxProviderEnvironmentResponse.static_credential_bindings:type_name -> openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry + 239, // 106: openshell.v1.UpdateConfigRequest.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 244, // 107: openshell.v1.UpdateConfigRequest.setting_value:type_name -> openshell.sandbox.v1.SettingValue + 136, // 108: openshell.v1.UpdateConfigRequest.merge_operations:type_name -> openshell.v1.PolicyMergeOperation + 232, // 109: openshell.v1.UpdateConfigRequest.annotations:type_name -> openshell.v1.UpdateConfigRequest.AnnotationsEntry + 137, // 110: openshell.v1.PolicyMergeOperation.add_rule:type_name -> openshell.v1.AddNetworkRule + 138, // 111: openshell.v1.PolicyMergeOperation.remove_endpoint:type_name -> openshell.v1.RemoveNetworkEndpoint + 139, // 112: openshell.v1.PolicyMergeOperation.remove_rule:type_name -> openshell.v1.RemoveNetworkRule + 140, // 113: openshell.v1.PolicyMergeOperation.add_deny_rules:type_name -> openshell.v1.AddDenyRules + 141, // 114: openshell.v1.PolicyMergeOperation.add_allow_rules:type_name -> openshell.v1.AddAllowRules + 142, // 115: openshell.v1.PolicyMergeOperation.remove_binary:type_name -> openshell.v1.RemoveNetworkBinary + 245, // 116: openshell.v1.AddNetworkRule.rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 246, // 117: openshell.v1.AddDenyRules.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 247, // 118: openshell.v1.AddAllowRules.rules:type_name -> openshell.sandbox.v1.L7Rule + 233, // 119: openshell.v1.UpdateConfigResponse.annotations:type_name -> openshell.v1.UpdateConfigResponse.AnnotationsEntry + 150, // 120: openshell.v1.GetSandboxPolicyStatusResponse.revision:type_name -> openshell.v1.SandboxPolicyRevision + 150, // 121: openshell.v1.ListSandboxPoliciesResponse.revisions:type_name -> openshell.v1.SandboxPolicyRevision + 4, // 122: openshell.v1.ReportPolicyStatusRequest.status:type_name -> openshell.v1.PolicyStatus + 4, // 123: openshell.v1.SandboxPolicyRevision.status:type_name -> openshell.v1.PolicyStatus + 239, // 124: openshell.v1.SandboxPolicyRevision.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 234, // 125: openshell.v1.SandboxPolicyRevision.provenance:type_name -> openshell.v1.SandboxPolicyRevision.ProvenanceEntry + 75, // 126: openshell.v1.PushSandboxLogsRequest.logs:type_name -> openshell.v1.SandboxLogLine + 75, // 127: openshell.v1.GetSandboxLogsResponse.logs:type_name -> openshell.v1.SandboxLogLine + 157, // 128: openshell.v1.SupervisorMessage.hello:type_name -> openshell.v1.SupervisorHello + 160, // 129: openshell.v1.SupervisorMessage.heartbeat:type_name -> openshell.v1.SupervisorHeartbeat + 167, // 130: openshell.v1.SupervisorMessage.relay_open_result:type_name -> openshell.v1.RelayOpenResult + 168, // 131: openshell.v1.SupervisorMessage.relay_close:type_name -> openshell.v1.RelayClose + 158, // 132: openshell.v1.GatewayMessage.session_accepted:type_name -> openshell.v1.SessionAccepted + 159, // 133: openshell.v1.GatewayMessage.session_rejected:type_name -> openshell.v1.SessionRejected + 161, // 134: openshell.v1.GatewayMessage.heartbeat:type_name -> openshell.v1.GatewayHeartbeat + 162, // 135: openshell.v1.GatewayMessage.relay_open:type_name -> openshell.v1.RelayOpen + 168, // 136: openshell.v1.GatewayMessage.relay_close:type_name -> openshell.v1.RelayClose + 163, // 137: openshell.v1.RelayOpen.ssh:type_name -> openshell.v1.SshRelayTarget + 164, // 138: openshell.v1.RelayOpen.tcp:type_name -> openshell.v1.TcpRelayTarget + 165, // 139: openshell.v1.RelayFrame.init:type_name -> openshell.v1.RelayInit + 169, // 140: openshell.v1.DenialSummary.l7_request_samples:type_name -> openshell.v1.L7RequestSample + 171, // 141: openshell.v1.NetworkActivitySummary.denials_by_group:type_name -> openshell.v1.DenialGroupCount + 245, // 142: openshell.v1.PolicyChunk.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 170, // 143: openshell.v1.SubmitPolicyAnalysisRequest.summaries:type_name -> openshell.v1.DenialSummary + 173, // 144: openshell.v1.SubmitPolicyAnalysisRequest.proposed_chunks:type_name -> openshell.v1.PolicyChunk + 172, // 145: openshell.v1.SubmitPolicyAnalysisRequest.network_activity_summaries:type_name -> openshell.v1.NetworkActivitySummary + 173, // 146: openshell.v1.GetDraftPolicyResponse.chunks:type_name -> openshell.v1.PolicyChunk + 245, // 147: openshell.v1.EditDraftChunkRequest.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 192, // 148: openshell.v1.GetDraftHistoryResponse.entries:type_name -> openshell.v1.DraftHistoryEntry + 239, // 149: openshell.v1.PolicyRevisionPayload.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 235, // 150: openshell.v1.PolicyRevisionPayload.provenance:type_name -> openshell.v1.PolicyRevisionPayload.ProvenanceEntry + 245, // 151: openshell.v1.DraftChunkPayload.proposed_rule:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 236, // 152: openshell.v1.StoredPolicyRevision.provenance:type_name -> openshell.v1.StoredPolicyRevision.ProvenanceEntry + 237, // 153: openshell.v1.CreateWorkspaceRequest.labels:type_name -> openshell.v1.CreateWorkspaceRequest.LabelsEntry + 248, // 154: openshell.v1.CreateWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 248, // 155: openshell.v1.GetWorkspaceResponse.workspace:type_name -> openshell.datamodel.v1.Workspace + 248, // 156: openshell.v1.ListWorkspacesResponse.workspaces:type_name -> openshell.datamodel.v1.Workspace + 238, // 157: openshell.v1.WorkspaceMember.metadata:type_name -> openshell.datamodel.v1.ObjectMeta + 6, // 158: openshell.v1.WorkspaceMember.role:type_name -> openshell.v1.WorkspaceRole + 6, // 159: openshell.v1.AddWorkspaceMemberRequest.role:type_name -> openshell.v1.WorkspaceRole + 206, // 160: openshell.v1.AddWorkspaceMemberResponse.member:type_name -> openshell.v1.WorkspaceMember + 206, // 161: openshell.v1.ListWorkspaceMembersResponse.members:type_name -> openshell.v1.WorkspaceMember + 91, // 162: openshell.v1.GetSandboxProviderEnvironmentResponse.DynamicCredentialsEntry.value:type_name -> openshell.v1.ProviderProfileCredential + 131, // 163: openshell.v1.GetSandboxProviderEnvironmentResponse.StaticCredentialBindingsEntry.value:type_name -> openshell.v1.StaticCredentialBinding + 11, // 164: openshell.v1.OpenShell.Health:input_type -> openshell.v1.HealthRequest + 13, // 165: openshell.v1.OpenShell.GetCurrentUser:input_type -> openshell.v1.GetCurrentUserRequest + 15, // 166: openshell.v1.OpenShell.GetGatewayInfo:input_type -> openshell.v1.GetGatewayInfoRequest + 29, // 167: openshell.v1.OpenShell.CreateSandbox:input_type -> openshell.v1.CreateSandboxRequest + 31, // 168: openshell.v1.OpenShell.GetSandboxDelegatedIdentityStatus:input_type -> openshell.v1.GetSandboxDelegatedIdentityStatusRequest + 33, // 169: openshell.v1.OpenShell.WithdrawSandboxDelegatedIdentity:input_type -> openshell.v1.WithdrawSandboxDelegatedIdentityRequest + 35, // 170: openshell.v1.OpenShell.ExtendSandboxDelegatedIdentity:input_type -> openshell.v1.ExtendSandboxDelegatedIdentityRequest + 37, // 171: openshell.v1.OpenShell.GetSandbox:input_type -> openshell.v1.GetSandboxRequest + 38, // 172: openshell.v1.OpenShell.ListSandboxes:input_type -> openshell.v1.ListSandboxesRequest + 39, // 173: openshell.v1.OpenShell.ListSandboxProviders:input_type -> openshell.v1.ListSandboxProvidersRequest + 40, // 174: openshell.v1.OpenShell.AttachSandboxProvider:input_type -> openshell.v1.AttachSandboxProviderRequest + 41, // 175: openshell.v1.OpenShell.DetachSandboxProvider:input_type -> openshell.v1.DetachSandboxProviderRequest + 42, // 176: openshell.v1.OpenShell.DeleteSandbox:input_type -> openshell.v1.DeleteSandboxRequest + 43, // 177: openshell.v1.OpenShell.StopSandbox:input_type -> openshell.v1.StopSandboxRequest + 44, // 178: openshell.v1.OpenShell.StartSandbox:input_type -> openshell.v1.StartSandboxRequest + 51, // 179: openshell.v1.OpenShell.CreateSshSession:input_type -> openshell.v1.CreateSshSessionRequest + 53, // 180: openshell.v1.OpenShell.ExposeService:input_type -> openshell.v1.ExposeServiceRequest + 54, // 181: openshell.v1.OpenShell.GetService:input_type -> openshell.v1.GetServiceRequest + 55, // 182: openshell.v1.OpenShell.ListServices:input_type -> openshell.v1.ListServicesRequest + 57, // 183: openshell.v1.OpenShell.DeleteService:input_type -> openshell.v1.DeleteServiceRequest + 61, // 184: openshell.v1.OpenShell.RevokeSshSession:input_type -> openshell.v1.RevokeSshSessionRequest + 63, // 185: openshell.v1.OpenShell.ExecSandbox:input_type -> openshell.v1.ExecSandboxRequest + 69, // 186: openshell.v1.OpenShell.ForwardTcp:input_type -> openshell.v1.TcpForwardFrame + 70, // 187: openshell.v1.OpenShell.ExecSandboxInteractive:input_type -> openshell.v1.ExecSandboxInput + 77, // 188: openshell.v1.OpenShell.CreateProvider:input_type -> openshell.v1.CreateProviderRequest + 78, // 189: openshell.v1.OpenShell.GetProvider:input_type -> openshell.v1.GetProviderRequest + 79, // 190: openshell.v1.OpenShell.ListProviders:input_type -> openshell.v1.ListProvidersRequest + 84, // 191: openshell.v1.OpenShell.ListProviderProfiles:input_type -> openshell.v1.ListProviderProfilesRequest + 85, // 192: openshell.v1.OpenShell.GetProviderProfile:input_type -> openshell.v1.GetProviderProfileRequest + 120, // 193: openshell.v1.OpenShell.ImportProviderProfiles:input_type -> openshell.v1.ImportProviderProfilesRequest + 122, // 194: openshell.v1.OpenShell.UpdateProviderProfiles:input_type -> openshell.v1.UpdateProviderProfilesRequest + 124, // 195: openshell.v1.OpenShell.LintProviderProfiles:input_type -> openshell.v1.LintProviderProfilesRequest + 80, // 196: openshell.v1.OpenShell.UpdateProvider:input_type -> openshell.v1.UpdateProviderRequest + 108, // 197: openshell.v1.OpenShell.GetProviderRefreshStatus:input_type -> openshell.v1.GetProviderRefreshStatusRequest + 110, // 198: openshell.v1.OpenShell.ConfigureProviderRefresh:input_type -> openshell.v1.ConfigureProviderRefreshRequest + 112, // 199: openshell.v1.OpenShell.RotateProviderCredential:input_type -> openshell.v1.RotateProviderCredentialRequest + 100, // 200: openshell.v1.OpenShell.ListDelegatedIdentityCredentials:input_type -> openshell.v1.ListDelegatedIdentityCredentialsRequest + 102, // 201: openshell.v1.OpenShell.GetDelegatedIdentityCredentialStatus:input_type -> openshell.v1.GetDelegatedIdentityCredentialStatusRequest + 104, // 202: openshell.v1.OpenShell.RevokeDelegatedIdentityCredential:input_type -> openshell.v1.RevokeDelegatedIdentityCredentialRequest + 106, // 203: openshell.v1.OpenShell.DeleteDelegatedIdentityCredential:input_type -> openshell.v1.DeleteDelegatedIdentityCredentialRequest + 114, // 204: openshell.v1.OpenShell.DeleteProviderRefresh:input_type -> openshell.v1.DeleteProviderRefreshRequest + 81, // 205: openshell.v1.OpenShell.DeleteProvider:input_type -> openshell.v1.DeleteProviderRequest + 127, // 206: openshell.v1.OpenShell.DeleteProviderProfile:input_type -> openshell.v1.DeleteProviderProfileRequest + 249, // 207: openshell.v1.OpenShell.GetSandboxConfig:input_type -> openshell.sandbox.v1.GetSandboxConfigRequest + 250, // 208: openshell.v1.OpenShell.GetGatewayConfig:input_type -> openshell.sandbox.v1.GetGatewayConfigRequest + 135, // 209: openshell.v1.OpenShell.UpdateConfig:input_type -> openshell.v1.UpdateConfigRequest + 144, // 210: openshell.v1.OpenShell.GetSandboxPolicyStatus:input_type -> openshell.v1.GetSandboxPolicyStatusRequest + 146, // 211: openshell.v1.OpenShell.ListSandboxPolicies:input_type -> openshell.v1.ListSandboxPoliciesRequest + 148, // 212: openshell.v1.OpenShell.ReportPolicyStatus:input_type -> openshell.v1.ReportPolicyStatusRequest + 129, // 213: openshell.v1.OpenShell.GetSandboxProviderEnvironment:input_type -> openshell.v1.GetSandboxProviderEnvironmentRequest + 133, // 214: openshell.v1.OpenShell.ExchangeProviderSubjectToken:input_type -> openshell.v1.ExchangeProviderSubjectTokenRequest + 151, // 215: openshell.v1.OpenShell.GetSandboxLogs:input_type -> openshell.v1.GetSandboxLogsRequest + 152, // 216: openshell.v1.OpenShell.PushSandboxLogs:input_type -> openshell.v1.PushSandboxLogsRequest + 155, // 217: openshell.v1.OpenShell.ConnectSupervisor:input_type -> openshell.v1.SupervisorMessage + 166, // 218: openshell.v1.OpenShell.RelayStream:input_type -> openshell.v1.RelayFrame + 73, // 219: openshell.v1.OpenShell.WatchSandbox:input_type -> openshell.v1.WatchSandboxRequest + 175, // 220: openshell.v1.OpenShell.SubmitPolicyAnalysis:input_type -> openshell.v1.SubmitPolicyAnalysisRequest + 177, // 221: openshell.v1.OpenShell.GetDraftPolicy:input_type -> openshell.v1.GetDraftPolicyRequest + 179, // 222: openshell.v1.OpenShell.ApproveDraftChunk:input_type -> openshell.v1.ApproveDraftChunkRequest + 181, // 223: openshell.v1.OpenShell.RejectDraftChunk:input_type -> openshell.v1.RejectDraftChunkRequest + 183, // 224: openshell.v1.OpenShell.ApproveAllDraftChunks:input_type -> openshell.v1.ApproveAllDraftChunksRequest + 185, // 225: openshell.v1.OpenShell.EditDraftChunk:input_type -> openshell.v1.EditDraftChunkRequest + 187, // 226: openshell.v1.OpenShell.UndoDraftChunk:input_type -> openshell.v1.UndoDraftChunkRequest + 189, // 227: openshell.v1.OpenShell.ClearDraftChunks:input_type -> openshell.v1.ClearDraftChunksRequest + 191, // 228: openshell.v1.OpenShell.GetDraftHistory:input_type -> openshell.v1.GetDraftHistoryRequest + 7, // 229: openshell.v1.OpenShell.IssueSandboxToken:input_type -> openshell.v1.IssueSandboxTokenRequest + 9, // 230: openshell.v1.OpenShell.RefreshSandboxToken:input_type -> openshell.v1.RefreshSandboxTokenRequest + 198, // 231: openshell.v1.OpenShell.CreateWorkspace:input_type -> openshell.v1.CreateWorkspaceRequest + 200, // 232: openshell.v1.OpenShell.GetWorkspace:input_type -> openshell.v1.GetWorkspaceRequest + 202, // 233: openshell.v1.OpenShell.ListWorkspaces:input_type -> openshell.v1.ListWorkspacesRequest + 204, // 234: openshell.v1.OpenShell.DeleteWorkspace:input_type -> openshell.v1.DeleteWorkspaceRequest + 207, // 235: openshell.v1.OpenShell.AddWorkspaceMember:input_type -> openshell.v1.AddWorkspaceMemberRequest + 209, // 236: openshell.v1.OpenShell.RemoveWorkspaceMember:input_type -> openshell.v1.RemoveWorkspaceMemberRequest + 211, // 237: openshell.v1.OpenShell.ListWorkspaceMembers:input_type -> openshell.v1.ListWorkspaceMembersRequest + 12, // 238: openshell.v1.OpenShell.Health:output_type -> openshell.v1.HealthResponse + 14, // 239: openshell.v1.OpenShell.GetCurrentUser:output_type -> openshell.v1.GetCurrentUserResponse + 16, // 240: openshell.v1.OpenShell.GetGatewayInfo:output_type -> openshell.v1.GetGatewayInfoResponse + 45, // 241: openshell.v1.OpenShell.CreateSandbox:output_type -> openshell.v1.SandboxResponse + 32, // 242: openshell.v1.OpenShell.GetSandboxDelegatedIdentityStatus:output_type -> openshell.v1.GetSandboxDelegatedIdentityStatusResponse + 34, // 243: openshell.v1.OpenShell.WithdrawSandboxDelegatedIdentity:output_type -> openshell.v1.WithdrawSandboxDelegatedIdentityResponse + 36, // 244: openshell.v1.OpenShell.ExtendSandboxDelegatedIdentity:output_type -> openshell.v1.ExtendSandboxDelegatedIdentityResponse + 45, // 245: openshell.v1.OpenShell.GetSandbox:output_type -> openshell.v1.SandboxResponse + 46, // 246: openshell.v1.OpenShell.ListSandboxes:output_type -> openshell.v1.ListSandboxesResponse + 47, // 247: openshell.v1.OpenShell.ListSandboxProviders:output_type -> openshell.v1.ListSandboxProvidersResponse + 48, // 248: openshell.v1.OpenShell.AttachSandboxProvider:output_type -> openshell.v1.AttachSandboxProviderResponse + 49, // 249: openshell.v1.OpenShell.DetachSandboxProvider:output_type -> openshell.v1.DetachSandboxProviderResponse + 50, // 250: openshell.v1.OpenShell.DeleteSandbox:output_type -> openshell.v1.DeleteSandboxResponse + 45, // 251: openshell.v1.OpenShell.StopSandbox:output_type -> openshell.v1.SandboxResponse + 45, // 252: openshell.v1.OpenShell.StartSandbox:output_type -> openshell.v1.SandboxResponse + 52, // 253: openshell.v1.OpenShell.CreateSshSession:output_type -> openshell.v1.CreateSshSessionResponse + 60, // 254: openshell.v1.OpenShell.ExposeService:output_type -> openshell.v1.ServiceEndpointResponse + 60, // 255: openshell.v1.OpenShell.GetService:output_type -> openshell.v1.ServiceEndpointResponse + 56, // 256: openshell.v1.OpenShell.ListServices:output_type -> openshell.v1.ListServicesResponse + 58, // 257: openshell.v1.OpenShell.DeleteService:output_type -> openshell.v1.DeleteServiceResponse + 62, // 258: openshell.v1.OpenShell.RevokeSshSession:output_type -> openshell.v1.RevokeSshSessionResponse + 67, // 259: openshell.v1.OpenShell.ExecSandbox:output_type -> openshell.v1.ExecSandboxEvent + 69, // 260: openshell.v1.OpenShell.ForwardTcp:output_type -> openshell.v1.TcpForwardFrame + 67, // 261: openshell.v1.OpenShell.ExecSandboxInteractive:output_type -> openshell.v1.ExecSandboxEvent + 82, // 262: openshell.v1.OpenShell.CreateProvider:output_type -> openshell.v1.ProviderResponse + 82, // 263: openshell.v1.OpenShell.GetProvider:output_type -> openshell.v1.ProviderResponse + 83, // 264: openshell.v1.OpenShell.ListProviders:output_type -> openshell.v1.ListProvidersResponse + 119, // 265: openshell.v1.OpenShell.ListProviderProfiles:output_type -> openshell.v1.ListProviderProfilesResponse + 118, // 266: openshell.v1.OpenShell.GetProviderProfile:output_type -> openshell.v1.ProviderProfileResponse + 121, // 267: openshell.v1.OpenShell.ImportProviderProfiles:output_type -> openshell.v1.ImportProviderProfilesResponse + 123, // 268: openshell.v1.OpenShell.UpdateProviderProfiles:output_type -> openshell.v1.UpdateProviderProfilesResponse + 125, // 269: openshell.v1.OpenShell.LintProviderProfiles:output_type -> openshell.v1.LintProviderProfilesResponse + 82, // 270: openshell.v1.OpenShell.UpdateProvider:output_type -> openshell.v1.ProviderResponse + 109, // 271: openshell.v1.OpenShell.GetProviderRefreshStatus:output_type -> openshell.v1.GetProviderRefreshStatusResponse + 111, // 272: openshell.v1.OpenShell.ConfigureProviderRefresh:output_type -> openshell.v1.ConfigureProviderRefreshResponse + 113, // 273: openshell.v1.OpenShell.RotateProviderCredential:output_type -> openshell.v1.RotateProviderCredentialResponse + 101, // 274: openshell.v1.OpenShell.ListDelegatedIdentityCredentials:output_type -> openshell.v1.ListDelegatedIdentityCredentialsResponse + 103, // 275: openshell.v1.OpenShell.GetDelegatedIdentityCredentialStatus:output_type -> openshell.v1.GetDelegatedIdentityCredentialStatusResponse + 105, // 276: openshell.v1.OpenShell.RevokeDelegatedIdentityCredential:output_type -> openshell.v1.RevokeDelegatedIdentityCredentialResponse + 107, // 277: openshell.v1.OpenShell.DeleteDelegatedIdentityCredential:output_type -> openshell.v1.DeleteDelegatedIdentityCredentialResponse + 115, // 278: openshell.v1.OpenShell.DeleteProviderRefresh:output_type -> openshell.v1.DeleteProviderRefreshResponse + 126, // 279: openshell.v1.OpenShell.DeleteProvider:output_type -> openshell.v1.DeleteProviderResponse + 128, // 280: openshell.v1.OpenShell.DeleteProviderProfile:output_type -> openshell.v1.DeleteProviderProfileResponse + 251, // 281: openshell.v1.OpenShell.GetSandboxConfig:output_type -> openshell.sandbox.v1.GetSandboxConfigResponse + 252, // 282: openshell.v1.OpenShell.GetGatewayConfig:output_type -> openshell.sandbox.v1.GetGatewayConfigResponse + 143, // 283: openshell.v1.OpenShell.UpdateConfig:output_type -> openshell.v1.UpdateConfigResponse + 145, // 284: openshell.v1.OpenShell.GetSandboxPolicyStatus:output_type -> openshell.v1.GetSandboxPolicyStatusResponse + 147, // 285: openshell.v1.OpenShell.ListSandboxPolicies:output_type -> openshell.v1.ListSandboxPoliciesResponse + 149, // 286: openshell.v1.OpenShell.ReportPolicyStatus:output_type -> openshell.v1.ReportPolicyStatusResponse + 132, // 287: openshell.v1.OpenShell.GetSandboxProviderEnvironment:output_type -> openshell.v1.GetSandboxProviderEnvironmentResponse + 134, // 288: openshell.v1.OpenShell.ExchangeProviderSubjectToken:output_type -> openshell.v1.ExchangeProviderSubjectTokenResponse + 154, // 289: openshell.v1.OpenShell.GetSandboxLogs:output_type -> openshell.v1.GetSandboxLogsResponse + 153, // 290: openshell.v1.OpenShell.PushSandboxLogs:output_type -> openshell.v1.PushSandboxLogsResponse + 156, // 291: openshell.v1.OpenShell.ConnectSupervisor:output_type -> openshell.v1.GatewayMessage + 166, // 292: openshell.v1.OpenShell.RelayStream:output_type -> openshell.v1.RelayFrame + 74, // 293: openshell.v1.OpenShell.WatchSandbox:output_type -> openshell.v1.SandboxStreamEvent + 176, // 294: openshell.v1.OpenShell.SubmitPolicyAnalysis:output_type -> openshell.v1.SubmitPolicyAnalysisResponse + 178, // 295: openshell.v1.OpenShell.GetDraftPolicy:output_type -> openshell.v1.GetDraftPolicyResponse + 180, // 296: openshell.v1.OpenShell.ApproveDraftChunk:output_type -> openshell.v1.ApproveDraftChunkResponse + 182, // 297: openshell.v1.OpenShell.RejectDraftChunk:output_type -> openshell.v1.RejectDraftChunkResponse + 184, // 298: openshell.v1.OpenShell.ApproveAllDraftChunks:output_type -> openshell.v1.ApproveAllDraftChunksResponse + 186, // 299: openshell.v1.OpenShell.EditDraftChunk:output_type -> openshell.v1.EditDraftChunkResponse + 188, // 300: openshell.v1.OpenShell.UndoDraftChunk:output_type -> openshell.v1.UndoDraftChunkResponse + 190, // 301: openshell.v1.OpenShell.ClearDraftChunks:output_type -> openshell.v1.ClearDraftChunksResponse + 193, // 302: openshell.v1.OpenShell.GetDraftHistory:output_type -> openshell.v1.GetDraftHistoryResponse + 8, // 303: openshell.v1.OpenShell.IssueSandboxToken:output_type -> openshell.v1.IssueSandboxTokenResponse + 10, // 304: openshell.v1.OpenShell.RefreshSandboxToken:output_type -> openshell.v1.RefreshSandboxTokenResponse + 199, // 305: openshell.v1.OpenShell.CreateWorkspace:output_type -> openshell.v1.CreateWorkspaceResponse + 201, // 306: openshell.v1.OpenShell.GetWorkspace:output_type -> openshell.v1.GetWorkspaceResponse + 203, // 307: openshell.v1.OpenShell.ListWorkspaces:output_type -> openshell.v1.ListWorkspacesResponse + 205, // 308: openshell.v1.OpenShell.DeleteWorkspace:output_type -> openshell.v1.DeleteWorkspaceResponse + 208, // 309: openshell.v1.OpenShell.AddWorkspaceMember:output_type -> openshell.v1.AddWorkspaceMemberResponse + 210, // 310: openshell.v1.OpenShell.RemoveWorkspaceMember:output_type -> openshell.v1.RemoveWorkspaceMemberResponse + 212, // 311: openshell.v1.OpenShell.ListWorkspaceMembers:output_type -> openshell.v1.ListWorkspaceMembersResponse + 238, // [238:312] is the sub-list for method output_type + 164, // [164:238] is the sub-list for method input_type + 164, // [164:164] is the sub-list for extension type_name + 164, // [164:164] is the sub-list for extension extendee + 0, // [0:164] is the sub-list for field type_name } func init() { file_openshell_proto_init() } @@ -14788,35 +16455,35 @@ func file_openshell_proto_init() { if File_openshell_proto != nil { return } - file_openshell_proto_msgTypes[15].OneofWrappers = []any{} - file_openshell_proto_msgTypes[16].OneofWrappers = []any{} - file_openshell_proto_msgTypes[51].OneofWrappers = []any{ + file_openshell_proto_msgTypes[17].OneofWrappers = []any{} + file_openshell_proto_msgTypes[18].OneofWrappers = []any{} + file_openshell_proto_msgTypes[60].OneofWrappers = []any{ (*ExecSandboxEvent_Stdout)(nil), (*ExecSandboxEvent_Stderr)(nil), (*ExecSandboxEvent_Exit)(nil), } - file_openshell_proto_msgTypes[52].OneofWrappers = []any{ + file_openshell_proto_msgTypes[61].OneofWrappers = []any{ (*TcpForwardInit_Ssh)(nil), (*TcpForwardInit_Tcp)(nil), } - file_openshell_proto_msgTypes[53].OneofWrappers = []any{ + file_openshell_proto_msgTypes[62].OneofWrappers = []any{ (*TcpForwardFrame_Init)(nil), (*TcpForwardFrame_Data)(nil), } - file_openshell_proto_msgTypes[54].OneofWrappers = []any{ + file_openshell_proto_msgTypes[63].OneofWrappers = []any{ (*ExecSandboxInput_Start)(nil), (*ExecSandboxInput_Stdin)(nil), (*ExecSandboxInput_Resize)(nil), } - file_openshell_proto_msgTypes[58].OneofWrappers = []any{ + file_openshell_proto_msgTypes[67].OneofWrappers = []any{ (*SandboxStreamEvent_Sandbox)(nil), (*SandboxStreamEvent_Log)(nil), (*SandboxStreamEvent_Event)(nil), (*SandboxStreamEvent_Warning)(nil), (*SandboxStreamEvent_DraftPolicyUpdate)(nil), } - file_openshell_proto_msgTypes[83].OneofWrappers = []any{} - file_openshell_proto_msgTypes[107].OneofWrappers = []any{ + file_openshell_proto_msgTypes[103].OneofWrappers = []any{} + file_openshell_proto_msgTypes[129].OneofWrappers = []any{ (*PolicyMergeOperation_AddRule)(nil), (*PolicyMergeOperation_RemoveEndpoint)(nil), (*PolicyMergeOperation_RemoveRule)(nil), @@ -14824,36 +16491,36 @@ func file_openshell_proto_init() { (*PolicyMergeOperation_AddAllowRules)(nil), (*PolicyMergeOperation_RemoveBinary)(nil), } - file_openshell_proto_msgTypes[126].OneofWrappers = []any{ + file_openshell_proto_msgTypes[148].OneofWrappers = []any{ (*SupervisorMessage_Hello)(nil), (*SupervisorMessage_Heartbeat)(nil), (*SupervisorMessage_RelayOpenResult)(nil), (*SupervisorMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[127].OneofWrappers = []any{ + file_openshell_proto_msgTypes[149].OneofWrappers = []any{ (*GatewayMessage_SessionAccepted)(nil), (*GatewayMessage_SessionRejected)(nil), (*GatewayMessage_Heartbeat)(nil), (*GatewayMessage_RelayOpen)(nil), (*GatewayMessage_RelayClose)(nil), } - file_openshell_proto_msgTypes[133].OneofWrappers = []any{ + file_openshell_proto_msgTypes[155].OneofWrappers = []any{ (*RelayOpen_Ssh)(nil), (*RelayOpen_Tcp)(nil), } - file_openshell_proto_msgTypes[137].OneofWrappers = []any{ + file_openshell_proto_msgTypes[159].OneofWrappers = []any{ (*RelayFrame_Init)(nil), (*RelayFrame_Data)(nil), } - file_openshell_proto_msgTypes[167].OneofWrappers = []any{} - file_openshell_proto_msgTypes[168].OneofWrappers = []any{} + file_openshell_proto_msgTypes[189].OneofWrappers = []any{} + file_openshell_proto_msgTypes[190].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: 209, + NumEnums: 7, + NumMessages: 231, 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 92c94ef299..748f52155f 100644 --- a/sdk/go/proto/openshellv1/openshell_grpc.pb.go +++ b/sdk/go/proto/openshellv1/openshell_grpc.pb.go @@ -23,72 +23,80 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - OpenShell_Health_FullMethodName = "/openshell.v1.OpenShell/Health" - OpenShell_GetCurrentUser_FullMethodName = "/openshell.v1.OpenShell/GetCurrentUser" - OpenShell_GetGatewayInfo_FullMethodName = "/openshell.v1.OpenShell/GetGatewayInfo" - OpenShell_CreateSandbox_FullMethodName = "/openshell.v1.OpenShell/CreateSandbox" - OpenShell_GetSandbox_FullMethodName = "/openshell.v1.OpenShell/GetSandbox" - OpenShell_ListSandboxes_FullMethodName = "/openshell.v1.OpenShell/ListSandboxes" - OpenShell_ListSandboxProviders_FullMethodName = "/openshell.v1.OpenShell/ListSandboxProviders" - OpenShell_AttachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/AttachSandboxProvider" - OpenShell_DetachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/DetachSandboxProvider" - OpenShell_DeleteSandbox_FullMethodName = "/openshell.v1.OpenShell/DeleteSandbox" - OpenShell_StopSandbox_FullMethodName = "/openshell.v1.OpenShell/StopSandbox" - OpenShell_StartSandbox_FullMethodName = "/openshell.v1.OpenShell/StartSandbox" - OpenShell_CreateSshSession_FullMethodName = "/openshell.v1.OpenShell/CreateSshSession" - OpenShell_ExposeService_FullMethodName = "/openshell.v1.OpenShell/ExposeService" - OpenShell_GetService_FullMethodName = "/openshell.v1.OpenShell/GetService" - OpenShell_ListServices_FullMethodName = "/openshell.v1.OpenShell/ListServices" - OpenShell_DeleteService_FullMethodName = "/openshell.v1.OpenShell/DeleteService" - OpenShell_RevokeSshSession_FullMethodName = "/openshell.v1.OpenShell/RevokeSshSession" - OpenShell_ExecSandbox_FullMethodName = "/openshell.v1.OpenShell/ExecSandbox" - OpenShell_ForwardTcp_FullMethodName = "/openshell.v1.OpenShell/ForwardTcp" - OpenShell_ExecSandboxInteractive_FullMethodName = "/openshell.v1.OpenShell/ExecSandboxInteractive" - OpenShell_CreateProvider_FullMethodName = "/openshell.v1.OpenShell/CreateProvider" - OpenShell_GetProvider_FullMethodName = "/openshell.v1.OpenShell/GetProvider" - OpenShell_ListProviders_FullMethodName = "/openshell.v1.OpenShell/ListProviders" - OpenShell_ListProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/ListProviderProfiles" - OpenShell_GetProviderProfile_FullMethodName = "/openshell.v1.OpenShell/GetProviderProfile" - OpenShell_ImportProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/ImportProviderProfiles" - OpenShell_UpdateProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/UpdateProviderProfiles" - OpenShell_LintProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/LintProviderProfiles" - OpenShell_UpdateProvider_FullMethodName = "/openshell.v1.OpenShell/UpdateProvider" - OpenShell_GetProviderRefreshStatus_FullMethodName = "/openshell.v1.OpenShell/GetProviderRefreshStatus" - OpenShell_ConfigureProviderRefresh_FullMethodName = "/openshell.v1.OpenShell/ConfigureProviderRefresh" - OpenShell_RotateProviderCredential_FullMethodName = "/openshell.v1.OpenShell/RotateProviderCredential" - OpenShell_DeleteProviderRefresh_FullMethodName = "/openshell.v1.OpenShell/DeleteProviderRefresh" - OpenShell_DeleteProvider_FullMethodName = "/openshell.v1.OpenShell/DeleteProvider" - OpenShell_DeleteProviderProfile_FullMethodName = "/openshell.v1.OpenShell/DeleteProviderProfile" - OpenShell_GetSandboxConfig_FullMethodName = "/openshell.v1.OpenShell/GetSandboxConfig" - OpenShell_GetGatewayConfig_FullMethodName = "/openshell.v1.OpenShell/GetGatewayConfig" - OpenShell_UpdateConfig_FullMethodName = "/openshell.v1.OpenShell/UpdateConfig" - OpenShell_GetSandboxPolicyStatus_FullMethodName = "/openshell.v1.OpenShell/GetSandboxPolicyStatus" - OpenShell_ListSandboxPolicies_FullMethodName = "/openshell.v1.OpenShell/ListSandboxPolicies" - OpenShell_ReportPolicyStatus_FullMethodName = "/openshell.v1.OpenShell/ReportPolicyStatus" - OpenShell_GetSandboxProviderEnvironment_FullMethodName = "/openshell.v1.OpenShell/GetSandboxProviderEnvironment" - OpenShell_GetSandboxLogs_FullMethodName = "/openshell.v1.OpenShell/GetSandboxLogs" - OpenShell_PushSandboxLogs_FullMethodName = "/openshell.v1.OpenShell/PushSandboxLogs" - OpenShell_ConnectSupervisor_FullMethodName = "/openshell.v1.OpenShell/ConnectSupervisor" - OpenShell_RelayStream_FullMethodName = "/openshell.v1.OpenShell/RelayStream" - OpenShell_WatchSandbox_FullMethodName = "/openshell.v1.OpenShell/WatchSandbox" - OpenShell_SubmitPolicyAnalysis_FullMethodName = "/openshell.v1.OpenShell/SubmitPolicyAnalysis" - OpenShell_GetDraftPolicy_FullMethodName = "/openshell.v1.OpenShell/GetDraftPolicy" - OpenShell_ApproveDraftChunk_FullMethodName = "/openshell.v1.OpenShell/ApproveDraftChunk" - OpenShell_RejectDraftChunk_FullMethodName = "/openshell.v1.OpenShell/RejectDraftChunk" - OpenShell_ApproveAllDraftChunks_FullMethodName = "/openshell.v1.OpenShell/ApproveAllDraftChunks" - OpenShell_EditDraftChunk_FullMethodName = "/openshell.v1.OpenShell/EditDraftChunk" - OpenShell_UndoDraftChunk_FullMethodName = "/openshell.v1.OpenShell/UndoDraftChunk" - OpenShell_ClearDraftChunks_FullMethodName = "/openshell.v1.OpenShell/ClearDraftChunks" - OpenShell_GetDraftHistory_FullMethodName = "/openshell.v1.OpenShell/GetDraftHistory" - OpenShell_IssueSandboxToken_FullMethodName = "/openshell.v1.OpenShell/IssueSandboxToken" - OpenShell_RefreshSandboxToken_FullMethodName = "/openshell.v1.OpenShell/RefreshSandboxToken" - OpenShell_CreateWorkspace_FullMethodName = "/openshell.v1.OpenShell/CreateWorkspace" - OpenShell_GetWorkspace_FullMethodName = "/openshell.v1.OpenShell/GetWorkspace" - OpenShell_ListWorkspaces_FullMethodName = "/openshell.v1.OpenShell/ListWorkspaces" - OpenShell_DeleteWorkspace_FullMethodName = "/openshell.v1.OpenShell/DeleteWorkspace" - OpenShell_AddWorkspaceMember_FullMethodName = "/openshell.v1.OpenShell/AddWorkspaceMember" - OpenShell_RemoveWorkspaceMember_FullMethodName = "/openshell.v1.OpenShell/RemoveWorkspaceMember" - OpenShell_ListWorkspaceMembers_FullMethodName = "/openshell.v1.OpenShell/ListWorkspaceMembers" + OpenShell_Health_FullMethodName = "/openshell.v1.OpenShell/Health" + OpenShell_GetCurrentUser_FullMethodName = "/openshell.v1.OpenShell/GetCurrentUser" + OpenShell_GetGatewayInfo_FullMethodName = "/openshell.v1.OpenShell/GetGatewayInfo" + OpenShell_CreateSandbox_FullMethodName = "/openshell.v1.OpenShell/CreateSandbox" + OpenShell_GetSandboxDelegatedIdentityStatus_FullMethodName = "/openshell.v1.OpenShell/GetSandboxDelegatedIdentityStatus" + OpenShell_WithdrawSandboxDelegatedIdentity_FullMethodName = "/openshell.v1.OpenShell/WithdrawSandboxDelegatedIdentity" + OpenShell_ExtendSandboxDelegatedIdentity_FullMethodName = "/openshell.v1.OpenShell/ExtendSandboxDelegatedIdentity" + OpenShell_GetSandbox_FullMethodName = "/openshell.v1.OpenShell/GetSandbox" + OpenShell_ListSandboxes_FullMethodName = "/openshell.v1.OpenShell/ListSandboxes" + OpenShell_ListSandboxProviders_FullMethodName = "/openshell.v1.OpenShell/ListSandboxProviders" + OpenShell_AttachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/AttachSandboxProvider" + OpenShell_DetachSandboxProvider_FullMethodName = "/openshell.v1.OpenShell/DetachSandboxProvider" + OpenShell_DeleteSandbox_FullMethodName = "/openshell.v1.OpenShell/DeleteSandbox" + OpenShell_StopSandbox_FullMethodName = "/openshell.v1.OpenShell/StopSandbox" + OpenShell_StartSandbox_FullMethodName = "/openshell.v1.OpenShell/StartSandbox" + OpenShell_CreateSshSession_FullMethodName = "/openshell.v1.OpenShell/CreateSshSession" + OpenShell_ExposeService_FullMethodName = "/openshell.v1.OpenShell/ExposeService" + OpenShell_GetService_FullMethodName = "/openshell.v1.OpenShell/GetService" + OpenShell_ListServices_FullMethodName = "/openshell.v1.OpenShell/ListServices" + OpenShell_DeleteService_FullMethodName = "/openshell.v1.OpenShell/DeleteService" + OpenShell_RevokeSshSession_FullMethodName = "/openshell.v1.OpenShell/RevokeSshSession" + OpenShell_ExecSandbox_FullMethodName = "/openshell.v1.OpenShell/ExecSandbox" + OpenShell_ForwardTcp_FullMethodName = "/openshell.v1.OpenShell/ForwardTcp" + OpenShell_ExecSandboxInteractive_FullMethodName = "/openshell.v1.OpenShell/ExecSandboxInteractive" + OpenShell_CreateProvider_FullMethodName = "/openshell.v1.OpenShell/CreateProvider" + OpenShell_GetProvider_FullMethodName = "/openshell.v1.OpenShell/GetProvider" + OpenShell_ListProviders_FullMethodName = "/openshell.v1.OpenShell/ListProviders" + OpenShell_ListProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/ListProviderProfiles" + OpenShell_GetProviderProfile_FullMethodName = "/openshell.v1.OpenShell/GetProviderProfile" + OpenShell_ImportProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/ImportProviderProfiles" + OpenShell_UpdateProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/UpdateProviderProfiles" + OpenShell_LintProviderProfiles_FullMethodName = "/openshell.v1.OpenShell/LintProviderProfiles" + OpenShell_UpdateProvider_FullMethodName = "/openshell.v1.OpenShell/UpdateProvider" + OpenShell_GetProviderRefreshStatus_FullMethodName = "/openshell.v1.OpenShell/GetProviderRefreshStatus" + OpenShell_ConfigureProviderRefresh_FullMethodName = "/openshell.v1.OpenShell/ConfigureProviderRefresh" + OpenShell_RotateProviderCredential_FullMethodName = "/openshell.v1.OpenShell/RotateProviderCredential" + OpenShell_ListDelegatedIdentityCredentials_FullMethodName = "/openshell.v1.OpenShell/ListDelegatedIdentityCredentials" + OpenShell_GetDelegatedIdentityCredentialStatus_FullMethodName = "/openshell.v1.OpenShell/GetDelegatedIdentityCredentialStatus" + OpenShell_RevokeDelegatedIdentityCredential_FullMethodName = "/openshell.v1.OpenShell/RevokeDelegatedIdentityCredential" + OpenShell_DeleteDelegatedIdentityCredential_FullMethodName = "/openshell.v1.OpenShell/DeleteDelegatedIdentityCredential" + OpenShell_DeleteProviderRefresh_FullMethodName = "/openshell.v1.OpenShell/DeleteProviderRefresh" + OpenShell_DeleteProvider_FullMethodName = "/openshell.v1.OpenShell/DeleteProvider" + OpenShell_DeleteProviderProfile_FullMethodName = "/openshell.v1.OpenShell/DeleteProviderProfile" + OpenShell_GetSandboxConfig_FullMethodName = "/openshell.v1.OpenShell/GetSandboxConfig" + OpenShell_GetGatewayConfig_FullMethodName = "/openshell.v1.OpenShell/GetGatewayConfig" + OpenShell_UpdateConfig_FullMethodName = "/openshell.v1.OpenShell/UpdateConfig" + OpenShell_GetSandboxPolicyStatus_FullMethodName = "/openshell.v1.OpenShell/GetSandboxPolicyStatus" + 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" + OpenShell_RelayStream_FullMethodName = "/openshell.v1.OpenShell/RelayStream" + OpenShell_WatchSandbox_FullMethodName = "/openshell.v1.OpenShell/WatchSandbox" + OpenShell_SubmitPolicyAnalysis_FullMethodName = "/openshell.v1.OpenShell/SubmitPolicyAnalysis" + OpenShell_GetDraftPolicy_FullMethodName = "/openshell.v1.OpenShell/GetDraftPolicy" + OpenShell_ApproveDraftChunk_FullMethodName = "/openshell.v1.OpenShell/ApproveDraftChunk" + OpenShell_RejectDraftChunk_FullMethodName = "/openshell.v1.OpenShell/RejectDraftChunk" + OpenShell_ApproveAllDraftChunks_FullMethodName = "/openshell.v1.OpenShell/ApproveAllDraftChunks" + OpenShell_EditDraftChunk_FullMethodName = "/openshell.v1.OpenShell/EditDraftChunk" + OpenShell_UndoDraftChunk_FullMethodName = "/openshell.v1.OpenShell/UndoDraftChunk" + OpenShell_ClearDraftChunks_FullMethodName = "/openshell.v1.OpenShell/ClearDraftChunks" + OpenShell_GetDraftHistory_FullMethodName = "/openshell.v1.OpenShell/GetDraftHistory" + OpenShell_IssueSandboxToken_FullMethodName = "/openshell.v1.OpenShell/IssueSandboxToken" + OpenShell_RefreshSandboxToken_FullMethodName = "/openshell.v1.OpenShell/RefreshSandboxToken" + OpenShell_CreateWorkspace_FullMethodName = "/openshell.v1.OpenShell/CreateWorkspace" + OpenShell_GetWorkspace_FullMethodName = "/openshell.v1.OpenShell/GetWorkspace" + OpenShell_ListWorkspaces_FullMethodName = "/openshell.v1.OpenShell/ListWorkspaces" + OpenShell_DeleteWorkspace_FullMethodName = "/openshell.v1.OpenShell/DeleteWorkspace" + OpenShell_AddWorkspaceMember_FullMethodName = "/openshell.v1.OpenShell/AddWorkspaceMember" + OpenShell_RemoveWorkspaceMember_FullMethodName = "/openshell.v1.OpenShell/RemoveWorkspaceMember" + OpenShell_ListWorkspaceMembers_FullMethodName = "/openshell.v1.OpenShell/ListWorkspaceMembers" ) // OpenShellClient is the client API for OpenShell service. @@ -112,6 +120,12 @@ type OpenShellClient interface { GetGatewayInfo(ctx context.Context, in *GetGatewayInfoRequest, opts ...grpc.CallOption) (*GetGatewayInfoResponse, error) // Create a new sandbox. CreateSandbox(ctx context.Context, in *CreateSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) + // Fetch delegated identity status for one sandbox. + GetSandboxDelegatedIdentityStatus(ctx context.Context, in *GetSandboxDelegatedIdentityStatusRequest, opts ...grpc.CallOption) (*GetSandboxDelegatedIdentityStatusResponse, error) + // Withdraw delegated identity from one sandbox. + WithdrawSandboxDelegatedIdentity(ctx context.Context, in *WithdrawSandboxDelegatedIdentityRequest, opts ...grpc.CallOption) (*WithdrawSandboxDelegatedIdentityResponse, error) + // Extend delegated identity for one sandbox. + ExtendSandboxDelegatedIdentity(ctx context.Context, in *ExtendSandboxDelegatedIdentityRequest, opts ...grpc.CallOption) (*ExtendSandboxDelegatedIdentityResponse, error) // Fetch a sandbox by name. GetSandbox(ctx context.Context, in *GetSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) // List sandboxes. @@ -172,6 +186,14 @@ type OpenShellClient interface { ConfigureProviderRefresh(ctx context.Context, in *ConfigureProviderRefreshRequest, opts ...grpc.CallOption) (*ConfigureProviderRefreshResponse, error) // Record a gateway-owned refresh request for one provider credential. RotateProviderCredential(ctx context.Context, in *RotateProviderCredentialRequest, opts ...grpc.CallOption) (*RotateProviderCredentialResponse, error) + // List delegated identity credentials visible to the caller. + ListDelegatedIdentityCredentials(ctx context.Context, in *ListDelegatedIdentityCredentialsRequest, opts ...grpc.CallOption) (*ListDelegatedIdentityCredentialsResponse, error) + // Fetch delegated identity credential status. + GetDelegatedIdentityCredentialStatus(ctx context.Context, in *GetDelegatedIdentityCredentialStatusRequest, opts ...grpc.CallOption) (*GetDelegatedIdentityCredentialStatusResponse, error) + // Revoke a delegated identity credential. + RevokeDelegatedIdentityCredential(ctx context.Context, in *RevokeDelegatedIdentityCredentialRequest, opts ...grpc.CallOption) (*RevokeDelegatedIdentityCredentialResponse, error) + // Delete a delegated identity credential. + DeleteDelegatedIdentityCredential(ctx context.Context, in *DeleteDelegatedIdentityCredentialRequest, opts ...grpc.CallOption) (*DeleteDelegatedIdentityCredentialResponse, error) // Delete gateway-owned refresh configuration for one provider credential. DeleteProviderRefresh(ctx context.Context, in *DeleteProviderRefreshRequest, opts ...grpc.CallOption) (*DeleteProviderRefreshResponse, error) // Delete a provider by name. @@ -199,6 +221,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). @@ -325,6 +350,36 @@ func (c *openShellClient) CreateSandbox(ctx context.Context, in *CreateSandboxRe return out, nil } +func (c *openShellClient) GetSandboxDelegatedIdentityStatus(ctx context.Context, in *GetSandboxDelegatedIdentityStatusRequest, opts ...grpc.CallOption) (*GetSandboxDelegatedIdentityStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSandboxDelegatedIdentityStatusResponse) + err := c.cc.Invoke(ctx, OpenShell_GetSandboxDelegatedIdentityStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) WithdrawSandboxDelegatedIdentity(ctx context.Context, in *WithdrawSandboxDelegatedIdentityRequest, opts ...grpc.CallOption) (*WithdrawSandboxDelegatedIdentityResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WithdrawSandboxDelegatedIdentityResponse) + err := c.cc.Invoke(ctx, OpenShell_WithdrawSandboxDelegatedIdentity_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) ExtendSandboxDelegatedIdentity(ctx context.Context, in *ExtendSandboxDelegatedIdentityRequest, opts ...grpc.CallOption) (*ExtendSandboxDelegatedIdentityResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExtendSandboxDelegatedIdentityResponse) + err := c.cc.Invoke(ctx, OpenShell_ExtendSandboxDelegatedIdentity_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) GetSandbox(ctx context.Context, in *GetSandboxRequest, opts ...grpc.CallOption) (*SandboxResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SandboxResponse) @@ -630,6 +685,46 @@ func (c *openShellClient) RotateProviderCredential(ctx context.Context, in *Rota return out, nil } +func (c *openShellClient) ListDelegatedIdentityCredentials(ctx context.Context, in *ListDelegatedIdentityCredentialsRequest, opts ...grpc.CallOption) (*ListDelegatedIdentityCredentialsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListDelegatedIdentityCredentialsResponse) + err := c.cc.Invoke(ctx, OpenShell_ListDelegatedIdentityCredentials_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) GetDelegatedIdentityCredentialStatus(ctx context.Context, in *GetDelegatedIdentityCredentialStatusRequest, opts ...grpc.CallOption) (*GetDelegatedIdentityCredentialStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetDelegatedIdentityCredentialStatusResponse) + err := c.cc.Invoke(ctx, OpenShell_GetDelegatedIdentityCredentialStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) RevokeDelegatedIdentityCredential(ctx context.Context, in *RevokeDelegatedIdentityCredentialRequest, opts ...grpc.CallOption) (*RevokeDelegatedIdentityCredentialResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RevokeDelegatedIdentityCredentialResponse) + err := c.cc.Invoke(ctx, OpenShell_RevokeDelegatedIdentityCredential_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *openShellClient) DeleteDelegatedIdentityCredential(ctx context.Context, in *DeleteDelegatedIdentityCredentialRequest, opts ...grpc.CallOption) (*DeleteDelegatedIdentityCredentialResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteDelegatedIdentityCredentialResponse) + err := c.cc.Invoke(ctx, OpenShell_DeleteDelegatedIdentityCredential_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *openShellClient) DeleteProviderRefresh(ctx context.Context, in *DeleteProviderRefreshRequest, opts ...grpc.CallOption) (*DeleteProviderRefreshResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(DeleteProviderRefreshResponse) @@ -730,6 +825,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) @@ -999,6 +1104,12 @@ type OpenShellServer interface { GetGatewayInfo(context.Context, *GetGatewayInfoRequest) (*GetGatewayInfoResponse, error) // Create a new sandbox. CreateSandbox(context.Context, *CreateSandboxRequest) (*SandboxResponse, error) + // Fetch delegated identity status for one sandbox. + GetSandboxDelegatedIdentityStatus(context.Context, *GetSandboxDelegatedIdentityStatusRequest) (*GetSandboxDelegatedIdentityStatusResponse, error) + // Withdraw delegated identity from one sandbox. + WithdrawSandboxDelegatedIdentity(context.Context, *WithdrawSandboxDelegatedIdentityRequest) (*WithdrawSandboxDelegatedIdentityResponse, error) + // Extend delegated identity for one sandbox. + ExtendSandboxDelegatedIdentity(context.Context, *ExtendSandboxDelegatedIdentityRequest) (*ExtendSandboxDelegatedIdentityResponse, error) // Fetch a sandbox by name. GetSandbox(context.Context, *GetSandboxRequest) (*SandboxResponse, error) // List sandboxes. @@ -1059,6 +1170,14 @@ type OpenShellServer interface { ConfigureProviderRefresh(context.Context, *ConfigureProviderRefreshRequest) (*ConfigureProviderRefreshResponse, error) // Record a gateway-owned refresh request for one provider credential. RotateProviderCredential(context.Context, *RotateProviderCredentialRequest) (*RotateProviderCredentialResponse, error) + // List delegated identity credentials visible to the caller. + ListDelegatedIdentityCredentials(context.Context, *ListDelegatedIdentityCredentialsRequest) (*ListDelegatedIdentityCredentialsResponse, error) + // Fetch delegated identity credential status. + GetDelegatedIdentityCredentialStatus(context.Context, *GetDelegatedIdentityCredentialStatusRequest) (*GetDelegatedIdentityCredentialStatusResponse, error) + // Revoke a delegated identity credential. + RevokeDelegatedIdentityCredential(context.Context, *RevokeDelegatedIdentityCredentialRequest) (*RevokeDelegatedIdentityCredentialResponse, error) + // Delete a delegated identity credential. + DeleteDelegatedIdentityCredential(context.Context, *DeleteDelegatedIdentityCredentialRequest) (*DeleteDelegatedIdentityCredentialResponse, error) // Delete gateway-owned refresh configuration for one provider credential. DeleteProviderRefresh(context.Context, *DeleteProviderRefreshRequest) (*DeleteProviderRefreshResponse, error) // Delete a provider by name. @@ -1086,6 +1205,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). @@ -1184,6 +1306,15 @@ func (UnimplementedOpenShellServer) GetGatewayInfo(context.Context, *GetGatewayI func (UnimplementedOpenShellServer) CreateSandbox(context.Context, *CreateSandboxRequest) (*SandboxResponse, error) { return nil, status.Error(codes.Unimplemented, "method CreateSandbox not implemented") } +func (UnimplementedOpenShellServer) GetSandboxDelegatedIdentityStatus(context.Context, *GetSandboxDelegatedIdentityStatusRequest) (*GetSandboxDelegatedIdentityStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSandboxDelegatedIdentityStatus not implemented") +} +func (UnimplementedOpenShellServer) WithdrawSandboxDelegatedIdentity(context.Context, *WithdrawSandboxDelegatedIdentityRequest) (*WithdrawSandboxDelegatedIdentityResponse, error) { + return nil, status.Error(codes.Unimplemented, "method WithdrawSandboxDelegatedIdentity not implemented") +} +func (UnimplementedOpenShellServer) ExtendSandboxDelegatedIdentity(context.Context, *ExtendSandboxDelegatedIdentityRequest) (*ExtendSandboxDelegatedIdentityResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ExtendSandboxDelegatedIdentity not implemented") +} func (UnimplementedOpenShellServer) GetSandbox(context.Context, *GetSandboxRequest) (*SandboxResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetSandbox not implemented") } @@ -1271,6 +1402,18 @@ func (UnimplementedOpenShellServer) ConfigureProviderRefresh(context.Context, *C func (UnimplementedOpenShellServer) RotateProviderCredential(context.Context, *RotateProviderCredentialRequest) (*RotateProviderCredentialResponse, error) { return nil, status.Error(codes.Unimplemented, "method RotateProviderCredential not implemented") } +func (UnimplementedOpenShellServer) ListDelegatedIdentityCredentials(context.Context, *ListDelegatedIdentityCredentialsRequest) (*ListDelegatedIdentityCredentialsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListDelegatedIdentityCredentials not implemented") +} +func (UnimplementedOpenShellServer) GetDelegatedIdentityCredentialStatus(context.Context, *GetDelegatedIdentityCredentialStatusRequest) (*GetDelegatedIdentityCredentialStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetDelegatedIdentityCredentialStatus not implemented") +} +func (UnimplementedOpenShellServer) RevokeDelegatedIdentityCredential(context.Context, *RevokeDelegatedIdentityCredentialRequest) (*RevokeDelegatedIdentityCredentialResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RevokeDelegatedIdentityCredential not implemented") +} +func (UnimplementedOpenShellServer) DeleteDelegatedIdentityCredential(context.Context, *DeleteDelegatedIdentityCredentialRequest) (*DeleteDelegatedIdentityCredentialResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteDelegatedIdentityCredential not implemented") +} func (UnimplementedOpenShellServer) DeleteProviderRefresh(context.Context, *DeleteProviderRefreshRequest) (*DeleteProviderRefreshResponse, error) { return nil, status.Error(codes.Unimplemented, "method DeleteProviderRefresh not implemented") } @@ -1301,6 +1444,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") } @@ -1463,6 +1609,60 @@ func _OpenShell_CreateSandbox_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _OpenShell_GetSandboxDelegatedIdentityStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSandboxDelegatedIdentityStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetSandboxDelegatedIdentityStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetSandboxDelegatedIdentityStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetSandboxDelegatedIdentityStatus(ctx, req.(*GetSandboxDelegatedIdentityStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_WithdrawSandboxDelegatedIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WithdrawSandboxDelegatedIdentityRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).WithdrawSandboxDelegatedIdentity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_WithdrawSandboxDelegatedIdentity_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).WithdrawSandboxDelegatedIdentity(ctx, req.(*WithdrawSandboxDelegatedIdentityRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_ExtendSandboxDelegatedIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExtendSandboxDelegatedIdentityRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ExtendSandboxDelegatedIdentity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ExtendSandboxDelegatedIdentity_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ExtendSandboxDelegatedIdentity(ctx, req.(*ExtendSandboxDelegatedIdentityRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_GetSandbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetSandboxRequest) if err := dec(in); err != nil { @@ -1956,6 +2156,78 @@ func _OpenShell_RotateProviderCredential_Handler(srv interface{}, ctx context.Co return interceptor(ctx, in, info, handler) } +func _OpenShell_ListDelegatedIdentityCredentials_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListDelegatedIdentityCredentialsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).ListDelegatedIdentityCredentials(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_ListDelegatedIdentityCredentials_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).ListDelegatedIdentityCredentials(ctx, req.(*ListDelegatedIdentityCredentialsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_GetDelegatedIdentityCredentialStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetDelegatedIdentityCredentialStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).GetDelegatedIdentityCredentialStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_GetDelegatedIdentityCredentialStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).GetDelegatedIdentityCredentialStatus(ctx, req.(*GetDelegatedIdentityCredentialStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_RevokeDelegatedIdentityCredential_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RevokeDelegatedIdentityCredentialRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).RevokeDelegatedIdentityCredential(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_RevokeDelegatedIdentityCredential_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).RevokeDelegatedIdentityCredential(ctx, req.(*RevokeDelegatedIdentityCredentialRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OpenShell_DeleteDelegatedIdentityCredential_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteDelegatedIdentityCredentialRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OpenShellServer).DeleteDelegatedIdentityCredential(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OpenShell_DeleteDelegatedIdentityCredential_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OpenShellServer).DeleteDelegatedIdentityCredential(ctx, req.(*DeleteDelegatedIdentityCredentialRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _OpenShell_DeleteProviderRefresh_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DeleteProviderRefreshRequest) if err := dec(in); err != nil { @@ -2136,6 +2408,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 { @@ -2533,6 +2823,18 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "CreateSandbox", Handler: _OpenShell_CreateSandbox_Handler, }, + { + MethodName: "GetSandboxDelegatedIdentityStatus", + Handler: _OpenShell_GetSandboxDelegatedIdentityStatus_Handler, + }, + { + MethodName: "WithdrawSandboxDelegatedIdentity", + Handler: _OpenShell_WithdrawSandboxDelegatedIdentity_Handler, + }, + { + MethodName: "ExtendSandboxDelegatedIdentity", + Handler: _OpenShell_ExtendSandboxDelegatedIdentity_Handler, + }, { MethodName: "GetSandbox", Handler: _OpenShell_GetSandbox_Handler, @@ -2637,6 +2939,22 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "RotateProviderCredential", Handler: _OpenShell_RotateProviderCredential_Handler, }, + { + MethodName: "ListDelegatedIdentityCredentials", + Handler: _OpenShell_ListDelegatedIdentityCredentials_Handler, + }, + { + MethodName: "GetDelegatedIdentityCredentialStatus", + Handler: _OpenShell_GetDelegatedIdentityCredentialStatus_Handler, + }, + { + MethodName: "RevokeDelegatedIdentityCredential", + Handler: _OpenShell_RevokeDelegatedIdentityCredential_Handler, + }, + { + MethodName: "DeleteDelegatedIdentityCredential", + Handler: _OpenShell_DeleteDelegatedIdentityCredential_Handler, + }, { MethodName: "DeleteProviderRefresh", Handler: _OpenShell_DeleteProviderRefresh_Handler, @@ -2677,6 +2995,10 @@ var OpenShell_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetSandboxProviderEnvironment", Handler: _OpenShell_GetSandboxProviderEnvironment_Handler, }, + { + MethodName: "ExchangeProviderSubjectToken", + Handler: _OpenShell_ExchangeProviderSubjectToken_Handler, + }, { MethodName: "GetSandboxLogs", Handler: _OpenShell_GetSandboxLogs_Handler,