From 83b5e25204dd3b862e1d8300f846deb4d2d80052 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 6 Oct 2026 17:34:48 +0000 Subject: [PATCH 01/11] release: drop sam-box and nano-init build ids, gate the goreleaser config in CI cmd/sam-box and cmd/nano-init were removed in f6abfb82, after v0.1.0-rc.8, but .goreleaser.yaml still built both, so the goreleaser job on the next tag would fail before any binary or SDK package was published. install.sh listed both binaries as well. The test workflow now runs goreleaser check and a single-target snapshot build so a stale build id fails on the pull request, not on the tag. The chart appVersion follows the release line (0.1.0). --- .github/workflows/test.yaml | 24 ++++++++++++++++++++++++ .goreleaser.yaml | 34 ---------------------------------- charts/sam-mesh/Chart.yaml | 2 +- install.sh | 2 +- 4 files changed, 26 insertions(+), 36 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 098ddc0a..bd603a82 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -67,6 +67,30 @@ jobs: run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - run: make verify + # A build id whose cmd/ directory no longer exists only fails on the tag + # push, after the release is already underway; catch it on the PR instead. + goreleaser-check: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + distribution: goreleaser + version: "~> v2" + args: check + - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + distribution: goreleaser + version: "~> v2" + args: build --snapshot --clean --single-target + helm-lint: runs-on: ubuntu-latest steps: diff --git a/.goreleaser.yaml b/.goreleaser.yaml index db1cbeb3..6a3ed65d 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -60,20 +60,6 @@ builds: - arm64 ldflags: - -s -w - - id: sam-box - main: ./cmd/sam-box - binary: sam-box - env: - - CGO_ENABLED=0 - goos: - - linux - - windows - - darwin - goarch: - - amd64 - - arm64 - ldflags: - - -s -w - id: sam-console main: ./cmd/sam-console binary: sam-console @@ -102,23 +88,6 @@ builds: - arm64 ldflags: - -s -w -X github.com/google/sam/internal/version.Version={{ .Tag }} - - id: nano-init - main: . - # nano-init is its own module: it carries a userspace TCP stack, and that - # dependency has no business in the graph every other binary builds from. - dir: ./cmd/nano-init - binary: nano-init - env: - - CGO_ENABLED=0 - # linux only: nano-init is PID 1 inside a Linux sandbox — TUN ioctls, - # netlink, namespaces and vsock have no meaning anywhere else. - goos: - - linux - goarch: - - amd64 - - arm64 - ldflags: - - -s -w archives: - formats: - tar.gz @@ -133,6 +102,3 @@ archives: - goos: windows formats: - zip - # nano-init only builds for linux (see build id "nano-init" above), so the - # windows and darwin archives intentionally have one fewer binary. - allow_different_binary_count: true diff --git a/charts/sam-mesh/Chart.yaml b/charts/sam-mesh/Chart.yaml index 59fbb336..c7b90481 100644 --- a/charts/sam-mesh/Chart.yaml +++ b/charts/sam-mesh/Chart.yaml @@ -3,4 +3,4 @@ name: sam-mesh description: A Helm chart for deploying the SAM control plane, router and console type: application version: 0.1.0 -appVersion: "1.0.0" +appVersion: "0.1.0" diff --git a/install.sh b/install.sh index 82aa3b96..9504c395 100755 --- a/install.sh +++ b/install.sh @@ -78,7 +78,7 @@ tar -xzf "${TAR_NAME}" echo "Installing to ${INSTALL_DIR} (may require sudo)..." INSTALLED_BINS=() -for b in sam-one sam-node sam-control-plane sam-router mcp-client sam-box sam-console nano-init; do +for b in sam-one sam-node sam-control-plane sam-router mcp-client sam-console; do if [ -f "$b" ]; then INSTALLED_BINS+=("$b") fi From e72704eb30cbc98aec459dd38b5c74bf4f599307 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 6 Oct 2026 17:34:49 +0000 Subject: [PATCH 02/11] docs: describe the ext_authz credential path as implemented The reference, security architecture and concept pages described an --ext-authz-addr flag and the extraction of a SPIFFE principal from AttributeContext.Source.Principal or X-Forwarded-Client-Cert. sam-node has neither: the Envoy endpoints are served on the local API listeners, and the caller presents a Biscuit or a platform JWT that sam-node exchanges at the control plane. The pages now name the headers the code reads and returns. --- site/content/docs/concepts/authorization.md | 4 +-- .../contributing/security-architecture.md | 18 +++++++------ .../docs/preview/agent-architecture.md | 14 +++++----- site/content/docs/preview/sandboxed-agents.md | 15 +++++------ site/content/docs/reference/node-api.md | 26 +++++++++++-------- 5 files changed, 41 insertions(+), 36 deletions(-) diff --git a/site/content/docs/concepts/authorization.md b/site/content/docs/concepts/authorization.md index 544fd1e9..1f7302fa 100644 --- a/site/content/docs/concepts/authorization.md +++ b/site/content/docs/concepts/authorization.md @@ -202,8 +202,8 @@ permissions: - On `/mcp` and `/v1/*` (or via RFC 8693 `POST /oauth/token` and Envoy `ext_authz` / `ext_proc`), the caller presents its own platform JWT (OIDC ID - token, Kubernetes projected SA JWT, SPIFFE JWT-SVID, or Istio mTLS XFCC - identity) or a task-attenuated Biscuit. + token, Kubernetes projected SA JWT, or SPIFFE JWT-SVID) or a task-attenuated + Biscuit. - `sam-node` exchanges platform JWTs via `POST /token/exchange` into a **Delegated Session Biscuit** carrying the caller's own `user()`, `email()`, `group()`, and `role()` facts, bound to the node via `client_peer_id()` and diff --git a/site/content/docs/contributing/security-architecture.md b/site/content/docs/contributing/security-architecture.md index 9ad1ff69..d0c905a6 100644 --- a/site/content/docs/contributing/security-architecture.md +++ b/site/content/docs/contributing/security-architecture.md @@ -79,7 +79,7 @@ SAM separates caller attestation from task authorization: | Layer | Question answered | Primitive | Role in SAM | | :--- | :--- | :--- | :--- | -| **1. Workload / Subject & Channel Attestation** | *"Which workload or user initiated this request, and through which node is it travelling?"* | **OIDC ID tokens**, **Kubernetes projected SA JWTs**, **GCE/Cloud Run identity tokens**, **SPIFFE JWT-SVIDs**, or **Istio XFCC**. | Verified at `POST /register` (node enrollment) or `POST /token/exchange` (caller delegation) to mint a Biscuit bound to the transport channel (`client_peer_id`, `actor_node`). | +| **1. Workload / Subject & Channel Attestation** | *"Which workload or user initiated this request, and through which node is it travelling?"* | **OIDC ID tokens**, **Kubernetes projected SA JWTs**, **GCE/Cloud Run identity tokens**, or **SPIFFE JWT-SVIDs**. | Verified at `POST /register` (node enrollment) or `POST /token/exchange` (caller delegation) to mint a Biscuit bound to the transport channel (`client_peer_id`, `actor_node`). | | **2. Task / Session Authorization (TAR)** | *"What subset of standing permissions may this specific task or sub-agent hop exercise right now?"* | **SAM Task Biscuit** (Block 0 Authority + appended `tar_block` blocks carrying `api.TaskAuthorizationRule`). | Attenuated offline across hops, enforced at every SAM Policy Enforcement Point (PEP), and translated into downscoped upstream cloud credentials by `CloudTokenExchanger` at egress. | --- @@ -366,11 +366,13 @@ as a standard OIDC issuer: 1. Envoy `ext_authz` (`/ext_authz` and `/envoy.service.auth.v3.Authorization/Check`) evaluates standing Datalog - policy and `tar_block` rules on incoming Envoy `CheckRequest` calls, accepts - verified SPIFFE IDs from `AttributeContext.Source.Principal` or - `X-Forwarded-Client-Cert` (XFCC) on trusted proxy listeners, and injects - brokered upstream credentials (`Authorization: Bearer ...`, - `X-Sam-Principal`, `X-Sam-Task-Id`). + policy and `tar_block` rules on incoming Envoy `CheckRequest` calls. The + caller presents a Biscuit (`X-Sam-Biscuit`, or a Bearer value in + `X-Sam-Authentication` or `Authorization`) or a platform JWT, which + `sam-node` exchanges at the control plane into a delegated Biscuit. On `OK` + it returns `X-Sam-Biscuit`, `X-Sam-Principal`, `X-Sam-Roles`, + `X-Sam-Task-Id` and, for `egress://` targets with a credential broker, the + brokered upstream `Authorization` header. 2. Envoy `ext_proc` (`/envoy.service.ext_proc.v3.ExternalProcessor/Process`) inspects buffered JSON-RPC request bodies so `operation.allowed_tools` on MCP `tools/call` is enforced before injecting the upstream credential. @@ -463,8 +465,8 @@ projected service account JWT) to authenticate to a local or cluster #### Blueprint 3: `agentgateway` / Istio service mesh -Workloads authenticate to `agentgateway` or Istio via mTLS SPIFFE XFCC or a -Task Biscuit; the gateway calls `sam-node` over `ext_authz`, `ext_proc`, or RFC +Workloads authenticate to `agentgateway` or Istio with a platform JWT (for +example a SPIFFE JWT-SVID) or a Task Biscuit; the gateway calls `sam-node` over `ext_authz`, `ext_proc`, or RFC 8693 `/oauth/token` for local policy enforcement and routes cross-cluster MCP, A2A, and cloud egress calls through `sam-node`. diff --git a/site/content/docs/preview/agent-architecture.md b/site/content/docs/preview/agent-architecture.md index 42a785f0..e909adee 100644 --- a/site/content/docs/preview/agent-architecture.md +++ b/site/content/docs/preview/agent-architecture.md @@ -56,7 +56,7 @@ SAM separates the two layers: | Layer | Question answered | Primitive | |---|---|---| -| **Subject & channel attestation** | *Which user or workload initiated this request, and through which node?* | OIDC ID token, Kubernetes projected SA JWT, SPIFFE JWT-SVID, or Istio mTLS identity (`source.principal` / XFCC), exchanged into a Biscuit bound to the channel (`client_peer_id`, `actor_node`). | +| **Subject & channel attestation** | *Which user or workload initiated this request, and through which node?* | OIDC ID token, Kubernetes projected SA JWT, or SPIFFE JWT-SVID, exchanged into a Biscuit bound to the channel (`client_peer_id`, `actor_node`). | | **Task authorization (TAR)** | *What subset of standing permissions may this task or sub-agent hop exercise?* | Appended `tar_block` blocks carrying `api.TaskAuthorizationRule`, intersected across hops and translated into downscoped cloud credentials at egress. | ## Separation of responsibilities @@ -158,12 +158,12 @@ Where a cluster already runs Envoy, Istio, or `agentgateway`, `sam-node` acts as their external Policy Decision Point and Token Service over three standard interfaces: -1. **Envoy `ext_authz` (`--ext-authz-addr`):** evaluates standing policy and - `tar_block` chains on incoming `CheckRequest` calls, accepts verified - SPIFFE principals from `AttributeContext.Source.Principal` or - `X-Forwarded-Client-Cert` (XFCC) on trusted proxy listeners, and injects - brokered upstream credentials (`Authorization: Bearer ...`, - `X-Sam-Principal`, `X-Sam-Task-Id`). +1. **Envoy `ext_authz` (on the local API listeners):** evaluates standing + policy and `tar_block` chains on incoming `CheckRequest` calls. The caller + presents a Biscuit, or a platform JWT that `sam-node` exchanges at the + control plane into a delegated Biscuit. On `OK` it returns `X-Sam-Biscuit`, + `X-Sam-Principal`, `X-Sam-Roles`, `X-Sam-Task-Id` and, for `egress://` + targets with a credential broker, the brokered `Authorization` header. 2. **Envoy `ext_proc` (`envoy.service.ext_proc.v3.ExternalProcessor`):** the body-aware counterpart to `ext_authz`. Because MCP tool names travel inside the JSON-RPC request body (`params.name`), `ext_proc` inspects the buffered diff --git a/site/content/docs/preview/sandboxed-agents.md b/site/content/docs/preview/sandboxed-agents.md index ca8ad265..1bd3f34e 100644 --- a/site/content/docs/preview/sandboxed-agents.md +++ b/site/content/docs/preview/sandboxed-agents.md @@ -127,15 +127,14 @@ In clusters that already route agent traffic through **`agentgateway`**, continues to flow through those proxies while `sam-node` serves as the Policy Decision Point and Token Service: -- **Envoy `ext_authz` (`--ext-authz-addr`) and `ext_proc`:** +- **Envoy `ext_authz` and `ext_proc` (on the local API listeners):** Istio `AuthorizationPolicy (action: CUSTOM)` or `agentgateway` calls - `sam-node`. When no Bearer token is present on a trusted proxy listener, - `sam-node` reads the caller's verified SPIFFE ID from - `AttributeContext.Source.Principal` or `X-Forwarded-Client-Cert` (XFCC), - exchanges it into a Delegated Session Biscuit, evaluates the standing Datalog - policy and any `TaskAuthorizationRule` chain (including MCP tool names in - JSON-RPC bodies via `ext_proc`), and injects the upstream credential into - `Authorization` before the gateway forwards the request. + `sam-node` with the caller's Biscuit, or with a platform JWT that + `sam-node` exchanges at the control plane into a delegated Biscuit. + `sam-node` evaluates the standing Datalog policy and any + `TaskAuthorizationRule` chain (including MCP tool names in JSON-RPC bodies + via `ext_proc`), and for `egress://` targets injects the brokered upstream + credential into `Authorization` before the gateway forwards the request. - **RFC 8693 backend token exchange (`POST /oauth/token`):** `agentgateway`'s built-in RFC 8693 token exchange policy can point directly at `http://sam-node:8080/oauth/token` to exchange workload JWTs or narrow diff --git a/site/content/docs/reference/node-api.md b/site/content/docs/reference/node-api.md index d41355e6..6e6e7911 100644 --- a/site/content/docs/reference/node-api.md +++ b/site/content/docs/reference/node-api.md @@ -8,9 +8,9 @@ aliases: The local API that `sam-node run` serves to agents, gateways, and scripts on the same machine or cluster. It is available on TCP (`--bind-addr`, default -`127.0.0.1:8080`), on a Unix socket (`--socket-path`, default -`/sam.sock`), and optionally on a dedicated Envoy `ext_authz` / -`ext_proc` listener (`--ext-authz-addr`). +`127.0.0.1:8080`) and on a Unix socket (`--socket-path`, default +`/sam.sock`). The Envoy `ext_authz` and `ext_proc` endpoints are +served on the same listeners. ## Authentication @@ -85,17 +85,21 @@ revocation ID in the node's local revocation cache until the token expires. ## Envoy `ext_authz` and `ext_proc` gateway integration -When `--ext-authz-addr` (`host:port` or `unix:/path`) is set (or on the local -API listener), `sam-node` serves: +On the local API listeners, `sam-node` serves: - **Envoy HTTP `ext_authz` (`/ext_authz`, `/ext_authz/*`)** and **gRPC `envoy.service.auth.v3.Authorization/Check`**: evaluates standing Datalog - policy and any `tar_block` chain on the request. On a trusted `--ext-authz-addr` - listener, if no Bearer token is present, `sam-node` extracts the caller's - verified SPIFFE principal from `AttributeContext.Source.Principal` or - `X-Forwarded-Client-Cert` (XFCC) and exchanges it into a Delegated Session - Biscuit. On `OK`, it injects `Authorization: Bearer `, - `X-Sam-Principal`, and `X-Sam-Task-Id`. + policy and any `tar_block` chain on the request. The credential is read + from `X-Sam-Biscuit`, or from a Bearer value in `X-Sam-Authentication` or + `Authorization`. A Bearer value that is a platform JWT is exchanged at the + control plane (`POST /token/exchange`) into a delegated Biscuit before + evaluation; a request with no credential is refused with `401`. The target + service is taken from `X-Sam-Target-Service` or from the `/sam///` + request path. On `OK`, the response carries `X-Sam-Biscuit`, + `X-Sam-Principal`, `X-Sam-Roles`, and, when the token is task-attenuated, + `X-Sam-Task` (the last `TaskAuthorizationRule` as protojson) and + `X-Sam-Task-Id`. For an `egress://` target with a configured credential + broker it also carries the brokered `Authorization` header for the upstream. - **Envoy gRPC `envoy.service.ext_proc.v3.ExternalProcessor/Process`**: the body-aware gateway processor. It inspects JSON-RPC bodies on MCP `tools/call` requests to enforce `operation.allowed_tools` in `TaskAuthorizationRule` From 425bd75a80ea70fd97150aeab154212d52fcfab7 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 6 Oct 2026 18:27:53 +0000 Subject: [PATCH 03/11] api: operator plane serves api.proto messages /admin/status, /user/status, /admin/bootstrap-tokens, /user/bootstrap-tokens and /admin/enrollments encoded Go structs with json tags, internal/storage records and map[string]any. AGENTS.md reserves every SAM-defined surface for messages in api/sam.proto, and after v0.1.0 the proto is additive-only, so this is the last release that can change these shapes. New messages: BootstrapTokenCreateRequest/Response, BootstrapToken and BootstrapTokenListResponse, EnrollmentRequest and EnrollmentRequestListResponse, User, EnrolledNode, RouterLease, NodeServices, AdminStatusResponse and UserStatusResponse. Instants are Timestamps named *_time; the policy travels as an embedded PolicyConfig instead of a JSON string; the enrollment status is the enum name. The handlers read and write protojson with proto field names and reject unknown fields. Field names of the request body (role, ttl_hours, max_usages, description, owner_id, autonomous_recovery) and the `token` in the response are unchanged, so the Helm bootstrap job, deploy.yaml and the bats suites keep working. /admin/status no longer carries each enrolled node's biscuit and public key; the integration tests that compared the issued biscuit now read it from the control plane's own store. api/bootstrap_token.go is removed; sam-one, the console and the tests use the generated bindings. SDK bindings regenerated. --- api/bootstrap_token.go | 55 - api/sam.pb.go | 1341 +++++++++++++++-- api/sam.proto | 154 ++ cmd/sam-one/admin.go | 49 +- cmd/sam-one/admin_test.go | 35 +- internal/console/public/app.js | 100 +- internal/controlplane/approval_labels_test.go | 13 +- internal/controlplane/catalog.go | 34 +- internal/controlplane/catalog_test.go | 25 +- .../controlplane/identity_lifecycle_test.go | 26 +- internal/controlplane/server.go | 248 ++- internal/controlplane/server_test.go | 98 +- internal/controlplane/ui.go | 50 +- internal/controlplane/views.go | 143 ++ sdk/js/src/gen/sam_pb.ts | 571 ++++++- sdk/python/src/agent_mesh/_proto/sam_pb2.py | 106 +- sdk/python/src/agent_mesh/_proto/sam_pb2.pyi | 201 +++ .../docs/guides/headless-enrollment.md | 2 +- site/content/docs/reference/control-plane.md | 21 +- .../integration/enroll_status_polling_test.go | 13 +- tests/integration/login_test.go | 35 +- tests/integration/minimal_helpers_test.go | 37 +- tests/integration/rotation_test.go | 2 +- tests/integration/sdk_enroll_test.go | 21 +- tests/ui/console.spec.js | 20 +- 25 files changed, 2765 insertions(+), 635 deletions(-) delete mode 100644 api/bootstrap_token.go create mode 100644 internal/controlplane/views.go diff --git a/api/bootstrap_token.go b/api/bootstrap_token.go deleted file mode 100644 index 03b05a87..00000000 --- a/api/bootstrap_token.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package api - -// BootstrapTokenRequest is the JSON body that mints a bootstrap token, on -// POST /admin/bootstrap-tokens (admin bearer) and POST /users/me/tokens -// (OIDC user). It is the one definition both the control plane and its -// clients (sam-one's CLI, the console) marshal, so a field name exists in -// exactly one place. -type BootstrapTokenRequest struct { - // Role the token enrolls into, e.g. RoleNode. Required on the admin - // endpoint; the user endpoint defaults it to RoleNode. - Role string `json:"role"` - // OwnerID is the user the token is issued on behalf of. Honored by the - // user endpoint only, and only for admins; defaults to the caller. - OwnerID string `json:"owner_id,omitempty"` - // TTLHours bounds the token's validity; the control plane defaults a - // non-positive value to 24. - TTLHours int `json:"ttl_hours"` - // MaxUsages is how many enrollments the token admits; the control plane - // defaults a non-positive value to 1. - MaxUsages int `json:"max_usages"` - // Description is a free-form operator note stored with the token. - Description string `json:"description,omitempty"` - // AutonomousRecovery is copied onto every node the token enrolls: such a - // node may still refresh its credential after the control plane's - // signing key rotated past its grace period, on proof of possession of - // its own key alone. Admin-only, because a node that can always recover - // holds a credential that never expires (see - // storage.EnrolledNode.AutonomousRecovery). - AutonomousRecovery bool `json:"autonomous_recovery"` -} - -// BootstrapTokenResponse is returned (201) when a bootstrap token is minted. -// Token is the plaintext and is shown exactly once; the control plane keeps -// only its hash, which is also the ID. -type BootstrapTokenResponse struct { - ID string `json:"id"` - Token string `json:"token"` - Role string `json:"role"` - OwnerID string `json:"owner_id,omitempty"` - ExpiresAt string `json:"expires_at"` -} diff --git a/api/sam.pb.go b/api/sam.pb.go index 6a78d874..e2264df2 100644 --- a/api/sam.pb.go +++ b/api/sam.pb.go @@ -3263,6 +3263,1008 @@ func (x *TokenRevokeResponse) GetError() string { return "" } +// BootstrapTokenCreateRequest is the body of POST /admin/bootstrap-tokens +// (admin bearer) and POST /user/bootstrap-tokens (mesh user). +type BootstrapTokenCreateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Role the token enrolls into, e.g. "sam:role:node". Required on the admin + // endpoint; the user endpoint defaults it to "sam:role:node". + Role string `protobuf:"bytes,1,opt,name=role,proto3" json:"role,omitempty"` + // User the token is issued on behalf of. Honored by the user endpoint + // only, and only for admins; defaults to the caller. + OwnerId string `protobuf:"bytes,2,opt,name=owner_id,json=ownerId,proto3" json:"owner_id,omitempty"` + // How long the token stays valid; unset or non-positive means 24. + TtlHours int32 `protobuf:"varint,3,opt,name=ttl_hours,json=ttlHours,proto3" json:"ttl_hours,omitempty"` + // How many enrollments the token admits; unset or non-positive means 1. + MaxUsages int32 `protobuf:"varint,4,opt,name=max_usages,json=maxUsages,proto3" json:"max_usages,omitempty"` + // Free-form operator note stored with the token. + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + // Copied onto every node the token enrolls: such a node may still refresh + // its credential after the control plane's signing key rotated past its + // grace period, on proof of possession of its own key alone. Admin-only, + // because a node that can always recover holds a credential that never + // expires. + AutonomousRecovery bool `protobuf:"varint,6,opt,name=autonomous_recovery,json=autonomousRecovery,proto3" json:"autonomous_recovery,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BootstrapTokenCreateRequest) Reset() { + *x = BootstrapTokenCreateRequest{} + mi := &file_api_sam_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BootstrapTokenCreateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BootstrapTokenCreateRequest) ProtoMessage() {} + +func (x *BootstrapTokenCreateRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[40] + 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 BootstrapTokenCreateRequest.ProtoReflect.Descriptor instead. +func (*BootstrapTokenCreateRequest) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{40} +} + +func (x *BootstrapTokenCreateRequest) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *BootstrapTokenCreateRequest) GetOwnerId() string { + if x != nil { + return x.OwnerId + } + return "" +} + +func (x *BootstrapTokenCreateRequest) GetTtlHours() int32 { + if x != nil { + return x.TtlHours + } + return 0 +} + +func (x *BootstrapTokenCreateRequest) GetMaxUsages() int32 { + if x != nil { + return x.MaxUsages + } + return 0 +} + +func (x *BootstrapTokenCreateRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *BootstrapTokenCreateRequest) GetAutonomousRecovery() bool { + if x != nil { + return x.AutonomousRecovery + } + return false +} + +// BootstrapTokenCreateResponse is returned (201) when a token is minted. +// token is the plaintext and is shown exactly once; the control plane keeps +// only its hash, which is also the id. +type BootstrapTokenCreateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` + Role string `protobuf:"bytes,3,opt,name=role,proto3" json:"role,omitempty"` + OwnerId string `protobuf:"bytes,4,opt,name=owner_id,json=ownerId,proto3" json:"owner_id,omitempty"` + ExpireTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=expire_time,json=expireTime,proto3" json:"expire_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BootstrapTokenCreateResponse) Reset() { + *x = BootstrapTokenCreateResponse{} + mi := &file_api_sam_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BootstrapTokenCreateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BootstrapTokenCreateResponse) ProtoMessage() {} + +func (x *BootstrapTokenCreateResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[41] + 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 BootstrapTokenCreateResponse.ProtoReflect.Descriptor instead. +func (*BootstrapTokenCreateResponse) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{41} +} + +func (x *BootstrapTokenCreateResponse) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *BootstrapTokenCreateResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *BootstrapTokenCreateResponse) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *BootstrapTokenCreateResponse) GetOwnerId() string { + if x != nil { + return x.OwnerId + } + return "" +} + +func (x *BootstrapTokenCreateResponse) GetExpireTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpireTime + } + return nil +} + +// BootstrapToken is a minted token as operators list it. +type BootstrapToken struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"` + OwnerId string `protobuf:"bytes,3,opt,name=owner_id,json=ownerId,proto3" json:"owner_id,omitempty"` + MaxUsages int32 `protobuf:"varint,4,opt,name=max_usages,json=maxUsages,proto3" json:"max_usages,omitempty"` + UsagesCount int32 `protobuf:"varint,5,opt,name=usages_count,json=usagesCount,proto3" json:"usages_count,omitempty"` + Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` + CreateTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + ExpireTime *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=expire_time,json=expireTime,proto3" json:"expire_time,omitempty"` + // Set when an operator revoked the token, which is distinct from expiry + // or exhausted usages. Unset means never revoked. + RevokeTime *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=revoke_time,json=revokeTime,proto3" json:"revoke_time,omitempty"` + AutonomousRecovery bool `protobuf:"varint,10,opt,name=autonomous_recovery,json=autonomousRecovery,proto3" json:"autonomous_recovery,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BootstrapToken) Reset() { + *x = BootstrapToken{} + mi := &file_api_sam_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BootstrapToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BootstrapToken) ProtoMessage() {} + +func (x *BootstrapToken) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[42] + 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 BootstrapToken.ProtoReflect.Descriptor instead. +func (*BootstrapToken) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{42} +} + +func (x *BootstrapToken) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *BootstrapToken) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *BootstrapToken) GetOwnerId() string { + if x != nil { + return x.OwnerId + } + return "" +} + +func (x *BootstrapToken) GetMaxUsages() int32 { + if x != nil { + return x.MaxUsages + } + return 0 +} + +func (x *BootstrapToken) GetUsagesCount() int32 { + if x != nil { + return x.UsagesCount + } + return 0 +} + +func (x *BootstrapToken) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *BootstrapToken) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *BootstrapToken) GetExpireTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpireTime + } + return nil +} + +func (x *BootstrapToken) GetRevokeTime() *timestamppb.Timestamp { + if x != nil { + return x.RevokeTime + } + return nil +} + +func (x *BootstrapToken) GetAutonomousRecovery() bool { + if x != nil { + return x.AutonomousRecovery + } + return false +} + +type BootstrapTokenListResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tokens []*BootstrapToken `protobuf:"bytes,1,rep,name=tokens,proto3" json:"tokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BootstrapTokenListResponse) Reset() { + *x = BootstrapTokenListResponse{} + mi := &file_api_sam_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BootstrapTokenListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BootstrapTokenListResponse) ProtoMessage() {} + +func (x *BootstrapTokenListResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[43] + 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 BootstrapTokenListResponse.ProtoReflect.Descriptor instead. +func (*BootstrapTokenListResponse) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{43} +} + +func (x *BootstrapTokenListResponse) GetTokens() []*BootstrapToken { + if x != nil { + return x.Tokens + } + return nil +} + +// EnrollmentRequest is a bootstrap enrollment awaiting or past an operator +// decision (see BootstrapEnrollRequest). +type EnrollmentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + PeerId string `protobuf:"bytes,2,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + // The bootstrap token the request was made with. + TokenId string `protobuf:"bytes,3,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"` + Status EnrollmentStatus `protobuf:"varint,4,opt,name=status,proto3,enum=sam.v1.EnrollmentStatus" json:"status,omitempty"` + // Labels the node declared; approval attests them into its biscuit. + Labels map[string]string `protobuf:"bytes,5,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + CreateTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // Unset while the request is pending. + ResolveTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=resolve_time,json=resolveTime,proto3" json:"resolve_time,omitempty"` + ResolvedBy string `protobuf:"bytes,8,opt,name=resolved_by,json=resolvedBy,proto3" json:"resolved_by,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnrollmentRequest) Reset() { + *x = EnrollmentRequest{} + mi := &file_api_sam_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnrollmentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnrollmentRequest) ProtoMessage() {} + +func (x *EnrollmentRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[44] + 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 EnrollmentRequest.ProtoReflect.Descriptor instead. +func (*EnrollmentRequest) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{44} +} + +func (x *EnrollmentRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *EnrollmentRequest) GetPeerId() string { + if x != nil { + return x.PeerId + } + return "" +} + +func (x *EnrollmentRequest) GetTokenId() string { + if x != nil { + return x.TokenId + } + return "" +} + +func (x *EnrollmentRequest) GetStatus() EnrollmentStatus { + if x != nil { + return x.Status + } + return EnrollmentStatus_ENROLLMENT_STATUS_UNSPECIFIED +} + +func (x *EnrollmentRequest) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *EnrollmentRequest) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *EnrollmentRequest) GetResolveTime() *timestamppb.Timestamp { + if x != nil { + return x.ResolveTime + } + return nil +} + +func (x *EnrollmentRequest) GetResolvedBy() string { + if x != nil { + return x.ResolvedBy + } + return "" +} + +type EnrollmentRequestListResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Requests []*EnrollmentRequest `protobuf:"bytes,1,rep,name=requests,proto3" json:"requests,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnrollmentRequestListResponse) Reset() { + *x = EnrollmentRequestListResponse{} + mi := &file_api_sam_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnrollmentRequestListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnrollmentRequestListResponse) ProtoMessage() {} + +func (x *EnrollmentRequestListResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[45] + 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 EnrollmentRequestListResponse.ProtoReflect.Descriptor instead. +func (*EnrollmentRequestListResponse) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{45} +} + +func (x *EnrollmentRequestListResponse) GetRequests() []*EnrollmentRequest { + if x != nil { + return x.Requests + } + return nil +} + +// User is a human identity known to the mesh. +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The identity provider's subject. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Issuer string `protobuf:"bytes,2,opt,name=issuer,proto3" json:"issuer,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + // "admin" or "user". + Role string `protobuf:"bytes,4,opt,name=role,proto3" json:"role,omitempty"` + CreateTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_api_sam_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[46] + 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 User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{46} +} + +func (x *User) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *User) GetIssuer() string { + if x != nil { + return x.Issuer + } + return "" +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *User) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +// EnrolledNode is a member's enrollment record without its credential. +type EnrolledNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"` + // How the node enrolled, e.g. "oidc" or "bootstrap". + EnrollmentType string `protobuf:"bytes,3,opt,name=enrollment_type,json=enrollmentType,proto3" json:"enrollment_type,omitempty"` + // The identity provider's claims as stored at enrollment. Admin-only. + ClaimsJson string `protobuf:"bytes,4,opt,name=claims_json,json=claimsJson,proto3" json:"claims_json,omitempty"` + OwnerId string `protobuf:"bytes,5,opt,name=owner_id,json=ownerId,proto3" json:"owner_id,omitempty"` + Labels map[string]string `protobuf:"bytes,6,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + EnrollTime *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=enroll_time,json=enrollTime,proto3" json:"enroll_time,omitempty"` + // When the enrollment session ends; unset means it does not expire. + ExpireTime *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=expire_time,json=expireTime,proto3" json:"expire_time,omitempty"` + Banned bool `protobuf:"varint,9,opt,name=banned,proto3" json:"banned,omitempty"` + AutonomousRecovery bool `protobuf:"varint,10,opt,name=autonomous_recovery,json=autonomousRecovery,proto3" json:"autonomous_recovery,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnrolledNode) Reset() { + *x = EnrolledNode{} + mi := &file_api_sam_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnrolledNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnrolledNode) ProtoMessage() {} + +func (x *EnrolledNode) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[47] + 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 EnrolledNode.ProtoReflect.Descriptor instead. +func (*EnrolledNode) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{47} +} + +func (x *EnrolledNode) GetPeerId() string { + if x != nil { + return x.PeerId + } + return "" +} + +func (x *EnrolledNode) GetRole() string { + if x != nil { + return x.Role + } + return "" +} + +func (x *EnrolledNode) GetEnrollmentType() string { + if x != nil { + return x.EnrollmentType + } + return "" +} + +func (x *EnrolledNode) GetClaimsJson() string { + if x != nil { + return x.ClaimsJson + } + return "" +} + +func (x *EnrolledNode) GetOwnerId() string { + if x != nil { + return x.OwnerId + } + return "" +} + +func (x *EnrolledNode) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + +func (x *EnrolledNode) GetEnrollTime() *timestamppb.Timestamp { + if x != nil { + return x.EnrollTime + } + return nil +} + +func (x *EnrolledNode) GetExpireTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpireTime + } + return nil +} + +func (x *EnrolledNode) GetBanned() bool { + if x != nil { + return x.Banned + } + return false +} + +func (x *EnrolledNode) GetAutonomousRecovery() bool { + if x != nil { + return x.AutonomousRecovery + } + return false +} + +// RouterLease is a router's current registration with the control plane. +type RouterLease struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + // Multiaddrs, `/p2p/` suffixed. + Addresses []string `protobuf:"bytes,2,rep,name=addresses,proto3" json:"addresses,omitempty"` + LastRenewalTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_renewal_time,json=lastRenewalTime,proto3" json:"last_renewal_time,omitempty"` + ExpireTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expire_time,json=expireTime,proto3" json:"expire_time,omitempty"` + ConnectedPeers []string `protobuf:"bytes,5,rep,name=connected_peers,json=connectedPeers,proto3" json:"connected_peers,omitempty"` + DhtSize int32 `protobuf:"varint,6,opt,name=dht_size,json=dhtSize,proto3" json:"dht_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RouterLease) Reset() { + *x = RouterLease{} + mi := &file_api_sam_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RouterLease) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RouterLease) ProtoMessage() {} + +func (x *RouterLease) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[48] + 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 RouterLease.ProtoReflect.Descriptor instead. +func (*RouterLease) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{48} +} + +func (x *RouterLease) GetPeerId() string { + if x != nil { + return x.PeerId + } + return "" +} + +func (x *RouterLease) GetAddresses() []string { + if x != nil { + return x.Addresses + } + return nil +} + +func (x *RouterLease) GetLastRenewalTime() *timestamppb.Timestamp { + if x != nil { + return x.LastRenewalTime + } + return nil +} + +func (x *RouterLease) GetExpireTime() *timestamppb.Timestamp { + if x != nil { + return x.ExpireTime + } + return nil +} + +func (x *RouterLease) GetConnectedPeers() []string { + if x != nil { + return x.ConnectedPeers + } + return nil +} + +func (x *RouterLease) GetDhtSize() int32 { + if x != nil { + return x.DhtSize + } + return 0 +} + +// NodeServices is the services one node last reported (see +// NodeCatalogReport). +type NodeServices struct { + state protoimpl.MessageState `protogen:"open.v1"` + Services []*ServiceInfo `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` + ReportTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=report_time,json=reportTime,proto3" json:"report_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NodeServices) Reset() { + *x = NodeServices{} + mi := &file_api_sam_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NodeServices) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeServices) ProtoMessage() {} + +func (x *NodeServices) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[49] + 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 NodeServices.ProtoReflect.Descriptor instead. +func (*NodeServices) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{49} +} + +func (x *NodeServices) GetServices() []*ServiceInfo { + if x != nil { + return x.Services + } + return nil +} + +func (x *NodeServices) GetReportTime() *timestamppb.Timestamp { + if x != nil { + return x.ReportTime + } + return nil +} + +// AdminStatusResponse answers GET /admin/status: everything the console +// shows an administrator. +type AdminStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + ActiveRouters []*RouterLease `protobuf:"bytes,2,rep,name=active_routers,json=activeRouters,proto3" json:"active_routers,omitempty"` + EnrolledNodes []*EnrolledNode `protobuf:"bytes,3,rep,name=enrolled_nodes,json=enrolledNodes,proto3" json:"enrolled_nodes,omitempty"` + EnrollmentRequests []*EnrollmentRequest `protobuf:"bytes,4,rep,name=enrollment_requests,json=enrollmentRequests,proto3" json:"enrollment_requests,omitempty"` + BootstrapTokens []*BootstrapToken `protobuf:"bytes,5,rep,name=bootstrap_tokens,json=bootstrapTokens,proto3" json:"bootstrap_tokens,omitempty"` + Policy *PolicyConfig `protobuf:"bytes,6,opt,name=policy,proto3" json:"policy,omitempty"` + // Keyed by the reporting node's peer ID; admitted nodes only. + NodeCatalog map[string]*NodeServices `protobuf:"bytes,7,rep,name=node_catalog,json=nodeCatalog,proto3" json:"node_catalog,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminStatusResponse) Reset() { + *x = AdminStatusResponse{} + mi := &file_api_sam_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminStatusResponse) ProtoMessage() {} + +func (x *AdminStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[50] + 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 AdminStatusResponse.ProtoReflect.Descriptor instead. +func (*AdminStatusResponse) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{50} +} + +func (x *AdminStatusResponse) GetUsers() []*User { + if x != nil { + return x.Users + } + return nil +} + +func (x *AdminStatusResponse) GetActiveRouters() []*RouterLease { + if x != nil { + return x.ActiveRouters + } + return nil +} + +func (x *AdminStatusResponse) GetEnrolledNodes() []*EnrolledNode { + if x != nil { + return x.EnrolledNodes + } + return nil +} + +func (x *AdminStatusResponse) GetEnrollmentRequests() []*EnrollmentRequest { + if x != nil { + return x.EnrollmentRequests + } + return nil +} + +func (x *AdminStatusResponse) GetBootstrapTokens() []*BootstrapToken { + if x != nil { + return x.BootstrapTokens + } + return nil +} + +func (x *AdminStatusResponse) GetPolicy() *PolicyConfig { + if x != nil { + return x.Policy + } + return nil +} + +func (x *AdminStatusResponse) GetNodeCatalog() map[string]*NodeServices { + if x != nil { + return x.NodeCatalog + } + return nil +} + +// UserStatusResponse answers GET /user/status: the caller and what it owns. +// The router fleet and the mesh policy describe the whole mesh and are set +// for an administrator only. +type UserStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + EnrolledNodes []*EnrolledNode `protobuf:"bytes,2,rep,name=enrolled_nodes,json=enrolledNodes,proto3" json:"enrolled_nodes,omitempty"` + BootstrapTokens []*BootstrapToken `protobuf:"bytes,3,rep,name=bootstrap_tokens,json=bootstrapTokens,proto3" json:"bootstrap_tokens,omitempty"` + ActiveRouters []*RouterLease `protobuf:"bytes,4,rep,name=active_routers,json=activeRouters,proto3" json:"active_routers,omitempty"` + Policy *PolicyConfig `protobuf:"bytes,5,opt,name=policy,proto3" json:"policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserStatusResponse) Reset() { + *x = UserStatusResponse{} + mi := &file_api_sam_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserStatusResponse) ProtoMessage() {} + +func (x *UserStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_sam_proto_msgTypes[51] + 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 UserStatusResponse.ProtoReflect.Descriptor instead. +func (*UserStatusResponse) Descriptor() ([]byte, []int) { + return file_api_sam_proto_rawDescGZIP(), []int{51} +} + +func (x *UserStatusResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +func (x *UserStatusResponse) GetEnrolledNodes() []*EnrolledNode { + if x != nil { + return x.EnrolledNodes + } + return nil +} + +func (x *UserStatusResponse) GetBootstrapTokens() []*BootstrapToken { + if x != nil { + return x.BootstrapTokens + } + return nil +} + +func (x *UserStatusResponse) GetActiveRouters() []*RouterLease { + if x != nil { + return x.ActiveRouters + } + return nil +} + +func (x *UserStatusResponse) GetPolicy() *PolicyConfig { + if x != nil { + return x.Policy + } + return nil +} + type IdentityEvidenceResponse struct { state protoimpl.MessageState `protogen:"open.v1"` PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` @@ -3277,7 +4279,7 @@ type IdentityEvidenceResponse struct { func (x *IdentityEvidenceResponse) Reset() { *x = IdentityEvidenceResponse{} - mi := &file_api_sam_proto_msgTypes[40] + mi := &file_api_sam_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3289,7 +4291,7 @@ func (x *IdentityEvidenceResponse) String() string { func (*IdentityEvidenceResponse) ProtoMessage() {} func (x *IdentityEvidenceResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[40] + mi := &file_api_sam_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3302,7 +4304,7 @@ func (x *IdentityEvidenceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use IdentityEvidenceResponse.ProtoReflect.Descriptor instead. func (*IdentityEvidenceResponse) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{40} + return file_api_sam_proto_rawDescGZIP(), []int{52} } func (x *IdentityEvidenceResponse) GetPeerId() string { @@ -3363,7 +4365,7 @@ type PeerEvidenceResponse struct { func (x *PeerEvidenceResponse) Reset() { *x = PeerEvidenceResponse{} - mi := &file_api_sam_proto_msgTypes[41] + mi := &file_api_sam_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3375,7 +4377,7 @@ func (x *PeerEvidenceResponse) String() string { func (*PeerEvidenceResponse) ProtoMessage() {} func (x *PeerEvidenceResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[41] + mi := &file_api_sam_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3388,7 +4390,7 @@ func (x *PeerEvidenceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerEvidenceResponse.ProtoReflect.Descriptor instead. func (*PeerEvidenceResponse) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{41} + return file_api_sam_proto_rawDescGZIP(), []int{53} } func (x *PeerEvidenceResponse) GetPeerId() string { @@ -3473,7 +4475,7 @@ type MemberCredential struct { func (x *MemberCredential) Reset() { *x = MemberCredential{} - mi := &file_api_sam_proto_msgTypes[42] + mi := &file_api_sam_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3485,7 +4487,7 @@ func (x *MemberCredential) String() string { func (*MemberCredential) ProtoMessage() {} func (x *MemberCredential) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[42] + mi := &file_api_sam_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3498,7 +4500,7 @@ func (x *MemberCredential) ProtoReflect() protoreflect.Message { // Deprecated: Use MemberCredential.ProtoReflect.Descriptor instead. func (*MemberCredential) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{42} + return file_api_sam_proto_rawDescGZIP(), []int{54} } func (x *MemberCredential) GetControlPlaneUrl() string { @@ -3563,7 +4565,7 @@ type TrustedSigningKey struct { func (x *TrustedSigningKey) Reset() { *x = TrustedSigningKey{} - mi := &file_api_sam_proto_msgTypes[43] + mi := &file_api_sam_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3575,7 +4577,7 @@ func (x *TrustedSigningKey) String() string { func (*TrustedSigningKey) ProtoMessage() {} func (x *TrustedSigningKey) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[43] + mi := &file_api_sam_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3588,7 +4590,7 @@ func (x *TrustedSigningKey) ProtoReflect() protoreflect.Message { // Deprecated: Use TrustedSigningKey.ProtoReflect.Descriptor instead. func (*TrustedSigningKey) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{43} + return file_api_sam_proto_rawDescGZIP(), []int{55} } func (x *TrustedSigningKey) GetPublicKey() []byte { @@ -3617,7 +4619,7 @@ type OIDCSession struct { func (x *OIDCSession) Reset() { *x = OIDCSession{} - mi := &file_api_sam_proto_msgTypes[44] + mi := &file_api_sam_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3629,7 +4631,7 @@ func (x *OIDCSession) String() string { func (*OIDCSession) ProtoMessage() {} func (x *OIDCSession) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[44] + mi := &file_api_sam_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3642,7 +4644,7 @@ func (x *OIDCSession) ProtoReflect() protoreflect.Message { // Deprecated: Use OIDCSession.ProtoReflect.Descriptor instead. func (*OIDCSession) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{44} + return file_api_sam_proto_rawDescGZIP(), []int{56} } func (x *OIDCSession) GetIssuer() string { @@ -3694,7 +4696,7 @@ type TaskAuthorizationRule struct { func (x *TaskAuthorizationRule) Reset() { *x = TaskAuthorizationRule{} - mi := &file_api_sam_proto_msgTypes[45] + mi := &file_api_sam_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3706,7 +4708,7 @@ func (x *TaskAuthorizationRule) String() string { func (*TaskAuthorizationRule) ProtoMessage() {} func (x *TaskAuthorizationRule) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[45] + mi := &file_api_sam_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3719,7 +4721,7 @@ func (x *TaskAuthorizationRule) ProtoReflect() protoreflect.Message { // Deprecated: Use TaskAuthorizationRule.ProtoReflect.Descriptor instead. func (*TaskAuthorizationRule) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{45} + return file_api_sam_proto_rawDescGZIP(), []int{57} } func (x *TaskAuthorizationRule) GetName() string { @@ -3771,7 +4773,7 @@ type TaskRule struct { func (x *TaskRule) Reset() { *x = TaskRule{} - mi := &file_api_sam_proto_msgTypes[46] + mi := &file_api_sam_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3783,7 +4785,7 @@ func (x *TaskRule) String() string { func (*TaskRule) ProtoMessage() {} func (x *TaskRule) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[46] + mi := &file_api_sam_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3796,7 +4798,7 @@ func (x *TaskRule) ProtoReflect() protoreflect.Message { // Deprecated: Use TaskRule.ProtoReflect.Descriptor instead. func (*TaskRule) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{46} + return file_api_sam_proto_rawDescGZIP(), []int{58} } func (x *TaskRule) GetDescription() string { @@ -3845,7 +4847,7 @@ type TaskOperation struct { func (x *TaskOperation) Reset() { *x = TaskOperation{} - mi := &file_api_sam_proto_msgTypes[47] + mi := &file_api_sam_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3857,7 +4859,7 @@ func (x *TaskOperation) String() string { func (*TaskOperation) ProtoMessage() {} func (x *TaskOperation) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[47] + mi := &file_api_sam_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3870,7 +4872,7 @@ func (x *TaskOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use TaskOperation.ProtoReflect.Descriptor instead. func (*TaskOperation) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{47} + return file_api_sam_proto_rawDescGZIP(), []int{59} } func (x *TaskOperation) GetAllowedTools() []string { @@ -3930,7 +4932,7 @@ type TokenExchangeRequest struct { func (x *TokenExchangeRequest) Reset() { *x = TokenExchangeRequest{} - mi := &file_api_sam_proto_msgTypes[48] + mi := &file_api_sam_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3942,7 +4944,7 @@ func (x *TokenExchangeRequest) String() string { func (*TokenExchangeRequest) ProtoMessage() {} func (x *TokenExchangeRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[48] + mi := &file_api_sam_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3955,7 +4957,7 @@ func (x *TokenExchangeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TokenExchangeRequest.ProtoReflect.Descriptor instead. func (*TokenExchangeRequest) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{48} + return file_api_sam_proto_rawDescGZIP(), []int{60} } func (x *TokenExchangeRequest) GetSubjectToken() string { @@ -4005,7 +5007,7 @@ type TokenExchangeResponse struct { func (x *TokenExchangeResponse) Reset() { *x = TokenExchangeResponse{} - mi := &file_api_sam_proto_msgTypes[49] + mi := &file_api_sam_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4017,7 +5019,7 @@ func (x *TokenExchangeResponse) String() string { func (*TokenExchangeResponse) ProtoMessage() {} func (x *TokenExchangeResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[49] + mi := &file_api_sam_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4030,7 +5032,7 @@ func (x *TokenExchangeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TokenExchangeResponse.ProtoReflect.Descriptor instead. func (*TokenExchangeResponse) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{49} + return file_api_sam_proto_rawDescGZIP(), []int{61} } func (x *TokenExchangeResponse) GetBiscuitToken() []byte { @@ -4086,7 +5088,7 @@ type STSTokenRequest struct { func (x *STSTokenRequest) Reset() { *x = STSTokenRequest{} - mi := &file_api_sam_proto_msgTypes[50] + mi := &file_api_sam_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4098,7 +5100,7 @@ func (x *STSTokenRequest) String() string { func (*STSTokenRequest) ProtoMessage() {} func (x *STSTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[50] + mi := &file_api_sam_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4111,7 +5113,7 @@ func (x *STSTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use STSTokenRequest.ProtoReflect.Descriptor instead. func (*STSTokenRequest) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{50} + return file_api_sam_proto_rawDescGZIP(), []int{62} } func (x *STSTokenRequest) GetBiscuit() []byte { @@ -4162,7 +5164,7 @@ type STSTokenResponse struct { func (x *STSTokenResponse) Reset() { *x = STSTokenResponse{} - mi := &file_api_sam_proto_msgTypes[51] + mi := &file_api_sam_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4174,7 +5176,7 @@ func (x *STSTokenResponse) String() string { func (*STSTokenResponse) ProtoMessage() {} func (x *STSTokenResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[51] + mi := &file_api_sam_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4187,7 +5189,7 @@ func (x *STSTokenResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use STSTokenResponse.ProtoReflect.Descriptor instead. func (*STSTokenResponse) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{51} + return file_api_sam_proto_rawDescGZIP(), []int{63} } func (x *STSTokenResponse) GetJwt() string { @@ -4238,7 +5240,7 @@ type RevocationsResponse struct { func (x *RevocationsResponse) Reset() { *x = RevocationsResponse{} - mi := &file_api_sam_proto_msgTypes[52] + mi := &file_api_sam_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4250,7 +5252,7 @@ func (x *RevocationsResponse) String() string { func (*RevocationsResponse) ProtoMessage() {} func (x *RevocationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_sam_proto_msgTypes[52] + mi := &file_api_sam_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4263,7 +5265,7 @@ func (x *RevocationsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RevocationsResponse.ProtoReflect.Descriptor instead. func (*RevocationsResponse) Descriptor() ([]byte, []int) { - return file_api_sam_proto_rawDescGZIP(), []int{52} + return file_api_sam_proto_rawDescGZIP(), []int{64} } func (x *RevocationsResponse) GetRevocationIds() []string { @@ -4524,7 +5526,110 @@ const file_api_sam_proto_rawDesc = "" + "\apeer_id\x18\x01 \x01(\tR\x06peerId\"E\n" + "\x13TokenRevokeResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x14\n" + - "\x05error\x18\x02 \x01(\tR\x05error\"\xbd\x02\n" + + "\x05error\x18\x02 \x01(\tR\x05error\"\xdb\x01\n" + + "\x1bBootstrapTokenCreateRequest\x12\x12\n" + + "\x04role\x18\x01 \x01(\tR\x04role\x12\x19\n" + + "\bowner_id\x18\x02 \x01(\tR\aownerId\x12\x1b\n" + + "\tttl_hours\x18\x03 \x01(\x05R\bttlHours\x12\x1d\n" + + "\n" + + "max_usages\x18\x04 \x01(\x05R\tmaxUsages\x12 \n" + + "\vdescription\x18\x05 \x01(\tR\vdescription\x12/\n" + + "\x13autonomous_recovery\x18\x06 \x01(\bR\x12autonomousRecovery\"\xb0\x01\n" + + "\x1cBootstrapTokenCreateResponse\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05token\x18\x02 \x01(\tR\x05token\x12\x12\n" + + "\x04role\x18\x03 \x01(\tR\x04role\x12\x19\n" + + "\bowner_id\x18\x04 \x01(\tR\aownerId\x12;\n" + + "\vexpire_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "expireTime\"\x9b\x03\n" + + "\x0eBootstrapToken\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04role\x18\x02 \x01(\tR\x04role\x12\x19\n" + + "\bowner_id\x18\x03 \x01(\tR\aownerId\x12\x1d\n" + + "\n" + + "max_usages\x18\x04 \x01(\x05R\tmaxUsages\x12!\n" + + "\fusages_count\x18\x05 \x01(\x05R\vusagesCount\x12 \n" + + "\vdescription\x18\x06 \x01(\tR\vdescription\x12;\n" + + "\vcreate_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "createTime\x12;\n" + + "\vexpire_time\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "expireTime\x12;\n" + + "\vrevoke_time\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "revokeTime\x12/\n" + + "\x13autonomous_recovery\x18\n" + + " \x01(\bR\x12autonomousRecovery\"L\n" + + "\x1aBootstrapTokenListResponse\x12.\n" + + "\x06tokens\x18\x01 \x03(\v2\x16.sam.v1.BootstrapTokenR\x06tokens\"\xa0\x03\n" + + "\x11EnrollmentRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" + + "\apeer_id\x18\x02 \x01(\tR\x06peerId\x12\x19\n" + + "\btoken_id\x18\x03 \x01(\tR\atokenId\x120\n" + + "\x06status\x18\x04 \x01(\x0e2\x18.sam.v1.EnrollmentStatusR\x06status\x12=\n" + + "\x06labels\x18\x05 \x03(\v2%.sam.v1.EnrollmentRequest.LabelsEntryR\x06labels\x12;\n" + + "\vcreate_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "createTime\x12=\n" + + "\fresolve_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\vresolveTime\x12\x1f\n" + + "\vresolved_by\x18\b \x01(\tR\n" + + "resolvedBy\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"V\n" + + "\x1dEnrollmentRequestListResponse\x125\n" + + "\brequests\x18\x01 \x03(\v2\x19.sam.v1.EnrollmentRequestR\brequests\"\x95\x01\n" + + "\x04User\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x16\n" + + "\x06issuer\x18\x02 \x01(\tR\x06issuer\x12\x14\n" + + "\x05email\x18\x03 \x01(\tR\x05email\x12\x12\n" + + "\x04role\x18\x04 \x01(\tR\x04role\x12;\n" + + "\vcreate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "createTime\"\xd8\x03\n" + + "\fEnrolledNode\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\tR\x06peerId\x12\x12\n" + + "\x04role\x18\x02 \x01(\tR\x04role\x12'\n" + + "\x0fenrollment_type\x18\x03 \x01(\tR\x0eenrollmentType\x12\x1f\n" + + "\vclaims_json\x18\x04 \x01(\tR\n" + + "claimsJson\x12\x19\n" + + "\bowner_id\x18\x05 \x01(\tR\aownerId\x128\n" + + "\x06labels\x18\x06 \x03(\v2 .sam.v1.EnrolledNode.LabelsEntryR\x06labels\x12;\n" + + "\venroll_time\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "enrollTime\x12;\n" + + "\vexpire_time\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "expireTime\x12\x16\n" + + "\x06banned\x18\t \x01(\bR\x06banned\x12/\n" + + "\x13autonomous_recovery\x18\n" + + " \x01(\bR\x12autonomousRecovery\x1a9\n" + + "\vLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x8d\x02\n" + + "\vRouterLease\x12\x17\n" + + "\apeer_id\x18\x01 \x01(\tR\x06peerId\x12\x1c\n" + + "\taddresses\x18\x02 \x03(\tR\taddresses\x12F\n" + + "\x11last_renewal_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x0flastRenewalTime\x12;\n" + + "\vexpire_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "expireTime\x12'\n" + + "\x0fconnected_peers\x18\x05 \x03(\tR\x0econnectedPeers\x12\x19\n" + + "\bdht_size\x18\x06 \x01(\x05R\adhtSize\"|\n" + + "\fNodeServices\x12/\n" + + "\bservices\x18\x01 \x03(\v2\x13.sam.v1.ServiceInfoR\bservices\x12;\n" + + "\vreport_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "reportTime\"\x96\x04\n" + + "\x13AdminStatusResponse\x12\"\n" + + "\x05users\x18\x01 \x03(\v2\f.sam.v1.UserR\x05users\x12:\n" + + "\x0eactive_routers\x18\x02 \x03(\v2\x13.sam.v1.RouterLeaseR\ractiveRouters\x12;\n" + + "\x0eenrolled_nodes\x18\x03 \x03(\v2\x14.sam.v1.EnrolledNodeR\renrolledNodes\x12J\n" + + "\x13enrollment_requests\x18\x04 \x03(\v2\x19.sam.v1.EnrollmentRequestR\x12enrollmentRequests\x12A\n" + + "\x10bootstrap_tokens\x18\x05 \x03(\v2\x16.sam.v1.BootstrapTokenR\x0fbootstrapTokens\x12,\n" + + "\x06policy\x18\x06 \x01(\v2\x14.sam.v1.PolicyConfigR\x06policy\x12O\n" + + "\fnode_catalog\x18\a \x03(\v2,.sam.v1.AdminStatusResponse.NodeCatalogEntryR\vnodeCatalog\x1aT\n" + + "\x10NodeCatalogEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12*\n" + + "\x05value\x18\x02 \x01(\v2\x14.sam.v1.NodeServicesR\x05value:\x028\x01\"\xa0\x02\n" + + "\x12UserStatusResponse\x12 \n" + + "\x04user\x18\x01 \x01(\v2\f.sam.v1.UserR\x04user\x12;\n" + + "\x0eenrolled_nodes\x18\x02 \x03(\v2\x14.sam.v1.EnrolledNodeR\renrolledNodes\x12A\n" + + "\x10bootstrap_tokens\x18\x03 \x03(\v2\x16.sam.v1.BootstrapTokenR\x0fbootstrapTokens\x12:\n" + + "\x0eactive_routers\x18\x04 \x03(\v2\x13.sam.v1.RouterLeaseR\ractiveRouters\x12,\n" + + "\x06policy\x18\x05 \x01(\v2\x14.sam.v1.PolicyConfigR\x06policy\"\xbd\x02\n" + "\x18IdentityEvidenceResponse\x12\x17\n" + "\apeer_id\x18\x01 \x01(\tR\x06peerId\x12\x18\n" + "\abiscuit\x18\x02 \x01(\fR\abiscuit\x12J\n" + @@ -4641,7 +5746,7 @@ func file_api_sam_proto_rawDescGZIP() []byte { } var file_api_sam_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_api_sam_proto_msgTypes = make([]protoimpl.MessageInfo, 58) +var file_api_sam_proto_msgTypes = make([]protoimpl.MessageInfo, 73) var file_api_sam_proto_goTypes = []any{ (EnrollmentStatus)(0), // 0: sam.v1.EnrollmentStatus (ServiceType)(0), // 1: sam.v1.ServiceType @@ -4690,43 +5795,58 @@ var file_api_sam_proto_goTypes = []any{ (*NodeCatalogReport)(nil), // 44: sam.v1.NodeCatalogReport (*TokenRevokeRequest)(nil), // 45: sam.v1.TokenRevokeRequest (*TokenRevokeResponse)(nil), // 46: sam.v1.TokenRevokeResponse - (*IdentityEvidenceResponse)(nil), // 47: sam.v1.IdentityEvidenceResponse - (*PeerEvidenceResponse)(nil), // 48: sam.v1.PeerEvidenceResponse - (*MemberCredential)(nil), // 49: sam.v1.MemberCredential - (*TrustedSigningKey)(nil), // 50: sam.v1.TrustedSigningKey - (*OIDCSession)(nil), // 51: sam.v1.OIDCSession - (*TaskAuthorizationRule)(nil), // 52: sam.v1.TaskAuthorizationRule - (*TaskRule)(nil), // 53: sam.v1.TaskRule - (*TaskOperation)(nil), // 54: sam.v1.TaskOperation - (*TokenExchangeRequest)(nil), // 55: sam.v1.TokenExchangeRequest - (*TokenExchangeResponse)(nil), // 56: sam.v1.TokenExchangeResponse - (*STSTokenRequest)(nil), // 57: sam.v1.STSTokenRequest - (*STSTokenResponse)(nil), // 58: sam.v1.STSTokenResponse - (*RevocationsResponse)(nil), // 59: sam.v1.RevocationsResponse - nil, // 60: sam.v1.EnrollRequest.LabelsEntry - nil, // 61: sam.v1.BootstrapEnrollRequest.LabelsEntry - nil, // 62: sam.v1.CommandBackend.EnvEntry - nil, // 63: sam.v1.ServiceAnnounce.LabelsEntry - nil, // 64: sam.v1.PeerEvidenceResponse.LabelsEntry - (*timestamppb.Timestamp)(nil), // 65: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 66: google.protobuf.Duration + (*BootstrapTokenCreateRequest)(nil), // 47: sam.v1.BootstrapTokenCreateRequest + (*BootstrapTokenCreateResponse)(nil), // 48: sam.v1.BootstrapTokenCreateResponse + (*BootstrapToken)(nil), // 49: sam.v1.BootstrapToken + (*BootstrapTokenListResponse)(nil), // 50: sam.v1.BootstrapTokenListResponse + (*EnrollmentRequest)(nil), // 51: sam.v1.EnrollmentRequest + (*EnrollmentRequestListResponse)(nil), // 52: sam.v1.EnrollmentRequestListResponse + (*User)(nil), // 53: sam.v1.User + (*EnrolledNode)(nil), // 54: sam.v1.EnrolledNode + (*RouterLease)(nil), // 55: sam.v1.RouterLease + (*NodeServices)(nil), // 56: sam.v1.NodeServices + (*AdminStatusResponse)(nil), // 57: sam.v1.AdminStatusResponse + (*UserStatusResponse)(nil), // 58: sam.v1.UserStatusResponse + (*IdentityEvidenceResponse)(nil), // 59: sam.v1.IdentityEvidenceResponse + (*PeerEvidenceResponse)(nil), // 60: sam.v1.PeerEvidenceResponse + (*MemberCredential)(nil), // 61: sam.v1.MemberCredential + (*TrustedSigningKey)(nil), // 62: sam.v1.TrustedSigningKey + (*OIDCSession)(nil), // 63: sam.v1.OIDCSession + (*TaskAuthorizationRule)(nil), // 64: sam.v1.TaskAuthorizationRule + (*TaskRule)(nil), // 65: sam.v1.TaskRule + (*TaskOperation)(nil), // 66: sam.v1.TaskOperation + (*TokenExchangeRequest)(nil), // 67: sam.v1.TokenExchangeRequest + (*TokenExchangeResponse)(nil), // 68: sam.v1.TokenExchangeResponse + (*STSTokenRequest)(nil), // 69: sam.v1.STSTokenRequest + (*STSTokenResponse)(nil), // 70: sam.v1.STSTokenResponse + (*RevocationsResponse)(nil), // 71: sam.v1.RevocationsResponse + nil, // 72: sam.v1.EnrollRequest.LabelsEntry + nil, // 73: sam.v1.BootstrapEnrollRequest.LabelsEntry + nil, // 74: sam.v1.CommandBackend.EnvEntry + nil, // 75: sam.v1.ServiceAnnounce.LabelsEntry + nil, // 76: sam.v1.EnrollmentRequest.LabelsEntry + nil, // 77: sam.v1.EnrolledNode.LabelsEntry + nil, // 78: sam.v1.AdminStatusResponse.NodeCatalogEntry + nil, // 79: sam.v1.PeerEvidenceResponse.LabelsEntry + (*timestamppb.Timestamp)(nil), // 80: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 81: google.protobuf.Duration } var file_api_sam_proto_depIdxs = []int32{ 4, // 0: sam.v1.MeshEvent.type:type_name -> sam.v1.MeshEvent.Type - 65, // 1: sam.v1.MeshEvent.event_time:type_name -> google.protobuf.Timestamp - 60, // 2: sam.v1.EnrollRequest.labels:type_name -> sam.v1.EnrollRequest.LabelsEntry - 65, // 3: sam.v1.EnrollResponse.expire_time:type_name -> google.protobuf.Timestamp - 61, // 4: sam.v1.BootstrapEnrollRequest.labels:type_name -> sam.v1.BootstrapEnrollRequest.LabelsEntry + 80, // 1: sam.v1.MeshEvent.event_time:type_name -> google.protobuf.Timestamp + 72, // 2: sam.v1.EnrollRequest.labels:type_name -> sam.v1.EnrollRequest.LabelsEntry + 80, // 3: sam.v1.EnrollResponse.expire_time:type_name -> google.protobuf.Timestamp + 73, // 4: sam.v1.BootstrapEnrollRequest.labels:type_name -> sam.v1.BootstrapEnrollRequest.LabelsEntry 0, // 5: sam.v1.BootstrapEnrollResponse.status:type_name -> sam.v1.EnrollmentStatus - 65, // 6: sam.v1.BootstrapEnrollResponse.expire_time:type_name -> google.protobuf.Timestamp + 80, // 6: sam.v1.BootstrapEnrollResponse.expire_time:type_name -> google.protobuf.Timestamp 1, // 7: sam.v1.ServiceInfo.type:type_name -> sam.v1.ServiceType - 62, // 8: sam.v1.CommandBackend.env:type_name -> sam.v1.CommandBackend.EnvEntry + 74, // 8: sam.v1.CommandBackend.env:type_name -> sam.v1.CommandBackend.EnvEntry 14, // 9: sam.v1.RegisterServiceRequest.service:type_name -> sam.v1.ServiceInfo 15, // 10: sam.v1.RegisterServiceRequest.command:type_name -> sam.v1.CommandBackend 1, // 11: sam.v1.ServiceAnnounce.type:type_name -> sam.v1.ServiceType - 63, // 12: sam.v1.ServiceAnnounce.labels:type_name -> sam.v1.ServiceAnnounce.LabelsEntry - 65, // 13: sam.v1.ServiceAnnounce.announce_time:type_name -> google.protobuf.Timestamp - 65, // 14: sam.v1.RouterLeaseResponse.expire_time:type_name -> google.protobuf.Timestamp + 75, // 12: sam.v1.ServiceAnnounce.labels:type_name -> sam.v1.ServiceAnnounce.LabelsEntry + 80, // 13: sam.v1.ServiceAnnounce.announce_time:type_name -> google.protobuf.Timestamp + 80, // 14: sam.v1.RouterLeaseResponse.expire_time:type_name -> google.protobuf.Timestamp 23, // 15: sam.v1.PolicyRole.http:type_name -> sam.v1.HTTPGrant 30, // 16: sam.v1.EgressDestination.broker:type_name -> sam.v1.CredentialBroker 25, // 17: sam.v1.EgressDestination.inspection:type_name -> sam.v1.Inspection @@ -4735,9 +5855,9 @@ var file_api_sam_proto_depIdxs = []int32{ 27, // 20: sam.v1.Inspector.model_armor:type_name -> sam.v1.ModelArmor 28, // 21: sam.v1.Inspector.ext_proc:type_name -> sam.v1.ExtProc 3, // 22: sam.v1.ModelArmor.response:type_name -> sam.v1.ResponseInspection - 66, // 23: sam.v1.ModelArmor.timeout:type_name -> google.protobuf.Duration + 81, // 23: sam.v1.ModelArmor.timeout:type_name -> google.protobuf.Duration 29, // 24: sam.v1.ExtProc.processing_mode:type_name -> sam.v1.ExtProcProcessingMode - 66, // 25: sam.v1.ExtProc.message_timeout:type_name -> google.protobuf.Duration + 81, // 25: sam.v1.ExtProc.message_timeout:type_name -> google.protobuf.Duration 5, // 26: sam.v1.ExtProcProcessingMode.request_header_mode:type_name -> sam.v1.ExtProcProcessingMode.HeaderMode 5, // 27: sam.v1.ExtProcProcessingMode.response_header_mode:type_name -> sam.v1.ExtProcProcessingMode.HeaderMode 6, // 28: sam.v1.ExtProcProcessingMode.request_body_mode:type_name -> sam.v1.ExtProcProcessingMode.BodyMode @@ -4751,29 +5871,60 @@ var file_api_sam_proto_depIdxs = []int32{ 34, // 36: sam.v1.PolicyConfig.bindings:type_name -> sam.v1.PolicyBinding 24, // 37: sam.v1.PolicyConfig.egress:type_name -> sam.v1.EgressDestination 24, // 38: sam.v1.EgressAssignmentsResponse.egress:type_name -> sam.v1.EgressDestination - 65, // 39: sam.v1.KeysResponse.sign_time:type_name -> google.protobuf.Timestamp - 65, // 40: sam.v1.TokenRefreshResponse.expire_time:type_name -> google.protobuf.Timestamp + 80, // 39: sam.v1.KeysResponse.sign_time:type_name -> google.protobuf.Timestamp + 80, // 40: sam.v1.TokenRefreshResponse.expire_time:type_name -> google.protobuf.Timestamp 14, // 41: sam.v1.NodeCatalogReport.services:type_name -> sam.v1.ServiceInfo - 65, // 42: sam.v1.IdentityEvidenceResponse.biscuit_expire_time:type_name -> google.protobuf.Timestamp - 65, // 43: sam.v1.IdentityEvidenceResponse.check_time:type_name -> google.protobuf.Timestamp - 64, // 44: sam.v1.PeerEvidenceResponse.labels:type_name -> sam.v1.PeerEvidenceResponse.LabelsEntry - 65, // 45: sam.v1.PeerEvidenceResponse.expire_time:type_name -> google.protobuf.Timestamp - 65, // 46: sam.v1.PeerEvidenceResponse.check_time:type_name -> google.protobuf.Timestamp - 65, // 47: sam.v1.MemberCredential.expire_time:type_name -> google.protobuf.Timestamp - 50, // 48: sam.v1.MemberCredential.trusted_keys:type_name -> sam.v1.TrustedSigningKey - 51, // 49: sam.v1.MemberCredential.oidc_session:type_name -> sam.v1.OIDCSession - 65, // 50: sam.v1.TrustedSigningKey.receive_time:type_name -> google.protobuf.Timestamp - 53, // 51: sam.v1.TaskAuthorizationRule.rules:type_name -> sam.v1.TaskRule - 65, // 52: sam.v1.TaskAuthorizationRule.expire_time:type_name -> google.protobuf.Timestamp - 54, // 53: sam.v1.TaskRule.operation:type_name -> sam.v1.TaskOperation - 52, // 54: sam.v1.TokenExchangeRequest.task_rule:type_name -> sam.v1.TaskAuthorizationRule - 65, // 55: sam.v1.TokenExchangeResponse.expire_time:type_name -> google.protobuf.Timestamp - 65, // 56: sam.v1.STSTokenResponse.expire_time:type_name -> google.protobuf.Timestamp - 57, // [57:57] is the sub-list for method output_type - 57, // [57:57] is the sub-list for method input_type - 57, // [57:57] is the sub-list for extension type_name - 57, // [57:57] is the sub-list for extension extendee - 0, // [0:57] is the sub-list for field type_name + 80, // 42: sam.v1.BootstrapTokenCreateResponse.expire_time:type_name -> google.protobuf.Timestamp + 80, // 43: sam.v1.BootstrapToken.create_time:type_name -> google.protobuf.Timestamp + 80, // 44: sam.v1.BootstrapToken.expire_time:type_name -> google.protobuf.Timestamp + 80, // 45: sam.v1.BootstrapToken.revoke_time:type_name -> google.protobuf.Timestamp + 49, // 46: sam.v1.BootstrapTokenListResponse.tokens:type_name -> sam.v1.BootstrapToken + 0, // 47: sam.v1.EnrollmentRequest.status:type_name -> sam.v1.EnrollmentStatus + 76, // 48: sam.v1.EnrollmentRequest.labels:type_name -> sam.v1.EnrollmentRequest.LabelsEntry + 80, // 49: sam.v1.EnrollmentRequest.create_time:type_name -> google.protobuf.Timestamp + 80, // 50: sam.v1.EnrollmentRequest.resolve_time:type_name -> google.protobuf.Timestamp + 51, // 51: sam.v1.EnrollmentRequestListResponse.requests:type_name -> sam.v1.EnrollmentRequest + 80, // 52: sam.v1.User.create_time:type_name -> google.protobuf.Timestamp + 77, // 53: sam.v1.EnrolledNode.labels:type_name -> sam.v1.EnrolledNode.LabelsEntry + 80, // 54: sam.v1.EnrolledNode.enroll_time:type_name -> google.protobuf.Timestamp + 80, // 55: sam.v1.EnrolledNode.expire_time:type_name -> google.protobuf.Timestamp + 80, // 56: sam.v1.RouterLease.last_renewal_time:type_name -> google.protobuf.Timestamp + 80, // 57: sam.v1.RouterLease.expire_time:type_name -> google.protobuf.Timestamp + 14, // 58: sam.v1.NodeServices.services:type_name -> sam.v1.ServiceInfo + 80, // 59: sam.v1.NodeServices.report_time:type_name -> google.protobuf.Timestamp + 53, // 60: sam.v1.AdminStatusResponse.users:type_name -> sam.v1.User + 55, // 61: sam.v1.AdminStatusResponse.active_routers:type_name -> sam.v1.RouterLease + 54, // 62: sam.v1.AdminStatusResponse.enrolled_nodes:type_name -> sam.v1.EnrolledNode + 51, // 63: sam.v1.AdminStatusResponse.enrollment_requests:type_name -> sam.v1.EnrollmentRequest + 49, // 64: sam.v1.AdminStatusResponse.bootstrap_tokens:type_name -> sam.v1.BootstrapToken + 35, // 65: sam.v1.AdminStatusResponse.policy:type_name -> sam.v1.PolicyConfig + 78, // 66: sam.v1.AdminStatusResponse.node_catalog:type_name -> sam.v1.AdminStatusResponse.NodeCatalogEntry + 53, // 67: sam.v1.UserStatusResponse.user:type_name -> sam.v1.User + 54, // 68: sam.v1.UserStatusResponse.enrolled_nodes:type_name -> sam.v1.EnrolledNode + 49, // 69: sam.v1.UserStatusResponse.bootstrap_tokens:type_name -> sam.v1.BootstrapToken + 55, // 70: sam.v1.UserStatusResponse.active_routers:type_name -> sam.v1.RouterLease + 35, // 71: sam.v1.UserStatusResponse.policy:type_name -> sam.v1.PolicyConfig + 80, // 72: sam.v1.IdentityEvidenceResponse.biscuit_expire_time:type_name -> google.protobuf.Timestamp + 80, // 73: sam.v1.IdentityEvidenceResponse.check_time:type_name -> google.protobuf.Timestamp + 79, // 74: sam.v1.PeerEvidenceResponse.labels:type_name -> sam.v1.PeerEvidenceResponse.LabelsEntry + 80, // 75: sam.v1.PeerEvidenceResponse.expire_time:type_name -> google.protobuf.Timestamp + 80, // 76: sam.v1.PeerEvidenceResponse.check_time:type_name -> google.protobuf.Timestamp + 80, // 77: sam.v1.MemberCredential.expire_time:type_name -> google.protobuf.Timestamp + 62, // 78: sam.v1.MemberCredential.trusted_keys:type_name -> sam.v1.TrustedSigningKey + 63, // 79: sam.v1.MemberCredential.oidc_session:type_name -> sam.v1.OIDCSession + 80, // 80: sam.v1.TrustedSigningKey.receive_time:type_name -> google.protobuf.Timestamp + 65, // 81: sam.v1.TaskAuthorizationRule.rules:type_name -> sam.v1.TaskRule + 80, // 82: sam.v1.TaskAuthorizationRule.expire_time:type_name -> google.protobuf.Timestamp + 66, // 83: sam.v1.TaskRule.operation:type_name -> sam.v1.TaskOperation + 64, // 84: sam.v1.TokenExchangeRequest.task_rule:type_name -> sam.v1.TaskAuthorizationRule + 80, // 85: sam.v1.TokenExchangeResponse.expire_time:type_name -> google.protobuf.Timestamp + 80, // 86: sam.v1.STSTokenResponse.expire_time:type_name -> google.protobuf.Timestamp + 56, // 87: sam.v1.AdminStatusResponse.NodeCatalogEntry.value:type_name -> sam.v1.NodeServices + 88, // [88:88] is the sub-list for method output_type + 88, // [88:88] is the sub-list for method input_type + 88, // [88:88] is the sub-list for extension type_name + 88, // [88:88] is the sub-list for extension extendee + 0, // [0:88] is the sub-list for field type_name } func init() { file_api_sam_proto_init() } @@ -4801,7 +5952,7 @@ func file_api_sam_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_sam_proto_rawDesc), len(file_api_sam_proto_rawDesc)), NumEnums: 7, - NumMessages: 58, + NumMessages: 73, NumExtensions: 0, NumServices: 0, }, diff --git a/api/sam.proto b/api/sam.proto index db1ff822..cc9d4ed1 100644 --- a/api/sam.proto +++ b/api/sam.proto @@ -504,6 +504,160 @@ message TokenRevokeResponse { string error = 2; } +// ============================================================================ +// Operator plane +// ============================================================================ +// +// What the console and admin CLIs exchange with the control plane on +// /admin/* and /user/*: protojson of these messages with proto field names, +// unknown fields rejected. Credentials and key material never appear here: +// an enrollment request's public key and minted biscuit, an enrolled node's +// biscuit and a token's hash stay in the store. + +// BootstrapTokenCreateRequest is the body of POST /admin/bootstrap-tokens +// (admin bearer) and POST /user/bootstrap-tokens (mesh user). +message BootstrapTokenCreateRequest { + // Role the token enrolls into, e.g. "sam:role:node". Required on the admin + // endpoint; the user endpoint defaults it to "sam:role:node". + string role = 1; + // User the token is issued on behalf of. Honored by the user endpoint + // only, and only for admins; defaults to the caller. + string owner_id = 2; + // How long the token stays valid; unset or non-positive means 24. + int32 ttl_hours = 3; + // How many enrollments the token admits; unset or non-positive means 1. + int32 max_usages = 4; + // Free-form operator note stored with the token. + string description = 5; + // Copied onto every node the token enrolls: such a node may still refresh + // its credential after the control plane's signing key rotated past its + // grace period, on proof of possession of its own key alone. Admin-only, + // because a node that can always recover holds a credential that never + // expires. + bool autonomous_recovery = 6; +} + +// BootstrapTokenCreateResponse is returned (201) when a token is minted. +// token is the plaintext and is shown exactly once; the control plane keeps +// only its hash, which is also the id. +message BootstrapTokenCreateResponse { + string id = 1; + string token = 2; + string role = 3; + string owner_id = 4; + google.protobuf.Timestamp expire_time = 5; +} + +// BootstrapToken is a minted token as operators list it. +message BootstrapToken { + string id = 1; + string role = 2; + string owner_id = 3; + int32 max_usages = 4; + int32 usages_count = 5; + string description = 6; + google.protobuf.Timestamp create_time = 7; + google.protobuf.Timestamp expire_time = 8; + // Set when an operator revoked the token, which is distinct from expiry + // or exhausted usages. Unset means never revoked. + google.protobuf.Timestamp revoke_time = 9; + bool autonomous_recovery = 10; +} + +message BootstrapTokenListResponse { + repeated BootstrapToken tokens = 1; +} + +// EnrollmentRequest is a bootstrap enrollment awaiting or past an operator +// decision (see BootstrapEnrollRequest). +message EnrollmentRequest { + string id = 1; + string peer_id = 2; + // The bootstrap token the request was made with. + string token_id = 3; + EnrollmentStatus status = 4; + // Labels the node declared; approval attests them into its biscuit. + map labels = 5; + google.protobuf.Timestamp create_time = 6; + // Unset while the request is pending. + google.protobuf.Timestamp resolve_time = 7; + string resolved_by = 8; +} + +message EnrollmentRequestListResponse { + repeated EnrollmentRequest requests = 1; +} + +// User is a human identity known to the mesh. +message User { + // The identity provider's subject. + string id = 1; + string issuer = 2; + string email = 3; + // "admin" or "user". + string role = 4; + google.protobuf.Timestamp create_time = 5; +} + +// EnrolledNode is a member's enrollment record without its credential. +message EnrolledNode { + string peer_id = 1; + string role = 2; + // How the node enrolled, e.g. "oidc" or "bootstrap". + string enrollment_type = 3; + // The identity provider's claims as stored at enrollment. Admin-only. + string claims_json = 4; + string owner_id = 5; + map labels = 6; + google.protobuf.Timestamp enroll_time = 7; + // When the enrollment session ends; unset means it does not expire. + google.protobuf.Timestamp expire_time = 8; + bool banned = 9; + bool autonomous_recovery = 10; +} + +// RouterLease is a router's current registration with the control plane. +message RouterLease { + string peer_id = 1; + // Multiaddrs, `/p2p/` suffixed. + repeated string addresses = 2; + google.protobuf.Timestamp last_renewal_time = 3; + google.protobuf.Timestamp expire_time = 4; + repeated string connected_peers = 5; + int32 dht_size = 6; +} + +// NodeServices is the services one node last reported (see +// NodeCatalogReport). +message NodeServices { + repeated ServiceInfo services = 1; + google.protobuf.Timestamp report_time = 2; +} + +// AdminStatusResponse answers GET /admin/status: everything the console +// shows an administrator. +message AdminStatusResponse { + repeated User users = 1; + repeated RouterLease active_routers = 2; + repeated EnrolledNode enrolled_nodes = 3; + repeated EnrollmentRequest enrollment_requests = 4; + repeated BootstrapToken bootstrap_tokens = 5; + PolicyConfig policy = 6; + // Keyed by the reporting node's peer ID; admitted nodes only. + map node_catalog = 7; +} + +// UserStatusResponse answers GET /user/status: the caller and what it owns. +// The router fleet and the mesh policy describe the whole mesh and are set +// for an administrator only. +message UserStatusResponse { + User user = 1; + repeated EnrolledNode enrolled_nodes = 2; + repeated BootstrapToken bootstrap_tokens = 3; + repeated RouterLease active_routers = 4; + PolicyConfig policy = 5; +} + // ============================================================================ // Identity Evidence API // ============================================================================ diff --git a/cmd/sam-one/admin.go b/cmd/sam-one/admin.go index 87714b05..d859d22b 100644 --- a/cmd/sam-one/admin.go +++ b/cmd/sam-one/admin.go @@ -16,7 +16,6 @@ package main import ( "bytes" - "encoding/json" "fmt" "io" "net/http" @@ -27,8 +26,8 @@ import ( "github.com/google/sam/api" "github.com/google/sam/internal/standalone" - "github.com/google/sam/internal/storage" "github.com/spf13/cobra" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -85,11 +84,11 @@ func (c *adminClient) do(method, path, contentType string, body []byte) ([]byte, // control plane's signing key rotated past its grace period, which is what // a device that spends days offline needs and what a stolen device should // not get. -func (c *adminClient) createToken(role string, ttlHours, maxUsages int, description string, autonomousRecovery bool) (*api.BootstrapTokenResponse, error) { - payload, err := json.Marshal(api.BootstrapTokenRequest{ +func (c *adminClient) createToken(role string, ttlHours, maxUsages int, description string, autonomousRecovery bool) (*api.BootstrapTokenCreateResponse, error) { + payload, err := protojson.Marshal(&api.BootstrapTokenCreateRequest{ Role: role, - TTLHours: ttlHours, - MaxUsages: maxUsages, + TtlHours: int32(ttlHours), + MaxUsages: int32(maxUsages), Description: description, AutonomousRecovery: autonomousRecovery, }) @@ -100,23 +99,23 @@ func (c *adminClient) createToken(role string, ttlHours, maxUsages int, descript if err != nil { return nil, err } - var created api.BootstrapTokenResponse - if err := json.Unmarshal(body, &created); err != nil { + created := &api.BootstrapTokenCreateResponse{} + if err := protojson.Unmarshal(body, created); err != nil { return nil, fmt.Errorf("failed to decode response %q: %w", body, err) } - return &created, nil + return created, nil } -func (c *adminClient) listTokens() ([]storage.BootstrapToken, error) { +func (c *adminClient) listTokens() ([]*api.BootstrapToken, error) { body, err := c.do(http.MethodGet, "/admin/bootstrap-tokens", "", nil) if err != nil { return nil, err } - var list []storage.BootstrapToken - if err := json.Unmarshal(body, &list); err != nil { + list := &api.BootstrapTokenListResponse{} + if err := protojson.Unmarshal(body, list); err != nil { return nil, fmt.Errorf("failed to decode response %q: %w", body, err) } - return list, nil + return list.GetTokens(), nil } func (c *adminClient) banPeer(peerID string) error { @@ -149,8 +148,8 @@ func (c *adminClient) resolveTokenID(idOrPrefix string) (string, error) { } var matches []string for _, tok := range list { - if strings.HasPrefix(tok.ID, idOrPrefix) { - matches = append(matches, tok.ID) + if strings.HasPrefix(tok.GetId(), idOrPrefix) { + matches = append(matches, tok.GetId()) } } switch len(matches) { @@ -211,9 +210,9 @@ func newAdminSubcommands() []*cobra.Command { if err != nil { return err } - cmd.Printf("Token: %s\n", created.Token) - cmd.Printf("Role: %s\n", created.Role) - cmd.Printf("Expires: %s\n", created.ExpiresAt) + cmd.Printf("Token: %s\n", created.GetToken()) + cmd.Printf("Role: %s\n", created.GetRole()) + cmd.Printf("Expires: %s\n", created.GetExpireTime().AsTime().Format(time.RFC3339)) cmd.PrintErrln("The plain token is shown only once; store it now.") return nil }, @@ -240,13 +239,13 @@ func newAdminSubcommands() []*cobra.Command { tw := tabwriter.NewWriter(cmd.OutOrStdout(), 2, 4, 2, ' ', 0) _, _ = fmt.Fprintln(tw, "ID\tROLE\tUSAGES\tSTATUS\tEXPIRES\tDESCRIPTION") for _, tok := range list { - id := tok.ID + id := tok.GetId() if len(id) > 12 { id = id[:12] } _, _ = fmt.Fprintf(tw, "%s\t%s\t%d/%d\t%s\t%s\t%s\n", - id, tok.Role, tok.UsagesCount, tok.MaxUsages, tokenStatus(&tok, now), - tok.ExpiresAt.Format(time.RFC3339), tok.Description) + id, tok.GetRole(), tok.GetUsagesCount(), tok.GetMaxUsages(), tokenStatus(tok, now), + tok.GetExpireTime().AsTime().Format(time.RFC3339), tok.GetDescription()) } return tw.Flush() }, @@ -303,13 +302,13 @@ func newAdminSubcommands() []*cobra.Command { // tokenStatus mirrors the control plane's usability check (/enroll and // ConsumeBootstrapTokenUsage) for display. -func tokenStatus(tok *storage.BootstrapToken, now time.Time) string { +func tokenStatus(tok *api.BootstrapToken, now time.Time) string { switch { - case tok.IsRevoked(): + case tok.GetRevokeTime() != nil: return "revoked" - case !tok.ExpiresAt.IsZero() && now.After(tok.ExpiresAt): + case tok.GetExpireTime() != nil && now.After(tok.GetExpireTime().AsTime()): return "expired" - case tok.UsagesCount >= tok.MaxUsages: + case tok.GetUsagesCount() >= tok.GetMaxUsages(): return "exhausted" default: return "active" diff --git a/cmd/sam-one/admin_test.go b/cmd/sam-one/admin_test.go index 09229a51..d4a3b9d1 100644 --- a/cmd/sam-one/admin_test.go +++ b/cmd/sam-one/admin_test.go @@ -15,7 +15,6 @@ package main import ( - "encoding/json" "fmt" "io" "net/http" @@ -27,8 +26,9 @@ import ( "time" "github.com/google/sam/api" - "github.com/google/sam/internal/storage" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) func newFakeAdminAPI(t *testing.T) *httptest.Server { @@ -43,31 +43,34 @@ func newFakeAdminAPI(t *testing.T) *httptest.Server { case http.MethodPost: // Decode into the shared wire type, as the control plane does, so a // client that drifts from it fails here. - var req api.BootstrapTokenRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + body, _ := io.ReadAll(r.Body) + req := &api.BootstrapTokenCreateRequest{} + if err := protojson.Unmarshal(body, req); err != nil { http.Error(w, "bad body", http.StatusBadRequest) return } - if !req.AutonomousRecovery || req.MaxUsages != 1 || req.TTLHours != 24 || req.Description != "note" { + if !req.GetAutonomousRecovery() || req.GetMaxUsages() != 1 || req.GetTtlHours() != 24 || req.GetDescription() != "note" { http.Error(w, fmt.Sprintf("unexpected request %+v", req), http.StatusBadRequest) return } w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(api.BootstrapTokenResponse{ - ID: "abcdef123456", - Token: "sam-bt-fresh", - Role: req.Role, - ExpiresAt: "2026-12-31T00:00:00Z", + out, _ := protojson.MarshalOptions{UseProtoNames: true}.Marshal(&api.BootstrapTokenCreateResponse{ + Id: "abcdef123456", + Token: "sam-bt-fresh", + Role: req.GetRole(), + ExpireTime: timestamppb.New(time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC)), }) + _, _ = w.Write(out) case http.MethodGet: - _ = json.NewEncoder(w).Encode([]storage.BootstrapToken{{ - ID: "abcdef123456", + out, _ := protojson.MarshalOptions{UseProtoNames: true}.Marshal(&api.BootstrapTokenListResponse{Tokens: []*api.BootstrapToken{{ + Id: "abcdef123456", Role: api.RoleNode, MaxUsages: 3, UsagesCount: 1, Description: "seeded", - ExpiresAt: time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC), - }}) + ExpireTime: timestamppb.New(time.Date(2026, 12, 31, 0, 0, 0, 0, time.UTC)), + }}}) + _, _ = w.Write(out) default: http.Error(w, "method", http.StatusMethodNotAllowed) } @@ -109,7 +112,7 @@ func TestAdminClient(t *testing.T) { if err != nil { t.Fatalf("createToken failed: %v", err) } - if created.Token != "sam-bt-fresh" || created.Role != api.RoleNode { + if created.GetToken() != "sam-bt-fresh" || created.GetRole() != api.RoleNode { t.Errorf("unexpected created token: %+v", created) } @@ -117,7 +120,7 @@ func TestAdminClient(t *testing.T) { if err != nil { t.Fatalf("listTokens failed: %v", err) } - if len(list) != 1 || list[0].Description != "seeded" || list[0].UsagesCount != 1 { + if len(list) != 1 || list[0].GetDescription() != "seeded" || list[0].GetUsagesCount() != 1 { t.Errorf("unexpected token list: %+v", list) } diff --git a/internal/console/public/app.js b/internal/console/public/app.js index 093569f0..84a1799d 100644 --- a/internal/console/public/app.js +++ b/internal/console/public/app.js @@ -221,7 +221,7 @@ async function loadData() { // A revoked node is banned, not deleted: the record survives for unban // and audit, but it is out of the mesh, so no view may list it. - const enrolledNodes = (data.enrolled_nodes || []).filter(node => !node.Banned); + const enrolledNodes = (data.enrolled_nodes || []).filter(node => !node.banned); // Update Stats const usersCount = (data.users && data.users.length) || 0; @@ -234,7 +234,7 @@ async function loadData() { document.getElementById('stat-routers').innerText = routersCount; // Count pending - const pendingCount = (data.enrollment_requests || []).filter(r => r.Status === 0 || r.Status === 'ENROLLMENT_STATUS_PENDING').length; + const pendingCount = (data.enrollment_requests || []).filter(r => r.status === 'ENROLLMENT_STATUS_PENDING').length; document.getElementById('stat-pending').innerText = pendingCount; // Render Tables & Grid @@ -254,8 +254,8 @@ async function loadData() { renderBootstrapTokensTable(data.bootstrap_tokens || []); const policyArea = document.getElementById('policy-yaml'); - if (policyArea && data.policy_json !== undefined && !policyIsDirty()) { - policyArea.value = renderPolicyYAML(data.policy_json); + if (policyArea && role === 'admin' && !policyIsDirty()) { + policyArea.value = renderPolicyYAML(data.policy || {}); policyBaseline = policyArea.value; validatePolicyEditor(); } @@ -329,10 +329,10 @@ function renderUsersTable(users) { tbody.innerHTML = users.map(user => ` - ${escapeHTML(user.ID)} - ${escapeHTML(user.Role)} - ${escapeHTML(user.Email)} - ${new Date(user.CreatedAt).toLocaleString()} + ${escapeHTML(user.id)} + ${escapeHTML(user.role)} + ${escapeHTML(user.email)} + ${user.create_time ? new Date(user.create_time).toLocaleString() : '-'} `).join(''); } @@ -365,7 +365,7 @@ function peerCell(peerID, labels) { function buildLabelsByPeer(nodes) { const byPeer = {}; for (const node of nodes || []) { - byPeer[node.PeerID] = node.Labels || {}; + byPeer[node.peer_id] = node.labels || {}; } return byPeer; } @@ -378,25 +378,25 @@ function renderNodesTable(nodes, role) { } tbody.innerHTML = nodes.map(node => { - const recovery = !!node.AutonomousRecovery; + const recovery = !!node.autonomous_recovery; const recoveryBadge = recovery ? `Autonomous` : `Manual`; // The flag decides whether a lost device can rejoin on its own key, so // only admins get the toggle; owners just see the state. const toggle = role === 'admin' - ? `` + ? `` : ''; return ` - ${peerCell(node.PeerID, node.Labels)} - ${escapeHTML(node.Role)} - ${escapeHTML(node.OwnerID)} + ${peerCell(node.peer_id, node.labels)} + ${escapeHTML(node.role)} + ${escapeHTML(node.owner_id || '')} ${recoveryBadge}
${toggle} - +
@@ -413,9 +413,13 @@ window.setAutonomousRecovery = function(peerID, enabled) { } }; -// node_catalog is {peerID: {services: [{name, type, description}], reported_at}}, +// node_catalog is {peerID: {services: [{name, type, description}], report_time}}, // already restricted server-side to nodes that are still admitted; type is -// the short name ("mcp", "inference", "a2a") rendered by the control plane. +// the ServiceType enum name ("SERVICE_TYPE_MCP"), shown as its short form. +function serviceTypeLabel(type) { + return String(type || '').replace(/^SERVICE_TYPE_/, '').toLowerCase() || 'unknown'; +} + function renderServicesTable(nodeCatalog, labelsByPeer) { const tbody = document.getElementById('table-services'); const peerIDs = Object.keys(nodeCatalog || {}); @@ -425,7 +429,7 @@ function renderServicesTable(nodeCatalog, labelsByPeer) { const services = entry.services || []; for (const svc of services) { if (svc) { - rows.push({ peerID, reportedAt: entry.reported_at, svc }); + rows.push({ peerID, reportedAt: entry.report_time, svc }); } } } @@ -438,7 +442,7 @@ function renderServicesTable(nodeCatalog, labelsByPeer) { tbody.innerHTML = rows.map(({ peerID, reportedAt, svc }) => ` ${escapeHTML(svc.name || '')} - ${escapeHTML(svc.type || 'unknown')} + ${escapeHTML(serviceTypeLabel(svc.type))} ${escapeHTML(svc.description || '')} ${peerCell(peerID, (labelsByPeer || {})[peerID])} ${reportedAt ? escapeHTML(new Date(reportedAt).toLocaleString()) : '-'} @@ -447,11 +451,11 @@ function renderServicesTable(nodeCatalog, labelsByPeer) { } function getStatusBadge(status) { - if (status === 0 || status === 'ENROLLMENT_STATUS_PENDING') { + if (status === 'ENROLLMENT_STATUS_PENDING') { return `Pending`; - } else if (status === 1 || status === 'ENROLLMENT_STATUS_APPROVED') { + } else if (status === 'ENROLLMENT_STATUS_APPROVED') { return `Approved`; - } else if (status === 2 || status === 'ENROLLMENT_STATUS_REJECTED') { + } else if (status === 'ENROLLMENT_STATUS_REJECTED') { return `Rejected`; } return `Unknown`; @@ -465,21 +469,21 @@ function renderEnrollmentsTable(reqs) { } tbody.innerHTML = reqs.map(req => { - const isPending = req.Status === 0 || req.Status === 'ENROLLMENT_STATUS_PENDING'; + const isPending = req.status === 'ENROLLMENT_STATUS_PENDING'; let actions = ''; if (isPending) { actions = `
- - + +
`; } return ` - ${escapeHTML(req.ID)} - ${getStatusBadge(req.Status)} - ${escapeHTML(req.CreatedAt)} + ${escapeHTML(req.id)} + ${getStatusBadge(req.status)} + ${escapeHTML(req.create_time || '')} ${actions} `; @@ -495,9 +499,9 @@ function renderRoutersTable(routers) { tbody.innerHTML = routers.map(router => ` - ${escapeHTML(router.PeerID)} - ${router.Addresses ? router.Addresses.map(addr => escapeHTML(addr)).join('
') : '-'} - ${escapeHTML(router.ExpiresAt)} + ${escapeHTML(router.peer_id)} + ${router.addresses ? router.addresses.map(addr => escapeHTML(addr)).join('
') : '-'} + ${escapeHTML(router.expire_time || '')} `).join(''); } @@ -630,15 +634,15 @@ function policyIsDirty() { // The control plane sends the policy as protojson. Showing it as YAML keeps the // document readable without either side hand-maintaining a second field list. -function renderPolicyYAML(policyJSON) { - if (!policyJSON) { +function renderPolicyYAML(policy) { + if (!policy) { return ''; } try { - return jsyaml.dump(JSON.parse(policyJSON), { indent: 2, lineWidth: -1, noRefs: true }); + return jsyaml.dump(policy, { indent: 2, lineWidth: -1, noRefs: true }); } catch (err) { // Better to show the operator the raw document than an empty editor. - return policyJSON; + return JSON.stringify(policy, null, 2); } } @@ -692,13 +696,13 @@ function renderRouterTopography(routers) { } topoList.innerHTML = routers.map(r => { - const conns = r.ConnectedPeers || []; - const dhtSize = r.DHTSize || 0; - const peerID = String(r.PeerID || ''); + const conns = r.connected_peers || []; + const dhtSize = r.dht_size || 0; + const peerID = String(r.peer_id || ''); let remaining = 0; - if (r.ExpiresAt) { - remaining = Math.max(0, Math.floor((new Date(r.ExpiresAt) - new Date()) / 1000)); + if (r.expire_time) { + remaining = Math.max(0, Math.floor((new Date(r.expire_time) - new Date()) / 1000)); } const leaseClass = remaining === 0 ? 'badge-rejected' : 'badge-approved'; const leaseLabel = remaining === 0 ? 'Lease expired' : `Lease: ${formatDuration(remaining)}`; @@ -738,21 +742,21 @@ function renderBootstrapTokensTable(tokens) { } tbody.innerHTML = tokens.map(token => { - const expiresAt = token.ExpiresAt && !token.ExpiresAt.startsWith('0001') ? new Date(token.ExpiresAt).toLocaleString() : 'Never'; - const revoked = !!token.RevokedAt; + const expiresAt = token.expire_time ? new Date(token.expire_time).toLocaleString() : 'Never'; + const revoked = !!token.revoke_time; const status = revoked ? `Revoked` : `Active`; - const recovery = token.AutonomousRecovery ? 'Autonomous' : 'Manual'; + const recovery = token.autonomous_recovery ? 'Autonomous' : 'Manual'; const action = revoked ? '-' - : ``; + : ``; return ` - ${escapeHTML(String(token.ID || '').substring(0, 8))}... - ${escapeHTML(token.Role)} - ${escapeHTML(token.OwnerID || '-')} - ${escapeHTML(String(token.UsagesCount))} / ${escapeHTML(String(token.MaxUsages))} + ${escapeHTML(String(token.id || '').substring(0, 8))}... + ${escapeHTML(token.role)} + ${escapeHTML(token.owner_id || '-')} + ${escapeHTML(String(token.usages_count || 0))} / ${escapeHTML(String(token.max_usages || 0))} ${escapeHTML(expiresAt)} ${recovery} ${status} diff --git a/internal/controlplane/approval_labels_test.go b/internal/controlplane/approval_labels_test.go index 0c18a6ff..37f7ee65 100644 --- a/internal/controlplane/approval_labels_test.go +++ b/internal/controlplane/approval_labels_test.go @@ -24,9 +24,9 @@ import ( "time" "github.com/google/sam/api" - "github.com/google/sam/internal/storage" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -132,14 +132,15 @@ func TestApprovalRefusesLabelsTheRoleDoesNotGrant(t *testing.T) { if err != nil { t.Fatalf("GET /admin/enrollments: %v", err) } - var pending []storage.EnrollmentRequest - _ = json.NewDecoder(resp.Body).Decode(&pending) + listBody, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() + pending := &api.EnrollmentRequestListResponse{} + _ = protojson.Unmarshal(listBody, pending) var reqID string - for _, p := range pending { - if p.PeerID == pID.String() { - reqID = p.ID + for _, p := range pending.GetRequests() { + if p.GetPeerId() == pID.String() { + reqID = p.GetId() } } if reqID == "" { diff --git a/internal/controlplane/catalog.go b/internal/controlplane/catalog.go index 50ee2115..4886ca2a 100644 --- a/internal/controlplane/catalog.go +++ b/internal/controlplane/catalog.go @@ -24,6 +24,7 @@ import ( "github.com/google/sam/internal/storage" "github.com/libp2p/go-libp2p/core/peer" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) // maxCatalogServices bounds one report so a single admitted node cannot grow @@ -36,21 +37,6 @@ type nodeCatalogEntry struct { ReportedAt time.Time } -// catalogService is the console-facing shape of one reported service: a plain -// struct so the JSON the console reads does not depend on protoc-gen-go's -// struct layout or tags. -type catalogService struct { - Name string `json:"name"` - Type string `json:"type"` - Description string `json:"description"` -} - -// catalogView is HandleAdminStatus's node_catalog value for one peer. -type catalogView struct { - Services []catalogService `json:"services"` - ReportedAt time.Time `json:"reported_at"` -} - // catalogSnapshot returns a stable copy of the current node service catalog // cache, safe to range over without holding catalogMu. func (s *Server) catalogSnapshot() map[string]nodeCatalogEntry { @@ -66,9 +52,9 @@ func (s *Server) catalogSnapshot() map[string]nodeCatalogEntry { // catalogViewFor renders the cache for the console, restricted to nodes that // are still admitted so a banned or expired node's last report disappears // with its enrollment instead of lingering until the next restart. -func (s *Server) catalogViewFor(nodes []storage.EnrolledNode, now time.Time) map[string]catalogView { +func (s *Server) catalogViewFor(nodes []storage.EnrolledNode, now time.Time) map[string]*api.NodeServices { snap := s.catalogSnapshot() - view := make(map[string]catalogView, len(snap)) + view := make(map[string]*api.NodeServices, len(snap)) for i := range nodes { node := &nodes[i] // The cache is keyed by the canonical base58 form from the verified @@ -83,22 +69,14 @@ func (s *Server) catalogViewFor(nodes []storage.EnrolledNode, now time.Time) map if !ok || node.CheckAdmission(now) != nil { continue } - services := make([]catalogService, 0, len(entry.Services)) + services := make([]*api.ServiceInfo, 0, len(entry.Services)) for _, svc := range entry.Services { if svc == nil { continue } - typeName, err := api.ServiceTypeToString(svc.GetType()) - if err != nil { - typeName = "unknown" - } - services = append(services, catalogService{ - Name: svc.GetName(), - Type: typeName, - Description: svc.GetDescription(), - }) + services = append(services, svc) } - view[node.PeerID] = catalogView{Services: services, ReportedAt: entry.ReportedAt} + view[node.PeerID] = &api.NodeServices{Services: services, ReportTime: timestamppb.New(entry.ReportedAt)} } return view } diff --git a/internal/controlplane/catalog_test.go b/internal/controlplane/catalog_test.go index 4a1e2c73..c85054c7 100644 --- a/internal/controlplane/catalog_test.go +++ b/internal/controlplane/catalog_test.go @@ -19,7 +19,7 @@ import ( "context" "crypto/ed25519" "encoding/base64" - "encoding/json" + "io" "net/http" "strconv" "testing" @@ -29,6 +29,7 @@ import ( "github.com/google/sam/internal/identity" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -82,7 +83,7 @@ func catalogBody(t *testing.T, services ...*api.ServiceInfo) []byte { } // adminNodeCatalog fetches /admin/status and returns its node_catalog value. -func adminNodeCatalog(t *testing.T, cpURL, adminToken string) map[string]catalogView { +func adminNodeCatalog(t *testing.T, cpURL, adminToken string) map[string]*api.NodeServices { t.Helper() req, err := http.NewRequest(http.MethodGet, cpURL+"/admin/status", nil) @@ -98,13 +99,15 @@ func adminNodeCatalog(t *testing.T, cpURL, adminToken string) map[string]catalog if resp.StatusCode != http.StatusOK { t.Fatalf("GET /admin/status: status %d", resp.StatusCode) } - var status struct { - NodeCatalog map[string]catalogView `json:"node_catalog"` + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("read /admin/status: %v", err) } - if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { + status := &api.AdminStatusResponse{} + if err := protojson.Unmarshal(body, status); err != nil { t.Fatalf("decode /admin/status: %v", err) } - return status.NodeCatalog + return status.GetNodeCatalog() } func TestHandleNodeCatalog(t *testing.T) { @@ -151,14 +154,14 @@ func TestHandleNodeCatalog(t *testing.T) { t.Fatalf("second report must replace the first, got %+v", entry.Services) } - // The console sees the plain view with the type rendered as a name. + // The console sees the reported services with the report time. view := adminNodeCatalog(t, cpURL, srv.config.AdminToken) got, ok := view[nodePeer.String()] if !ok || len(view) != 1 { t.Fatalf("expected the reporting peer in node_catalog, got %v", view) } - want := []catalogService{{Name: "planner", Type: "a2a", Description: ""}} - if len(got.Services) != 1 || got.Services[0] != want[0] || got.ReportedAt.IsZero() { + want := &api.ServiceInfo{Type: api.ServiceType_SERVICE_TYPE_A2A, Name: "planner"} + if len(got.GetServices()) != 1 || !proto.Equal(got.GetServices()[0], want) || got.GetReportTime() == nil { t.Fatalf("node_catalog view = %+v, want services %+v", got, want) } @@ -166,7 +169,7 @@ func TestHandleNodeCatalog(t *testing.T) { if got := postCatalog(t, cpURL, bearer(biscuitBytes), priv, catalogBody(t)); got != http.StatusNoContent { t.Fatalf("empty report: got status %d, want %d", got, http.StatusNoContent) } - if view := adminNodeCatalog(t, cpURL, srv.config.AdminToken); len(view[nodePeer.String()].Services) != 0 { + if view := adminNodeCatalog(t, cpURL, srv.config.AdminToken); len(view[nodePeer.String()].GetServices()) != 0 { t.Fatalf("empty report must clear services, got %+v", view) } } @@ -357,7 +360,7 @@ func TestCatalogPeerIDCanonicalization(t *testing.T) { // The view must join the CIDv1 record with the canonically-keyed entry, // displayed under the record's own spelling. view := adminNodeCatalog(t, cpURL, srv.config.AdminToken) - if _, ok := view[cidForm]; !ok || len(view[cidForm].Services) != 1 { + if _, ok := view[cidForm]; !ok || len(view[cidForm].GetServices()) != 1 { t.Fatalf("CIDv1-enrolled node's report missing from node_catalog, got %v", view) } diff --git a/internal/controlplane/identity_lifecycle_test.go b/internal/controlplane/identity_lifecycle_test.go index fc31d03b..eb12d1ea 100644 --- a/internal/controlplane/identity_lifecycle_test.go +++ b/internal/controlplane/identity_lifecycle_test.go @@ -34,6 +34,7 @@ import ( "github.com/google/sam/internal/storage" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -446,33 +447,40 @@ func TestUserStatusTrimsCredentialsAndMeshState(t *testing.T) { if status != http.StatusOK { t.Fatalf("/user/status: %d %s", status, body) } - for _, leaked := range []string{`"Biscuit"`, `"PublicKey"`, `"ClaimsJSON"`, `"policy_json"`, `"active_routers"`} { + for _, leaked := range []string{`"biscuit"`, `"public_key"`, `"claims_json"`, `"policy"`, `"active_routers"`} { if bytes.Contains(body, []byte(leaked)) { t.Errorf("non-admin /user/status carries %s", leaked) } } - var resp struct { - Nodes []map[string]any `json:"enrolled_nodes"` - } - if err := json.Unmarshal(body, &resp); err != nil { + resp := &api.UserStatusResponse{} + if err := protojson.Unmarshal(body, resp); err != nil { t.Fatal(err) } - if len(resp.Nodes) != 0 { + if len(resp.GetEnrolledNodes()) != 0 { // The node was OIDC-enrolled with no owner; a user sees only nodes it // owns. What matters is the shape when present, checked as admin. - t.Errorf("non-owner sees %d nodes", len(resp.Nodes)) + t.Errorf("non-owner sees %d nodes", len(resp.GetEnrolledNodes())) } + // protojson omits an empty list, so give the admin view a router to show. + if err := h.store.UpsertRouterLease(context.Background(), &storage.RouterLease{ + PeerID: "12D3KooWStatusRouter000000000000000000000000000000", + Addresses: []string{"/dns4/router.example/tcp/4501"}, + LastRenewal: time.Now(), + ExpiresAt: time.Now().Add(time.Hour), + }); err != nil { + t.Fatal(err) + } status, body = h.do(http.MethodGet, "/user/status", "super-secret-admin-token", nil, "") if status != http.StatusOK { t.Fatalf("admin /user/status: %d %s", status, body) } - for _, leaked := range []string{`"Biscuit"`, `"PublicKey"`} { + for _, leaked := range []string{`"biscuit"`, `"public_key"`} { if bytes.Contains(body, []byte(leaked)) { t.Errorf("admin /user/status carries %s", leaked) } } - for _, wanted := range []string{`"policy_json"`, `"active_routers"`, `"ClaimsJSON"`, `"PeerID"`} { + for _, wanted := range []string{`"policy"`, `"active_routers"`, `"claims_json"`, `"peer_id"`} { if !bytes.Contains(body, []byte(wanted)) { t.Errorf("admin /user/status lacks %s", wanted) } diff --git a/internal/controlplane/server.go b/internal/controlplane/server.go index c59039e9..31b65b72 100644 --- a/internal/controlplane/server.go +++ b/internal/controlplane/server.go @@ -1716,24 +1716,22 @@ func (s *Server) HandleAdminPolicy(w http.ResponseWriter, r *http.Request) { if !s.checkAdminAuth(w, r) { return } - roles, bindings, err := s.store.GetMeshPolicy(r.Context()) - if err != nil && err != storage.ErrNotFound { + policy, err := s.loadPolicyConfig(r.Context()) + if err != nil { logger.Errorf("Failed to load policy: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - egress, err := s.store.GetEgressDestinations(r.Context()) - if err != nil && err != storage.ErrNotFound { - logger.Errorf("Failed to load egress destinations: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - writeProtoJSON(w, &api.PolicyConfig{Roles: roles, Bindings: bindings, Egress: egress}) + writeProtoJSON(w, policy) } // writeProtoJSON answers an operator-plane request with protojson of msg, // using the proto field names the console and the docs show. func writeProtoJSON(w http.ResponseWriter, msg proto.Message) { + writeProtoJSONStatus(w, http.StatusOK, msg) +} + +func writeProtoJSONStatus(w http.ResponseWriter, status int, msg proto.Message) { out, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(msg) if err != nil { logger.Errorf("Failed to render %T: %v", msg, err) @@ -1741,10 +1739,28 @@ func writeProtoJSON(w http.ResponseWriter, msg proto.Message) { return } w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) + w.WriteHeader(status) _, _ = w.Write(out) } +// readProtoJSON decodes an operator-plane request body into msg. Unknown +// fields are an error: a misspelled field would otherwise silently become +// the default. Writes the 400 itself and reports false on failure. +func readProtoJSON(w http.ResponseWriter, r *http.Request, msg proto.Message) bool { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodyBytes) + defer func() { _ = r.Body.Close() }() + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read body", http.StatusBadRequest) + return false + } + if err := protojson.Unmarshal(body, msg); err != nil { + http.Error(w, "Invalid JSON body: "+err.Error(), http.StatusBadRequest) + return false + } + return true +} + // Close shuts down background loops and HTTP server. func (s *Server) Close() error { s.shutdown = true @@ -2362,9 +2378,7 @@ func (s *Server) HandleAdminBootstrapTokens(w http.ResponseWriter, r *http.Reque http.Error(w, "Internal server error", http.StatusInternalServerError) return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(list) + writeProtoJSON(w, &api.BootstrapTokenListResponse{Tokens: bootstrapTokenViews(list)}) return } @@ -2373,24 +2387,21 @@ func (s *Server) HandleAdminBootstrapTokens(w http.ResponseWriter, r *http.Reque return } - var req api.BootstrapTokenRequest - r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodyBytes) - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid JSON body", http.StatusBadRequest) + req := &api.BootstrapTokenCreateRequest{} + if !readProtoJSON(w, r, req) { return } - defer func() { _ = r.Body.Close() }() - if req.Role == "" { + if req.GetRole() == "" { // No silent default: the old one was router, the most privileged // role a token can carry. http.Error(w, "role is required (e.g. \"sam:role:node\")", http.StatusBadRequest) return } - if req.TTLHours <= 0 { - req.TTLHours = 24 + if req.GetTtlHours() <= 0 { + req.TtlHours = 24 } - if req.MaxUsages <= 0 { + if req.GetMaxUsages() <= 0 { req.MaxUsages = 1 } @@ -2405,13 +2416,13 @@ func (s *Server) HandleAdminBootstrapTokens(w http.ResponseWriter, r *http.Reque tokenRecord := &storage.BootstrapToken{ ID: tokenID, TokenHash: tokenID, - Role: req.Role, - MaxUsages: req.MaxUsages, + Role: req.GetRole(), + MaxUsages: int(req.GetMaxUsages()), UsagesCount: 0, - Description: req.Description, + Description: req.GetDescription(), CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(time.Duration(req.TTLHours) * time.Hour), - AutonomousRecovery: req.AutonomousRecovery, + ExpiresAt: time.Now().Add(time.Duration(req.GetTtlHours()) * time.Hour), + AutonomousRecovery: req.GetAutonomousRecovery(), } if err := s.store.SaveBootstrapToken(r.Context(), tokenRecord); err != nil { @@ -2420,13 +2431,11 @@ func (s *Server) HandleAdminBootstrapTokens(w http.ResponseWriter, r *http.Reque return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(api.BootstrapTokenResponse{ - ID: tokenRecord.ID, - Token: tokenVal, - Role: tokenRecord.Role, - ExpiresAt: tokenRecord.ExpiresAt.Format(time.RFC3339), + writeProtoJSONStatus(w, http.StatusCreated, &api.BootstrapTokenCreateResponse{ + Id: tokenRecord.ID, + Token: tokenVal, + Role: tokenRecord.Role, + ExpireTime: timestamppb.New(tokenRecord.ExpiresAt), }) } @@ -2484,14 +2493,7 @@ func (s *Server) HandleAdminEnrollments(w http.ResponseWriter, r *http.Request) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - for i := range list { - list[i].BiscuitToken = nil - list[i].PublicKey = nil - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(list) + writeProtoJSON(w, &api.EnrollmentRequestListResponse{Requests: enrollmentRequestViews(list)}) } // HandleAdminEnrollmentAction HTTP POST `/admin/enrollments/{id}/approve` or `/admin/enrollments/{id}/reject` @@ -2969,10 +2971,10 @@ func (s *Server) HandleUserStatus(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal server error", http.StatusInternalServerError) return } - nodes := []enrolledNodeView{} - for _, n := range allNodes { - if isAdmin || n.OwnerID == user.ID { - nodes = append(nodes, viewEnrolledNode(n, isAdmin)) + nodes := []*api.EnrolledNode{} + for i := range allNodes { + if isAdmin || allNodes[i].OwnerID == user.ID { + nodes = append(nodes, enrolledNodeView(&allNodes[i], isAdmin)) } } @@ -2982,21 +2984,17 @@ func (s *Server) HandleUserStatus(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal server error", http.StatusInternalServerError) return } - tokens := []storage.BootstrapToken{} - for _, t := range allTokens { - if isAdmin || t.OwnerID == user.ID { - tokens = append(tokens, t) + tokens := []*api.BootstrapToken{} + for i := range allTokens { + if isAdmin || allTokens[i].OwnerID == user.ID { + tokens = append(tokens, bootstrapTokenView(&allTokens[i])) } } - resp := map[string]any{ - "user": map[string]any{ - "id": user.ID, - "email": user.Email, - "role": user.Role, - }, - "enrolled_nodes": nodes, - "bootstrap_tokens": tokens, + resp := &api.UserStatusResponse{ + User: userView(user), + EnrolledNodes: nodes, + BootstrapTokens: tokens, } // The mesh policy and the router fleet describe the whole mesh, not the @@ -3008,65 +3006,32 @@ func (s *Server) HandleUserStatus(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal server error", http.StatusInternalServerError) return } - resp["active_routers"] = routers + resp.ActiveRouters = routerLeaseViews(routers) - roles, bindings, err := s.store.GetMeshPolicy(ctx) - if err != nil && err != storage.ErrNotFound { - logger.Errorf("Failed to list policy: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - egress, err := s.store.GetEgressDestinations(ctx) - if err != nil && err != storage.ErrNotFound { - logger.Errorf("Failed to list egress destinations: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - rendered, err := marshalPolicyJSON(roles, bindings, egress) + policy, err := s.loadPolicyConfig(ctx) if err != nil { - logger.Errorf("Failed to render policy: %v", err) + logger.Errorf("Failed to list policy: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - resp["policy_json"] = rendered + resp.Policy = policy } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(resp) -} - -// enrolledNodeView is what status endpoints return for a node: the record -// minus its live credential and key material. ClaimsJSON is admin-only. -type enrolledNodeView struct { - PeerID string `json:"PeerID"` - Role string `json:"Role"` - EnrollmentType string `json:"EnrollmentType"` - ClaimsJSON string `json:"ClaimsJSON,omitempty"` - OwnerID string `json:"OwnerID"` - Labels map[string]string `json:"Labels"` - EnrolledAt time.Time `json:"EnrolledAt"` - ExpiresAt time.Time `json:"ExpiresAt"` - Banned bool `json:"Banned"` - AutonomousRecovery bool `json:"AutonomousRecovery"` -} - -func viewEnrolledNode(n storage.EnrolledNode, withClaims bool) enrolledNodeView { - v := enrolledNodeView{ - PeerID: n.PeerID, - Role: n.Role, - EnrollmentType: n.EnrollmentType, - OwnerID: n.OwnerID, - Labels: n.Labels, - EnrolledAt: n.EnrolledAt, - ExpiresAt: n.ExpiresAt, - Banned: n.Banned, - AutonomousRecovery: n.AutonomousRecovery, - } - if withClaims { - v.ClaimsJSON = n.ClaimsJSON - } - return v + writeProtoJSON(w, resp) +} + +// loadPolicyConfig reads the stored mesh policy as the message POST /policies +// accepts; an empty store yields an empty policy. +func (s *Server) loadPolicyConfig(ctx context.Context) (*api.PolicyConfig, error) { + roles, bindings, err := s.store.GetMeshPolicy(ctx) + if err != nil && err != storage.ErrNotFound { + return nil, err + } + egress, err := s.store.GetEgressDestinations(ctx) + if err != nil && err != storage.ErrNotFound { + return nil, err + } + return &api.PolicyConfig{Roles: roles, Bindings: bindings, Egress: egress}, nil } // Ceilings on what a non-admin may mint for itself: a token is a standing @@ -3090,45 +3055,42 @@ func (s *Server) HandleUserBootstrapTokens(w http.ResponseWriter, r *http.Reques // AutonomousRecovery is admin-only here: it decides whether a lost device // can rejoin the mesh on its own. - var req api.BootstrapTokenRequest - r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodyBytes) - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid JSON body", http.StatusBadRequest) + req := &api.BootstrapTokenCreateRequest{} + if !readProtoJSON(w, r, req) { return } - defer func() { _ = r.Body.Close() }() - if req.Role == "" { + if req.GetRole() == "" { req.Role = api.RoleNode } - if user.Role != "admin" && req.Role != api.RoleNode { + if user.Role != "admin" && req.GetRole() != api.RoleNode { http.Error(w, "Forbidden: Standard users can only generate tokens for node role", http.StatusForbidden) return } - if user.Role != "admin" && req.AutonomousRecovery { + if user.Role != "admin" && req.GetAutonomousRecovery() { http.Error(w, "Forbidden: only admins can issue tokens with autonomous_recovery", http.StatusForbidden) return } - ownerID, status, err := s.resolveTokenOwner(r.Context(), user, req.OwnerID) + ownerID, status, err := s.resolveTokenOwner(r.Context(), user, req.GetOwnerId()) if err != nil { http.Error(w, err.Error(), status) return } - if req.TTLHours <= 0 { - req.TTLHours = 24 + if req.GetTtlHours() <= 0 { + req.TtlHours = 24 } - if req.MaxUsages <= 0 { + if req.GetMaxUsages() <= 0 { req.MaxUsages = 1 } if user.Role != "admin" { - if req.TTLHours > userTokenMaxTTLHours { + if req.GetTtlHours() > userTokenMaxTTLHours { http.Error(w, fmt.Sprintf("ttl_hours may not exceed %d for non-admin users", userTokenMaxTTLHours), http.StatusBadRequest) return } - if req.MaxUsages > userTokenMaxUsages { + if req.GetMaxUsages() > userTokenMaxUsages { http.Error(w, fmt.Sprintf("max_usages may not exceed %d for non-admin users", userTokenMaxUsages), http.StatusBadRequest) return } @@ -3145,14 +3107,14 @@ func (s *Server) HandleUserBootstrapTokens(w http.ResponseWriter, r *http.Reques tokenRecord := &storage.BootstrapToken{ ID: tokenID, TokenHash: tokenID, - Role: req.Role, + Role: req.GetRole(), OwnerID: ownerID, - MaxUsages: req.MaxUsages, + MaxUsages: int(req.GetMaxUsages()), UsagesCount: 0, - Description: req.Description, + Description: req.GetDescription(), CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(time.Duration(req.TTLHours) * time.Hour), - AutonomousRecovery: req.AutonomousRecovery, + ExpiresAt: time.Now().Add(time.Duration(req.GetTtlHours()) * time.Hour), + AutonomousRecovery: req.GetAutonomousRecovery(), } if err := s.store.SaveBootstrapToken(r.Context(), tokenRecord); err != nil { @@ -3161,14 +3123,12 @@ func (s *Server) HandleUserBootstrapTokens(w http.ResponseWriter, r *http.Reques return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(api.BootstrapTokenResponse{ - ID: tokenRecord.ID, - Token: tokenVal, - Role: tokenRecord.Role, - OwnerID: tokenRecord.OwnerID, - ExpiresAt: tokenRecord.ExpiresAt.Format(time.RFC3339), + writeProtoJSONStatus(w, http.StatusCreated, &api.BootstrapTokenCreateResponse{ + Id: tokenRecord.ID, + Token: tokenVal, + Role: tokenRecord.Role, + OwnerId: tokenRecord.OwnerID, + ExpireTime: timestamppb.New(tokenRecord.ExpiresAt), }) } @@ -3401,20 +3361,6 @@ func toStringSlice(val any) []string { return nil } -// marshalPolicyJSON renders the stored mesh policy as protojson using the proto -// field names. Generated marshalling is the point: a hand-maintained mirror of -// PolicyRole silently drops any field it forgets, which is how custom_datalog -// went missing from the console for so long. -func marshalPolicyJSON(roles []*api.PolicyRole, bindings []*api.PolicyBinding, egress []*api.EgressDestination) (string, error) { - resp := &api.PolicyConfig{Roles: roles, Bindings: bindings, Egress: egress} - marshaler := protojson.MarshalOptions{UseProtoNames: true, Multiline: true, Indent: " "} - out, err := marshaler.Marshal(resp) - if err != nil { - return "", err - } - return string(out), nil -} - // maxIdentityFactBudget bounds the worst-case number of Datalog facts a policy // config could let a single identity accumulate across all of its resolved // roles. biscuit-go's authorizer defaults to rejecting worlds beyond ~1000 diff --git a/internal/controlplane/server_test.go b/internal/controlplane/server_test.go index 40724c07..c2129993 100644 --- a/internal/controlplane/server_test.go +++ b/internal/controlplane/server_test.go @@ -619,10 +619,11 @@ func TestMarshalPolicyJSONRoundTrip(t *testing.T) { {Role: "ops", Members: []string{"user:root"}}, } - rendered, err := marshalPolicyJSON(roles, bindings, nil) + renderedBytes, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(&api.PolicyConfig{Roles: roles, Bindings: bindings}) if err != nil { t.Fatalf("rendering the policy: %v", err) } + rendered := string(renderedBytes) // Proto names, because that is what the docs and the Helm bootstrap job use. if !strings.Contains(rendered, "allowed_services") || !strings.Contains(rendered, "custom_datalog") { @@ -712,11 +713,11 @@ func TestPoliciesAcceptConsoleJSON(t *testing.T) { } // What /status hands the console must be postable back unchanged. - rendered, err := marshalPolicyJSON(roles, bindings, nil) + rendered, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(&api.PolicyConfig{Roles: roles, Bindings: bindings}) if err != nil { t.Fatalf("rendering the stored policy: %v", err) } - req, _ = http.NewRequest(http.MethodPost, baseURL+"/policies", strings.NewReader(rendered)) + req, _ = http.NewRequest(http.MethodPost, baseURL+"/policies", bytes.NewReader(rendered)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer super-secret-admin-token") resp, err = client.Do(req) @@ -1001,14 +1002,17 @@ func TestEnrollmentWorkflow(t *testing.T) { if err != nil { t.Fatal(err) } - var enrollList []storage.EnrollmentRequest - _ = json.NewDecoder(resp.Body).Decode(&enrollList) + enrollListBody, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() + enrollList := &api.EnrollmentRequestListResponse{} + if err := protojson.Unmarshal(enrollListBody, enrollList); err != nil { + t.Fatalf("decode /admin/enrollments: %v", err) + } - if len(enrollList) != 1 || enrollList[0].PeerID != pID.String() { + if len(enrollList.GetRequests()) != 1 || enrollList.GetRequests()[0].GetPeerId() != pID.String() { t.Fatalf("unexpected enrollments list: %+v", enrollList) } - reqID := enrollList[0].ID + reqID := enrollList.GetRequests()[0].GetId() // Approve req, _ = http.NewRequest("POST", baseURL+"/admin/enrollments/"+reqID+"/approve", nil) @@ -2208,29 +2212,20 @@ func TestAdminPanelAndUI(t *testing.T) { t.Errorf("expected 200 status for authenticated status query, got: %d", resp.StatusCode) } - var statusData map[string]any - if err := json.NewDecoder(resp.Body).Decode(&statusData); err != nil { - t.Fatalf("failed to decode admin status response: %v", err) + statusBody, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read admin status response: %v", err) } _ = resp.Body.Close() - - if _, ok := statusData["active_routers"]; !ok { - t.Error("status response missing active_routers") - } - if _, ok := statusData["enrolled_nodes"]; !ok { - t.Error("status response missing enrolled_nodes") - } - if _, ok := statusData["enrollment_requests"]; !ok { - t.Error("status response missing enrollment_requests") - } - if _, ok := statusData["bootstrap_tokens"]; !ok { - t.Error("status response missing bootstrap_tokens") + statusData := &api.AdminStatusResponse{} + if err := protojson.Unmarshal(statusBody, statusData); err != nil { + t.Fatalf("failed to decode admin status response: %v", err) } - users, ok := statusData["users"].([]any) - if !ok || len(users) != 1 { - t.Fatalf("expected 1 user in status response, got: %v", statusData["users"]) + + if len(statusData.GetUsers()) != 1 { + t.Fatalf("expected 1 user in status response, got: %v", statusData.GetUsers()) } - if got := users[0].(map[string]any)["ID"]; got != testUser.ID { + if got := statusData.GetUsers()[0].GetId(); got != testUser.ID { t.Errorf("expected user ID %q, got %v", testUser.ID, got) } @@ -2341,20 +2336,19 @@ func TestUserStatusAndTenancy(t *testing.T) { t.Fatalf("expected 200, got: %d", resp.StatusCode) } - var data map[string]interface{} - _ = json.NewDecoder(resp.Body).Decode(&data) + statusBody, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() - - enrolledNodes, ok := data["enrolled_nodes"].([]interface{}) - if !ok { - t.Fatal("enrolled_nodes is not an array") + data := &api.UserStatusResponse{} + if err := protojson.Unmarshal(statusBody, data); err != nil { + t.Fatalf("decode /user/status: %v", err) } + + enrolledNodes := data.GetEnrolledNodes() if len(enrolledNodes) != 1 { t.Fatalf("expected 1 node for User A, got: %d", len(enrolledNodes)) } - nodeMap := enrolledNodes[0].(map[string]interface{}) - if nodeMap["PeerID"] != peerA.String() { - t.Errorf("expected node %q, got: %s", peerA.String(), nodeMap["PeerID"]) + if enrolledNodes[0].GetPeerId() != peerA.String() { + t.Errorf("expected node %q, got: %s", peerA.String(), enrolledNodes[0].GetPeerId()) } reqRevoke, _ := http.NewRequest("POST", baseURL+"/user/revoke?id="+peerB.String(), nil) @@ -3184,16 +3178,20 @@ func TestAdminBootstrapTokensList(t *testing.T) { if resp.StatusCode != http.StatusOK { t.Fatalf("GET /admin/bootstrap-tokens status = %s, want 200", resp.Status) } - var list []storage.BootstrapToken - if err := json.NewDecoder(resp.Body).Decode(&list); err != nil { - t.Fatalf("failed to decode token list: %v", err) + listBody, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read token list: %v", err) } _ = resp.Body.Close() - if len(list) != 1 { - t.Fatalf("token list length = %d, want 1", len(list)) + list := &api.BootstrapTokenListResponse{} + if err := protojson.Unmarshal(listBody, list); err != nil { + t.Fatalf("failed to decode token list: %v", err) + } + if len(list.GetTokens()) != 1 { + t.Fatalf("token list length = %d, want 1", len(list.GetTokens())) } - if list[0].Role != "sam:role:node" || list[0].MaxUsages != 3 || list[0].Description != "cli test" { - t.Errorf("unexpected token record: %+v", list[0]) + if tok := list.GetTokens()[0]; tok.GetRole() != "sam:role:node" || tok.GetMaxUsages() != 3 || tok.GetDescription() != "cli test" { + t.Errorf("unexpected token record: %+v", tok) } // GET without credentials is refused. @@ -3672,17 +3670,19 @@ func TestAdminRevokeBootstrapToken(t *testing.T) { if err != nil { t.Fatal(err) } - var listed []struct { - ID string - } - if err := json.NewDecoder(listResp.Body).Decode(&listed); err != nil { + listedBody, err := io.ReadAll(listResp.Body) + if err != nil { t.Fatal(err) } _ = listResp.Body.Close() - if len(listed) != 1 { - t.Fatalf("GET /admin/bootstrap-tokens returned %d tokens, want 1", len(listed)) + listed := &api.BootstrapTokenListResponse{} + if err := protojson.Unmarshal(listedBody, listed); err != nil { + t.Fatal(err) + } + if len(listed.GetTokens()) != 1 { + t.Fatalf("GET /admin/bootstrap-tokens returned %d tokens, want 1", len(listed.GetTokens())) } - tokenID := listed[0].ID + tokenID := listed.GetTokens()[0].GetId() priv, pub, err := crypto.GenerateKeyPair(crypto.Ed25519, -1) if err != nil { diff --git a/internal/controlplane/ui.go b/internal/controlplane/ui.go index 2b906412..530cba6f 100644 --- a/internal/controlplane/ui.go +++ b/internal/controlplane/ui.go @@ -15,14 +15,14 @@ package controlplane import ( - "encoding/json" "net/http" "time" - "github.com/google/sam/internal/storage" + "github.com/google/sam/api" ) -// HandleAdminStatus returns a consolidated JSON state of the control plane. +// HandleAdminStatus HTTP GET `/admin/status`: the whole mesh as the console +// shows it, protojson of AdminStatusResponse. func (s *Server) HandleAdminStatus(w http.ResponseWriter, r *http.Request) { if !s.checkAdminAuth(w, r) { return @@ -55,10 +55,6 @@ func (s *Server) HandleAdminStatus(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal server error", http.StatusInternalServerError) return } - for i := range reqs { - reqs[i].BiscuitToken = nil - reqs[i].PublicKey = nil - } tokens, err := s.store.ListBootstrapTokens(ctx) if err != nil { @@ -74,37 +70,25 @@ func (s *Server) HandleAdminStatus(w http.ResponseWriter, r *http.Request) { return } - roles, bindings, err := s.store.GetMeshPolicy(r.Context()) - if err != nil && err != storage.ErrNotFound { - logger.Errorf("Failed to list policy: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - egress, err := s.store.GetEgressDestinations(r.Context()) - if err != nil && err != storage.ErrNotFound { - logger.Errorf("Failed to list egress destinations: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - policyJSON, err := marshalPolicyJSON(roles, bindings, egress) + policy, err := s.loadPolicyConfig(ctx) if err != nil { - logger.Errorf("Failed to render policy: %v", err) + logger.Errorf("Failed to list policy: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - resp := map[string]any{ - "users": users, - "active_routers": routers, - "enrolled_nodes": nodes, - "enrollment_requests": reqs, - "bootstrap_tokens": tokens, - "policy_json": policyJSON, - "node_catalog": s.catalogViewFor(nodes, time.Now()), + enrolled := make([]*api.EnrolledNode, 0, len(nodes)) + for i := range nodes { + enrolled = append(enrolled, enrolledNodeView(&nodes[i], true)) } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(resp) + writeProtoJSON(w, &api.AdminStatusResponse{ + Users: userViews(users), + ActiveRouters: routerLeaseViews(routers), + EnrolledNodes: enrolled, + EnrollmentRequests: enrollmentRequestViews(reqs), + BootstrapTokens: bootstrapTokenViews(tokens), + Policy: policy, + NodeCatalog: s.catalogViewFor(nodes, time.Now()), + }) } diff --git a/internal/controlplane/views.go b/internal/controlplane/views.go new file mode 100644 index 00000000..a38519e1 --- /dev/null +++ b/internal/controlplane/views.go @@ -0,0 +1,143 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controlplane + +import ( + "time" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/google/sam/api" + "github.com/google/sam/internal/storage" +) + +// The operator plane (/admin/*, /user/*) serves api.proto messages; these +// render the store's records into them, leaving credentials and key material +// behind. + +// timestampOrNil keeps a zero time unset, which protojson omits. +func timestampOrNil(t time.Time) *timestamppb.Timestamp { + if t.IsZero() { + return nil + } + return timestamppb.New(t) +} + +func bootstrapTokenView(t *storage.BootstrapToken) *api.BootstrapToken { + v := &api.BootstrapToken{ + Id: t.ID, + Role: t.Role, + OwnerId: t.OwnerID, + MaxUsages: int32(t.MaxUsages), + UsagesCount: int32(t.UsagesCount), + Description: t.Description, + CreateTime: timestampOrNil(t.CreatedAt), + ExpireTime: timestampOrNil(t.ExpiresAt), + AutonomousRecovery: t.AutonomousRecovery, + } + if t.RevokedAt != nil { + v.RevokeTime = timestamppb.New(*t.RevokedAt) + } + return v +} + +func bootstrapTokenViews(list []storage.BootstrapToken) []*api.BootstrapToken { + out := make([]*api.BootstrapToken, 0, len(list)) + for i := range list { + out = append(out, bootstrapTokenView(&list[i])) + } + return out +} + +func enrollmentRequestView(r *storage.EnrollmentRequest) *api.EnrollmentRequest { + v := &api.EnrollmentRequest{ + Id: r.ID, + PeerId: r.PeerID, + TokenId: r.TokenID, + Status: r.Status, + Labels: r.Labels, + CreateTime: timestampOrNil(r.CreatedAt), + ResolvedBy: r.ResolvedBy, + } + if r.ResolvedAt != nil { + v.ResolveTime = timestamppb.New(*r.ResolvedAt) + } + return v +} + +func enrollmentRequestViews(list []storage.EnrollmentRequest) []*api.EnrollmentRequest { + out := make([]*api.EnrollmentRequest, 0, len(list)) + for i := range list { + out = append(out, enrollmentRequestView(&list[i])) + } + return out +} + +func userView(u *storage.User) *api.User { + return &api.User{ + Id: u.ID, + Issuer: u.Issuer, + Email: u.Email, + Role: u.Role, + CreateTime: timestampOrNil(u.CreatedAt), + } +} + +func userViews(list []storage.User) []*api.User { + out := make([]*api.User, 0, len(list)) + for i := range list { + out = append(out, userView(&list[i])) + } + return out +} + +// enrolledNodeView drops the node's biscuit and public key. The identity +// provider's claims are admin material. +func enrolledNodeView(n *storage.EnrolledNode, withClaims bool) *api.EnrolledNode { + v := &api.EnrolledNode{ + PeerId: n.PeerID, + Role: n.Role, + EnrollmentType: n.EnrollmentType, + OwnerId: n.OwnerID, + Labels: n.Labels, + EnrollTime: timestampOrNil(n.EnrolledAt), + ExpireTime: timestampOrNil(n.ExpiresAt), + Banned: n.Banned, + AutonomousRecovery: n.AutonomousRecovery, + } + if withClaims { + v.ClaimsJson = n.ClaimsJSON + } + return v +} + +func routerLeaseView(l *storage.RouterLease) *api.RouterLease { + return &api.RouterLease{ + PeerId: l.PeerID, + Addresses: l.Addresses, + LastRenewalTime: timestampOrNil(l.LastRenewal), + ExpireTime: timestampOrNil(l.ExpiresAt), + ConnectedPeers: l.ConnectedPeers, + DhtSize: int32(l.DHTSize), + } +} + +func routerLeaseViews(list []storage.RouterLease) []*api.RouterLease { + out := make([]*api.RouterLease, 0, len(list)) + for i := range list { + out = append(out, routerLeaseView(&list[i])) + } + return out +} diff --git a/sdk/js/src/gen/sam_pb.ts b/sdk/js/src/gen/sam_pb.ts index be7dea1e..93e02c31 100644 --- a/sdk/js/src/gen/sam_pb.ts +++ b/sdk/js/src/gen/sam_pb.ts @@ -26,7 +26,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file sam.proto. */ export const file_sam: GenFile = /*@__PURE__*/ - fileDesc("CglzYW0ucHJvdG8SBnNhbS52MSI0CglBdXRoRnJhbWUSDwoHYmlzY3VpdBgBIAEoDBIWCg50YXJnZXRfc2VydmljZRgCIAEoCSI/CgxBdXRoUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCRIPCgdiaXNjdWl0GAMgASgMItYBCglNZXNoRXZlbnQSJAoEdHlwZRgBIAEoDjIWLnNhbS52MS5NZXNoRXZlbnQuVHlwZRIPCgdwZWVyX2lkGAIgASgJEi4KCmV2ZW50X3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDm5ld19wdWJsaWNfa2V5GAQgASgMEhEKCXNpZ25hdHVyZRgFIAEoDCI3CgRUeXBlEgoKBkJBTk5FRBAAEhAKDEtFWV9ST1RBVElPThABEhEKDVBPTElDWV9VUERBVEUQAiLzAQoNRW5yb2xsUmVxdWVzdBILCgNqd3QYASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEjEKBmxhYmVscxgFIAMoCzIhLnNhbS52MS5FbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKrAQoORW5yb2xsUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIVCg1lcnJvcl9tZXNzYWdlGAIgASgJEiAKGGNvbnRyb2xfcGxhbmVfcHVibGljX2tleRgDIAEoDBIYChByb3V0ZXJfYWRkcmVzc2VzGAQgAygJEi8KC2V4cGlyZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKRAgoWQm9vdHN0cmFwRW5yb2xsUmVxdWVzdBIXCg9ib290c3RyYXBfdG9rZW4YASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEjoKBmxhYmVscxgFIAMoCzIqLnNhbS52MS5Cb290c3RyYXBFbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASL9AQoXQm9vdHN0cmFwRW5yb2xsUmVzcG9uc2USKAoGc3RhdHVzGAEgASgOMhguc2FtLnYxLkVucm9sbG1lbnRTdGF0dXMSFQoNYmlzY3VpdF90b2tlbhgCIAEoDBIdChVwb2xsX2ludGVydmFsX3NlY29uZHMYAyABKAUSFQoNZXJyb3JfbWVzc2FnZRgEIAEoCRIgChhjb250cm9sX3BsYW5lX3B1YmxpY19rZXkYBSABKAwSGAoQcm91dGVyX2FkZHJlc3NlcxgGIAMoCRIvCgtleHBpcmVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUwoLU2VydmljZUluZm8SIQoEdHlwZRgBIAEoDjITLnNhbS52MS5TZXJ2aWNlVHlwZRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJInsKDkNvbW1hbmRCYWNrZW5kEg8KB2NvbW1hbmQYASADKAkSLAoDZW52GAIgAygLMh8uc2FtLnYxLkNvbW1hbmRCYWNrZW5kLkVudkVudHJ5GioKCEVudkVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiigEKFlJlZ2lzdGVyU2VydmljZVJlcXVlc3QSJAoHc2VydmljZRgBIAEoCzITLnNhbS52MS5TZXJ2aWNlSW5mbxIUCgp0YXJnZXRfdXJsGAIgASgJSAASKQoHY29tbWFuZBgDIAEoCzIWLnNhbS52MS5Db21tYW5kQmFja2VuZEgAQgkKB2JhY2tlbmQiaQoSRGlzY292ZXJlZFByb3ZpZGVyEg8KB3BlZXJfaWQYASABKAkSFwoPbG9jYWxfcHJveHlfdXJsGAIgASgJEhAKCHNydl9uYW1lGAMgASgJEhcKD3Nydl9kZXNjcmlwdGlvbhgEIAEoCSKyAgoPU2VydmljZUFubm91bmNlEg8KB3BlZXJfaWQYASABKAkSIQoEdHlwZRgCIAEoDjITLnNhbS52MS5TZXJ2aWNlVHlwZRIUCgxzZXJ2aWNlX25hbWUYAyABKAkSDAoEa2V5cxgEIAMoCRIzCgZsYWJlbHMYBSADKAsyIy5zYW0udjEuU2VydmljZUFubm91bmNlLkxhYmVsc0VudHJ5EhcKD2FjdGl2ZV9yZXF1ZXN0cxgGIAEoDRIXCg9sYXRlbmN5X2V3bWFfbXMYByABKAESMQoNYW5ub3VuY2VfdGltZRgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKHAQoYQ29udHJvbFBsYW5lSW5mb1Jlc3BvbnNlEhMKC29pZGNfaXNzdWVyGAEgASgJEhEKCWNsaWVudF9pZBgCIAEoCRIQCghhdWRpZW5jZRgDIAEoCRIYChByb3V0ZXJfYWRkcmVzc2VzGAQgAygJEhcKD2Jhbm5lZF9wZWVyX2lkcxgFIAMoCSKsAQoSUm91dGVyTGVhc2VSZXF1ZXN0Eg8KB3BlZXJfaWQYASABKAkSEQoJYWRkcmVzc2VzGAIgAygJEg8KB2Jpc2N1aXQYAyABKAwSFwoPY29ubmVjdGVkX3BlZXJzGAQgAygJEhAKCGRodF9zaXplGAUgASgFEhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwiZgoTUm91dGVyTGVhc2VSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg0KBWVycm9yGAIgASgJEi8KC2V4cGlyZV90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKeAQoKUG9saWN5Um9sZRIMCgRuYW1lGAEgASgJEhcKD2FsbG93ZWRfdGFyZ2V0cxgCIAMoCRIYChBhbGxvd2VkX3NlcnZpY2VzGAMgAygJEhYKDmN1c3RvbV9kYXRhbG9nGAQgAygJEhYKDmFsbG93ZWRfbGFiZWxzGAUgAygJEh8KBGh0dHAYBiADKAsyES5zYW0udjEuSFRUUEdyYW50IjwKCUhUVFBHcmFudBIPCgdzZXJ2aWNlGAEgASgJEg8KB21ldGhvZHMYAiADKAkSDQoFcGF0aHMYAyADKAkijwIKEUVncmVzc0Rlc3RpbmF0aW9uEgwKBG5hbWUYASABKAkSEgoKdGFyZ2V0X3VybBgCIAEoCRISCgpjcmVkZW50aWFsGAMgASgJEhEKCXNlcnZlZF9ieRgEIAMoCRIoCgZicm9rZXIYBSABKAsyGC5zYW0udjEuQ3JlZGVudGlhbEJyb2tlchImCgppbnNwZWN0aW9uGAYgASgLMhIuc2FtLnYxLkluc3BlY3Rpb24SIAoEbW9kZRgHIAEoDjISLnNhbS52MS5FZ3Jlc3NNb2RlEg0KBXBvcnRzGAggAygNEhUKDXByZXNlcnZlX2hvc3QYCSABKAgSFwoPZm9yd2FyZF9jb250ZXh0GAogASgIIjMKCkluc3BlY3Rpb24SJQoKaW5zcGVjdG9ycxgBIAMoCzIRLnNhbS52MS5JbnNwZWN0b3IiYwoJSW5zcGVjdG9yEikKC21vZGVsX2FybW9yGAEgASgLMhIuc2FtLnYxLk1vZGVsQXJtb3JIABIjCghleHRfcHJvYxgCIAEoCzIPLnNhbS52MS5FeHRQcm9jSABCBgoEa2luZCKLAQoKTW9kZWxBcm1vchIQCgh0ZW1wbGF0ZRgBIAEoCRIsCghyZXNwb25zZRgCIAEoDjIaLnNhbS52MS5SZXNwb25zZUluc3BlY3Rpb24SEQoJZmFpbF9vcGVuGAMgASgIEioKB3RpbWVvdXQYBCABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iggIKB0V4dFByb2MSDgoGdGFyZ2V0GAEgASgJEgoKAmNhGAIgASgJEhoKEmNsaWVudF9jZXJ0aWZpY2F0ZRgDIAEoCRI2Cg9wcm9jZXNzaW5nX21vZGUYBCABKAsyHS5zYW0udjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlEhsKE2FsbG93X21vZGVfb3ZlcnJpZGUYBSABKAgSMgoPbWVzc2FnZV90aW1lb3V0GAYgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEhoKEmZhaWx1cmVfbW9kZV9hbGxvdxgHIAEoCBIaChJtYXhfYnVmZmVyZWRfYnl0ZXMYCCABKA0i2wQKFUV4dFByb2NQcm9jZXNzaW5nTW9kZRJFChNyZXF1ZXN0X2hlYWRlcl9tb2RlGAEgASgOMiguc2FtLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEkYKFHJlc3BvbnNlX2hlYWRlcl9tb2RlGAIgASgOMiguc2FtLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEkEKEXJlcXVlc3RfYm9keV9tb2RlGAMgASgOMiYuc2FtLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5Cb2R5TW9kZRJCChJyZXNwb25zZV9ib2R5X21vZGUYBCABKA4yJi5zYW0udjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlLkJvZHlNb2RlEkYKFHJlcXVlc3RfdHJhaWxlcl9tb2RlGAUgASgOMiguc2FtLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEkcKFXJlc3BvbnNlX3RyYWlsZXJfbW9kZRgGIAEoDjIoLnNhbS52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZSI5CgpIZWFkZXJNb2RlEhcKE0hFQURFUl9NT0RFX0RFRkFVTFQQABIICgRTRU5EEAESCAoEU0tJUBACImAKCEJvZHlNb2RlEggKBE5PTkUQABIMCghTVFJFQU1FRBABEgwKCEJVRkZFUkVEEAISFAoQQlVGRkVSRURfUEFSVElBTBADEhgKFEZVTExfRFVQTEVYX1NUUkVBTUVEEAQizwEKEENyZWRlbnRpYWxCcm9rZXISFwoNc3RhdGljX3NlY3JldBgBIAEoCUgAEjEKD29pZGNfZmVkZXJhdGlvbhgCIAEoCzIWLnNhbS52MS5PSURDRmVkZXJhdGlvbkgAEjAKD2F3c19hc3N1bWVfcm9sZRgDIAEoCzIVLnNhbS52MS5BV1NBc3N1bWVSb2xlSAASNQoRcGxhdGZvcm1faWRlbnRpdHkYBCABKAsyGC5zYW0udjEuUGxhdGZvcm1JZGVudGl0eUgAQgYKBGtpbmQiXwoOT0lEQ0ZlZGVyYXRpb24SFgoOdG9rZW5fZW5kcG9pbnQYASABKAkSEAoIYXVkaWVuY2UYAiABKAkSEwoLaW1wZXJzb25hdGUYAyABKAkSDgoGc2NvcGVzGAQgAygJIjkKDUFXU0Fzc3VtZVJvbGUSEAoIcm9sZV9hcm4YASABKAkSFgoOc2Vzc2lvbl9wb2xpY3kYAiABKAkiIgoQUGxhdGZvcm1JZGVudGl0eRIOCgZzY29wZXMYASADKAkiLgoNUG9saWN5QmluZGluZxIMCgRyb2xlGAEgASgJEg8KB21lbWJlcnMYAiADKAkihQEKDFBvbGljeUNvbmZpZxIhCgVyb2xlcxgBIAMoCzISLnNhbS52MS5Qb2xpY3lSb2xlEicKCGJpbmRpbmdzGAIgAygLMhUuc2FtLnYxLlBvbGljeUJpbmRpbmcSKQoGZWdyZXNzGAMgAygLMhkuc2FtLnYxLkVncmVzc0Rlc3RpbmF0aW9uIhgKFlBvbGljeUNvbmZpZ0dldFJlcXVlc3QiMAoXUG9saWN5Q29uZmlnR2V0UmVzcG9uc2USFQoNZGF0YWxvZ19ydWxlcxgBIAMoCSI8ChpQb2xpY3lDb25maWdVcGRhdGVSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg0KBWVycm9yGAIgASgJIhoKGEVncmVzc0Fzc2lnbm1lbnRzUmVxdWVzdCJGChlFZ3Jlc3NBc3NpZ25tZW50c1Jlc3BvbnNlEikKBmVncmVzcxgBIAMoCzIZLnNhbS52MS5FZ3Jlc3NEZXN0aW5hdGlvbiJmCgxLZXlzUmVzcG9uc2USEwoLcHVibGljX2tleXMYASADKAwSLQoJc2lnbl90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpzaWduYXR1cmVzGAMgAygMImsKE1Rva2VuUmVmcmVzaFJlcXVlc3QSGwoTY2hhbGxlbmdlX3NpZ25hdHVyZRgBIAEoDBIZChFjaGFsbGVuZ2VfdW5peF9tcxgCIAEoAxIPCgdwZWVyX2lkGAMgASgJEgsKA2p3dBgEIAEoCSJ1ChRUb2tlblJlZnJlc2hSZXNwb25zZRIVCg1iaXNjdWl0X3Rva2VuGAEgASgMEi8KC2V4cGlyZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIVCg1lcnJvcl9tZXNzYWdlGAMgASgJIjoKEU5vZGVDYXRhbG9nUmVwb3J0EiUKCHNlcnZpY2VzGAEgAygLMhMuc2FtLnYxLlNlcnZpY2VJbmZvIiUKElRva2VuUmV2b2tlUmVxdWVzdBIPCgdwZWVyX2lkGAEgASgJIjUKE1Rva2VuUmV2b2tlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCSLkAQoYSWRlbnRpdHlFdmlkZW5jZVJlc3BvbnNlEg8KB3BlZXJfaWQYASABKAkSDwoHYmlzY3VpdBgCIAEoDBI3ChNiaXNjdWl0X2V4cGlyZV90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIZChFjb250cm9sX3BsYW5lX3VybBgEIAEoCRIiChp0cnVzdGVkX2NvbnRyb2xfcGxhbmVfa2V5cxgFIAMoDBIuCgpjaGVja190aW1lGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCLAAgoUUGVlckV2aWRlbmNlUmVzcG9uc2USDwoHcGVlcl9pZBgBIAEoCRIPCgdiaXNjdWl0GAIgASgMEhUKDXZlcmlmeWluZ19rZXkYAyABKAwSDQoFcm9sZXMYBCADKAkSOAoGbGFiZWxzGAUgAygLMiguc2FtLnYxLlBlZXJFdmlkZW5jZVJlc3BvbnNlLkxhYmVsc0VudHJ5Ei8KC2V4cGlyZV90aW1lGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIWCg5yZXZvY2F0aW9uX2lkcxgHIAMoCRIuCgpjaGVja190aW1lGAggASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIoACChBNZW1iZXJDcmVkZW50aWFsEhkKEWNvbnRyb2xfcGxhbmVfdXJsGAEgASgJEg8KB2Jpc2N1aXQYAiABKAwSLwoLZXhwaXJlX3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KDHRydXN0ZWRfa2V5cxgEIAMoCzIZLnNhbS52MS5UcnVzdGVkU2lnbmluZ0tleRIZChFpc3N1ZWRfdW5kZXJfa2V5cxgFIAMoDBIYChByb3V0ZXJfYWRkcmVzc2VzGAYgAygJEikKDG9pZGNfc2Vzc2lvbhgHIAEoCzITLnNhbS52MS5PSURDU2Vzc2lvbiJZChFUcnVzdGVkU2lnbmluZ0tleRISCgpwdWJsaWNfa2V5GAEgASgMEjAKDHJlY2VpdmVfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiWQoLT0lEQ1Nlc3Npb24SDgoGaXNzdWVyGAEgASgJEhEKCWNsaWVudF9pZBgCIAEoCRIQCghhdWRpZW5jZRgDIAEoCRIVCg1yZWZyZXNoX3Rva2VuGAQgASgJIo0BChVUYXNrQXV0aG9yaXphdGlvblJ1bGUSDAoEbmFtZRgBIAEoCRIUCgxkaXNwbGF5X25hbWUYAiABKAkSHwoFcnVsZXMYAyADKAsyEC5zYW0udjEuVGFza1J1bGUSLwoLZXhwaXJlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIn4KCFRhc2tSdWxlEhMKC2Rlc2NyaXB0aW9uGAEgASgJEhgKEGFsbG93ZWRfc2VydmljZXMYAiADKAkSKAoJb3BlcmF0aW9uGAMgASgLMhUuc2FtLnYxLlRhc2tPcGVyYXRpb24SGQoRYWxsb3dlZF9yZXNvdXJjZXMYBCADKAkicwoNVGFza09wZXJhdGlvbhIVCg1hbGxvd2VkX3Rvb2xzGAEgAygJEhcKD2FsbG93ZWRfbWV0aG9kcxgCIAMoCRIVCg1hbGxvd2VkX3BhdGhzGAMgAygJEhsKE2FsbG93ZWRfcGVybWlzc2lvbnMYBCADKAkipQEKFFRva2VuRXhjaGFuZ2VSZXF1ZXN0EhUKDXN1YmplY3RfdG9rZW4YASABKAkSMAoJdGFza19ydWxlGAIgASgLMh0uc2FtLnYxLlRhc2tBdXRob3JpemF0aW9uUnVsZRIMCgRzZWFsGAMgASgIEhkKEWNoYWxsZW5nZV91bml4X21zGAQgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYBSABKAwifwoVVG9rZW5FeGNoYW5nZVJlc3BvbnNlEhUKDWJpc2N1aXRfdG9rZW4YASABKAwSLwoLZXhwaXJlX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg0KBXJvbGVzGAMgAygJEg8KB3N1YmplY3QYBCABKAkigQEKD1NUU1Rva2VuUmVxdWVzdBIPCgdiaXNjdWl0GAEgASgMEhMKC2Rlc3RpbmF0aW9uGAIgASgJEhAKCGF1ZGllbmNlGAMgASgJEhkKEWNoYWxsZW5nZV91bml4X21zGAQgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYBSABKAwigwEKEFNUU1Rva2VuUmVzcG9uc2USCwoDand0GAEgASgJEi8KC2V4cGlyZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdzdWJqZWN0GAMgASgJEg0KBXJvbGVzGAQgAygJEhEKCXRhc2tfbmFtZRgFIAEoCSJGChNSZXZvY2F0aW9uc1Jlc3BvbnNlEhYKDnJldm9jYXRpb25faWRzGAEgAygJEhcKD2Jhbm5lZF9wZWVyX2lkcxgCIAMoCSqUAQoQRW5yb2xsbWVudFN0YXR1cxIhCh1FTlJPTExNRU5UX1NUQVRVU19VTlNQRUNJRklFRBAAEh0KGUVOUk9MTE1FTlRfU1RBVFVTX1BFTkRJTkcQARIeChpFTlJPTExNRU5UX1NUQVRVU19BUFBST1ZFRBACEh4KGkVOUk9MTE1FTlRfU1RBVFVTX1JFSkVDVEVEEAMqjAEKC1NlcnZpY2VUeXBlEhwKGFNFUlZJQ0VfVFlQRV9VTlNQRUNJRklFRBAAEhQKEFNFUlZJQ0VfVFlQRV9NQ1AQARIaChZTRVJWSUNFX1RZUEVfSU5GRVJFTkNFEAISFAoQU0VSVklDRV9UWVBFX0EyQRADEhcKE1NFUlZJQ0VfVFlQRV9FR1JFU1MQBCo3CgpFZ3Jlc3NNb2RlEhQKEEVHUkVTU19NT0RFX0hUVFAQABITCg9FR1JFU1NfTU9ERV9UQ1AQASpcChJSZXNwb25zZUluc3BlY3Rpb24SIAocUkVTUE9OU0VfSU5TUEVDVElPTl9CVUZGRVJFRBAAEiQKIFJFU1BPTlNFX0lOU1BFQ1RJT05fUkVRVUVTVF9PTkxZEAFCG1oZZ2l0aHViLmNvbS9nb29nbGUvc2FtL2FwaWIGcHJvdG8z", [file_google_protobuf_duration, file_google_protobuf_timestamp]); + fileDesc("CglzYW0ucHJvdG8SBnNhbS52MSI0CglBdXRoRnJhbWUSDwoHYmlzY3VpdBgBIAEoDBIWCg50YXJnZXRfc2VydmljZRgCIAEoCSI/CgxBdXRoUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCRIPCgdiaXNjdWl0GAMgASgMItYBCglNZXNoRXZlbnQSJAoEdHlwZRgBIAEoDjIWLnNhbS52MS5NZXNoRXZlbnQuVHlwZRIPCgdwZWVyX2lkGAIgASgJEi4KCmV2ZW50X3RpbWUYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhYKDm5ld19wdWJsaWNfa2V5GAQgASgMEhEKCXNpZ25hdHVyZRgFIAEoDCI3CgRUeXBlEgoKBkJBTk5FRBAAEhAKDEtFWV9ST1RBVElPThABEhEKDVBPTElDWV9VUERBVEUQAiLzAQoNRW5yb2xsUmVxdWVzdBILCgNqd3QYASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEjEKBmxhYmVscxgFIAMoCzIhLnNhbS52MS5FbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKrAQoORW5yb2xsUmVzcG9uc2USFQoNYmlzY3VpdF90b2tlbhgBIAEoDBIVCg1lcnJvcl9tZXNzYWdlGAIgASgJEiAKGGNvbnRyb2xfcGxhbmVfcHVibGljX2tleRgDIAEoDBIYChByb3V0ZXJfYWRkcmVzc2VzGAQgAygJEi8KC2V4cGlyZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKRAgoWQm9vdHN0cmFwRW5yb2xsUmVxdWVzdBIXCg9ib290c3RyYXBfdG9rZW4YASABKAkSDwoHcGVlcl9pZBgCIAEoCRISCgpwdWJsaWNfa2V5GAMgASgMEhYKDnJlcXVlc3RlZF9yb2xlGAQgASgJEjoKBmxhYmVscxgFIAMoCzIqLnNhbS52MS5Cb290c3RyYXBFbnJvbGxSZXF1ZXN0LkxhYmVsc0VudHJ5EhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASL9AQoXQm9vdHN0cmFwRW5yb2xsUmVzcG9uc2USKAoGc3RhdHVzGAEgASgOMhguc2FtLnYxLkVucm9sbG1lbnRTdGF0dXMSFQoNYmlzY3VpdF90b2tlbhgCIAEoDBIdChVwb2xsX2ludGVydmFsX3NlY29uZHMYAyABKAUSFQoNZXJyb3JfbWVzc2FnZRgEIAEoCRIgChhjb250cm9sX3BsYW5lX3B1YmxpY19rZXkYBSABKAwSGAoQcm91dGVyX2FkZHJlc3NlcxgGIAMoCRIvCgtleHBpcmVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUwoLU2VydmljZUluZm8SIQoEdHlwZRgBIAEoDjITLnNhbS52MS5TZXJ2aWNlVHlwZRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJInsKDkNvbW1hbmRCYWNrZW5kEg8KB2NvbW1hbmQYASADKAkSLAoDZW52GAIgAygLMh8uc2FtLnYxLkNvbW1hbmRCYWNrZW5kLkVudkVudHJ5GioKCEVudkVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiigEKFlJlZ2lzdGVyU2VydmljZVJlcXVlc3QSJAoHc2VydmljZRgBIAEoCzITLnNhbS52MS5TZXJ2aWNlSW5mbxIUCgp0YXJnZXRfdXJsGAIgASgJSAASKQoHY29tbWFuZBgDIAEoCzIWLnNhbS52MS5Db21tYW5kQmFja2VuZEgAQgkKB2JhY2tlbmQiaQoSRGlzY292ZXJlZFByb3ZpZGVyEg8KB3BlZXJfaWQYASABKAkSFwoPbG9jYWxfcHJveHlfdXJsGAIgASgJEhAKCHNydl9uYW1lGAMgASgJEhcKD3Nydl9kZXNjcmlwdGlvbhgEIAEoCSKyAgoPU2VydmljZUFubm91bmNlEg8KB3BlZXJfaWQYASABKAkSIQoEdHlwZRgCIAEoDjITLnNhbS52MS5TZXJ2aWNlVHlwZRIUCgxzZXJ2aWNlX25hbWUYAyABKAkSDAoEa2V5cxgEIAMoCRIzCgZsYWJlbHMYBSADKAsyIy5zYW0udjEuU2VydmljZUFubm91bmNlLkxhYmVsc0VudHJ5EhcKD2FjdGl2ZV9yZXF1ZXN0cxgGIAEoDRIXCg9sYXRlbmN5X2V3bWFfbXMYByABKAESMQoNYW5ub3VuY2VfdGltZRgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKHAQoYQ29udHJvbFBsYW5lSW5mb1Jlc3BvbnNlEhMKC29pZGNfaXNzdWVyGAEgASgJEhEKCWNsaWVudF9pZBgCIAEoCRIQCghhdWRpZW5jZRgDIAEoCRIYChByb3V0ZXJfYWRkcmVzc2VzGAQgAygJEhcKD2Jhbm5lZF9wZWVyX2lkcxgFIAMoCSKsAQoSUm91dGVyTGVhc2VSZXF1ZXN0Eg8KB3BlZXJfaWQYASABKAkSEQoJYWRkcmVzc2VzGAIgAygJEg8KB2Jpc2N1aXQYAyABKAwSFwoPY29ubmVjdGVkX3BlZXJzGAQgAygJEhAKCGRodF9zaXplGAUgASgFEhkKEWNoYWxsZW5nZV91bml4X21zGAYgASgDEhsKE2NoYWxsZW5nZV9zaWduYXR1cmUYByABKAwiZgoTUm91dGVyTGVhc2VSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg0KBWVycm9yGAIgASgJEi8KC2V4cGlyZV90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKeAQoKUG9saWN5Um9sZRIMCgRuYW1lGAEgASgJEhcKD2FsbG93ZWRfdGFyZ2V0cxgCIAMoCRIYChBhbGxvd2VkX3NlcnZpY2VzGAMgAygJEhYKDmN1c3RvbV9kYXRhbG9nGAQgAygJEhYKDmFsbG93ZWRfbGFiZWxzGAUgAygJEh8KBGh0dHAYBiADKAsyES5zYW0udjEuSFRUUEdyYW50IjwKCUhUVFBHcmFudBIPCgdzZXJ2aWNlGAEgASgJEg8KB21ldGhvZHMYAiADKAkSDQoFcGF0aHMYAyADKAkijwIKEUVncmVzc0Rlc3RpbmF0aW9uEgwKBG5hbWUYASABKAkSEgoKdGFyZ2V0X3VybBgCIAEoCRISCgpjcmVkZW50aWFsGAMgASgJEhEKCXNlcnZlZF9ieRgEIAMoCRIoCgZicm9rZXIYBSABKAsyGC5zYW0udjEuQ3JlZGVudGlhbEJyb2tlchImCgppbnNwZWN0aW9uGAYgASgLMhIuc2FtLnYxLkluc3BlY3Rpb24SIAoEbW9kZRgHIAEoDjISLnNhbS52MS5FZ3Jlc3NNb2RlEg0KBXBvcnRzGAggAygNEhUKDXByZXNlcnZlX2hvc3QYCSABKAgSFwoPZm9yd2FyZF9jb250ZXh0GAogASgIIjMKCkluc3BlY3Rpb24SJQoKaW5zcGVjdG9ycxgBIAMoCzIRLnNhbS52MS5JbnNwZWN0b3IiYwoJSW5zcGVjdG9yEikKC21vZGVsX2FybW9yGAEgASgLMhIuc2FtLnYxLk1vZGVsQXJtb3JIABIjCghleHRfcHJvYxgCIAEoCzIPLnNhbS52MS5FeHRQcm9jSABCBgoEa2luZCKLAQoKTW9kZWxBcm1vchIQCgh0ZW1wbGF0ZRgBIAEoCRIsCghyZXNwb25zZRgCIAEoDjIaLnNhbS52MS5SZXNwb25zZUluc3BlY3Rpb24SEQoJZmFpbF9vcGVuGAMgASgIEioKB3RpbWVvdXQYBCABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb24iggIKB0V4dFByb2MSDgoGdGFyZ2V0GAEgASgJEgoKAmNhGAIgASgJEhoKEmNsaWVudF9jZXJ0aWZpY2F0ZRgDIAEoCRI2Cg9wcm9jZXNzaW5nX21vZGUYBCABKAsyHS5zYW0udjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlEhsKE2FsbG93X21vZGVfb3ZlcnJpZGUYBSABKAgSMgoPbWVzc2FnZV90aW1lb3V0GAYgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEhoKEmZhaWx1cmVfbW9kZV9hbGxvdxgHIAEoCBIaChJtYXhfYnVmZmVyZWRfYnl0ZXMYCCABKA0i2wQKFUV4dFByb2NQcm9jZXNzaW5nTW9kZRJFChNyZXF1ZXN0X2hlYWRlcl9tb2RlGAEgASgOMiguc2FtLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEkYKFHJlc3BvbnNlX2hlYWRlcl9tb2RlGAIgASgOMiguc2FtLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEkEKEXJlcXVlc3RfYm9keV9tb2RlGAMgASgOMiYuc2FtLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5Cb2R5TW9kZRJCChJyZXNwb25zZV9ib2R5X21vZGUYBCABKA4yJi5zYW0udjEuRXh0UHJvY1Byb2Nlc3NpbmdNb2RlLkJvZHlNb2RlEkYKFHJlcXVlc3RfdHJhaWxlcl9tb2RlGAUgASgOMiguc2FtLnYxLkV4dFByb2NQcm9jZXNzaW5nTW9kZS5IZWFkZXJNb2RlEkcKFXJlc3BvbnNlX3RyYWlsZXJfbW9kZRgGIAEoDjIoLnNhbS52MS5FeHRQcm9jUHJvY2Vzc2luZ01vZGUuSGVhZGVyTW9kZSI5CgpIZWFkZXJNb2RlEhcKE0hFQURFUl9NT0RFX0RFRkFVTFQQABIICgRTRU5EEAESCAoEU0tJUBACImAKCEJvZHlNb2RlEggKBE5PTkUQABIMCghTVFJFQU1FRBABEgwKCEJVRkZFUkVEEAISFAoQQlVGRkVSRURfUEFSVElBTBADEhgKFEZVTExfRFVQTEVYX1NUUkVBTUVEEAQizwEKEENyZWRlbnRpYWxCcm9rZXISFwoNc3RhdGljX3NlY3JldBgBIAEoCUgAEjEKD29pZGNfZmVkZXJhdGlvbhgCIAEoCzIWLnNhbS52MS5PSURDRmVkZXJhdGlvbkgAEjAKD2F3c19hc3N1bWVfcm9sZRgDIAEoCzIVLnNhbS52MS5BV1NBc3N1bWVSb2xlSAASNQoRcGxhdGZvcm1faWRlbnRpdHkYBCABKAsyGC5zYW0udjEuUGxhdGZvcm1JZGVudGl0eUgAQgYKBGtpbmQiXwoOT0lEQ0ZlZGVyYXRpb24SFgoOdG9rZW5fZW5kcG9pbnQYASABKAkSEAoIYXVkaWVuY2UYAiABKAkSEwoLaW1wZXJzb25hdGUYAyABKAkSDgoGc2NvcGVzGAQgAygJIjkKDUFXU0Fzc3VtZVJvbGUSEAoIcm9sZV9hcm4YASABKAkSFgoOc2Vzc2lvbl9wb2xpY3kYAiABKAkiIgoQUGxhdGZvcm1JZGVudGl0eRIOCgZzY29wZXMYASADKAkiLgoNUG9saWN5QmluZGluZxIMCgRyb2xlGAEgASgJEg8KB21lbWJlcnMYAiADKAkihQEKDFBvbGljeUNvbmZpZxIhCgVyb2xlcxgBIAMoCzISLnNhbS52MS5Qb2xpY3lSb2xlEicKCGJpbmRpbmdzGAIgAygLMhUuc2FtLnYxLlBvbGljeUJpbmRpbmcSKQoGZWdyZXNzGAMgAygLMhkuc2FtLnYxLkVncmVzc0Rlc3RpbmF0aW9uIhgKFlBvbGljeUNvbmZpZ0dldFJlcXVlc3QiMAoXUG9saWN5Q29uZmlnR2V0UmVzcG9uc2USFQoNZGF0YWxvZ19ydWxlcxgBIAMoCSI8ChpQb2xpY3lDb25maWdVcGRhdGVSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg0KBWVycm9yGAIgASgJIhoKGEVncmVzc0Fzc2lnbm1lbnRzUmVxdWVzdCJGChlFZ3Jlc3NBc3NpZ25tZW50c1Jlc3BvbnNlEikKBmVncmVzcxgBIAMoCzIZLnNhbS52MS5FZ3Jlc3NEZXN0aW5hdGlvbiJmCgxLZXlzUmVzcG9uc2USEwoLcHVibGljX2tleXMYASADKAwSLQoJc2lnbl90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpzaWduYXR1cmVzGAMgAygMImsKE1Rva2VuUmVmcmVzaFJlcXVlc3QSGwoTY2hhbGxlbmdlX3NpZ25hdHVyZRgBIAEoDBIZChFjaGFsbGVuZ2VfdW5peF9tcxgCIAEoAxIPCgdwZWVyX2lkGAMgASgJEgsKA2p3dBgEIAEoCSJ1ChRUb2tlblJlZnJlc2hSZXNwb25zZRIVCg1iaXNjdWl0X3Rva2VuGAEgASgMEi8KC2V4cGlyZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIVCg1lcnJvcl9tZXNzYWdlGAMgASgJIjoKEU5vZGVDYXRhbG9nUmVwb3J0EiUKCHNlcnZpY2VzGAEgAygLMhMuc2FtLnYxLlNlcnZpY2VJbmZvIiUKElRva2VuUmV2b2tlUmVxdWVzdBIPCgdwZWVyX2lkGAEgASgJIjUKE1Rva2VuUmV2b2tlUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCBINCgVlcnJvchgCIAEoCSKWAQobQm9vdHN0cmFwVG9rZW5DcmVhdGVSZXF1ZXN0EgwKBHJvbGUYASABKAkSEAoIb3duZXJfaWQYAiABKAkSEQoJdHRsX2hvdXJzGAMgASgFEhIKCm1heF91c2FnZXMYBCABKAUSEwoLZGVzY3JpcHRpb24YBSABKAkSGwoTYXV0b25vbW91c19yZWNvdmVyeRgGIAEoCCKKAQocQm9vdHN0cmFwVG9rZW5DcmVhdGVSZXNwb25zZRIKCgJpZBgBIAEoCRINCgV0b2tlbhgCIAEoCRIMCgRyb2xlGAMgASgJEhAKCG93bmVyX2lkGAQgASgJEi8KC2V4cGlyZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKrAgoOQm9vdHN0cmFwVG9rZW4SCgoCaWQYASABKAkSDAoEcm9sZRgCIAEoCRIQCghvd25lcl9pZBgDIAEoCRISCgptYXhfdXNhZ2VzGAQgASgFEhQKDHVzYWdlc19jb3VudBgFIAEoBRITCgtkZXNjcmlwdGlvbhgGIAEoCRIvCgtjcmVhdGVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZXhwaXJlX3RpbWUYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi8KC3Jldm9rZV90aW1lGAkgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIbChNhdXRvbm9tb3VzX3JlY292ZXJ5GAogASgIIkQKGkJvb3RzdHJhcFRva2VuTGlzdFJlc3BvbnNlEiYKBnRva2VucxgBIAMoCzIWLnNhbS52MS5Cb290c3RyYXBUb2tlbiLKAgoRRW5yb2xsbWVudFJlcXVlc3QSCgoCaWQYASABKAkSDwoHcGVlcl9pZBgCIAEoCRIQCgh0b2tlbl9pZBgDIAEoCRIoCgZzdGF0dXMYBCABKA4yGC5zYW0udjEuRW5yb2xsbWVudFN0YXR1cxI1CgZsYWJlbHMYBSADKAsyJS5zYW0udjEuRW5yb2xsbWVudFJlcXVlc3QuTGFiZWxzRW50cnkSLwoLY3JlYXRlX3RpbWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjAKDHJlc29sdmVfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASEwoLcmVzb2x2ZWRfYnkYCCABKAkaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJMCh1FbnJvbGxtZW50UmVxdWVzdExpc3RSZXNwb25zZRIrCghyZXF1ZXN0cxgBIAMoCzIZLnNhbS52MS5FbnJvbGxtZW50UmVxdWVzdCJwCgRVc2VyEgoKAmlkGAEgASgJEg4KBmlzc3VlchgCIAEoCRINCgVlbWFpbBgDIAEoCRIMCgRyb2xlGAQgASgJEi8KC2NyZWF0ZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCLdAgoMRW5yb2xsZWROb2RlEg8KB3BlZXJfaWQYASABKAkSDAoEcm9sZRgCIAEoCRIXCg9lbnJvbGxtZW50X3R5cGUYAyABKAkSEwoLY2xhaW1zX2pzb24YBCABKAkSEAoIb3duZXJfaWQYBSABKAkSMAoGbGFiZWxzGAYgAygLMiAuc2FtLnYxLkVucm9sbGVkTm9kZS5MYWJlbHNFbnRyeRIvCgtlbnJvbGxfdGltZRgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLZXhwaXJlX3RpbWUYCCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEg4KBmJhbm5lZBgJIAEoCBIbChNhdXRvbm9tb3VzX3JlY292ZXJ5GAogASgIGi0KC0xhYmVsc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEixAEKC1JvdXRlckxlYXNlEg8KB3BlZXJfaWQYASABKAkSEQoJYWRkcmVzc2VzGAIgAygJEjUKEWxhc3RfcmVuZXdhbF90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgtleHBpcmVfdGltZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFwoPY29ubmVjdGVkX3BlZXJzGAUgAygJEhAKCGRodF9zaXplGAYgASgFImYKDE5vZGVTZXJ2aWNlcxIlCghzZXJ2aWNlcxgBIAMoCzITLnNhbS52MS5TZXJ2aWNlSW5mbxIvCgtyZXBvcnRfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiqwMKE0FkbWluU3RhdHVzUmVzcG9uc2USGwoFdXNlcnMYASADKAsyDC5zYW0udjEuVXNlchIrCg5hY3RpdmVfcm91dGVycxgCIAMoCzITLnNhbS52MS5Sb3V0ZXJMZWFzZRIsCg5lbnJvbGxlZF9ub2RlcxgDIAMoCzIULnNhbS52MS5FbnJvbGxlZE5vZGUSNgoTZW5yb2xsbWVudF9yZXF1ZXN0cxgEIAMoCzIZLnNhbS52MS5FbnJvbGxtZW50UmVxdWVzdBIwChBib290c3RyYXBfdG9rZW5zGAUgAygLMhYuc2FtLnYxLkJvb3RzdHJhcFRva2VuEiQKBnBvbGljeRgGIAEoCzIULnNhbS52MS5Qb2xpY3lDb25maWcSQgoMbm9kZV9jYXRhbG9nGAcgAygLMiwuc2FtLnYxLkFkbWluU3RhdHVzUmVzcG9uc2UuTm9kZUNhdGFsb2dFbnRyeRpIChBOb2RlQ2F0YWxvZ0VudHJ5EgsKA2tleRgBIAEoCRIjCgV2YWx1ZRgCIAEoCzIULnNhbS52MS5Ob2RlU2VydmljZXM6AjgBIuMBChJVc2VyU3RhdHVzUmVzcG9uc2USGgoEdXNlchgBIAEoCzIMLnNhbS52MS5Vc2VyEiwKDmVucm9sbGVkX25vZGVzGAIgAygLMhQuc2FtLnYxLkVucm9sbGVkTm9kZRIwChBib290c3RyYXBfdG9rZW5zGAMgAygLMhYuc2FtLnYxLkJvb3RzdHJhcFRva2VuEisKDmFjdGl2ZV9yb3V0ZXJzGAQgAygLMhMuc2FtLnYxLlJvdXRlckxlYXNlEiQKBnBvbGljeRgFIAEoCzIULnNhbS52MS5Qb2xpY3lDb25maWci5AEKGElkZW50aXR5RXZpZGVuY2VSZXNwb25zZRIPCgdwZWVyX2lkGAEgASgJEg8KB2Jpc2N1aXQYAiABKAwSNwoTYmlzY3VpdF9leHBpcmVfdGltZRgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRY29udHJvbF9wbGFuZV91cmwYBCABKAkSIgoadHJ1c3RlZF9jb250cm9sX3BsYW5lX2tleXMYBSADKAwSLgoKY2hlY2tfdGltZRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiwAIKFFBlZXJFdmlkZW5jZVJlc3BvbnNlEg8KB3BlZXJfaWQYASABKAkSDwoHYmlzY3VpdBgCIAEoDBIVCg12ZXJpZnlpbmdfa2V5GAMgASgMEg0KBXJvbGVzGAQgAygJEjgKBmxhYmVscxgFIAMoCzIoLnNhbS52MS5QZWVyRXZpZGVuY2VSZXNwb25zZS5MYWJlbHNFbnRyeRIvCgtleHBpcmVfdGltZRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASFgoOcmV2b2NhdGlvbl9pZHMYByADKAkSLgoKY2hlY2tfdGltZRgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAaLQoLTGFiZWxzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKAAgoQTWVtYmVyQ3JlZGVudGlhbBIZChFjb250cm9sX3BsYW5lX3VybBgBIAEoCRIPCgdiaXNjdWl0GAIgASgMEi8KC2V4cGlyZV90aW1lGAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgx0cnVzdGVkX2tleXMYBCADKAsyGS5zYW0udjEuVHJ1c3RlZFNpZ25pbmdLZXkSGQoRaXNzdWVkX3VuZGVyX2tleXMYBSADKAwSGAoQcm91dGVyX2FkZHJlc3NlcxgGIAMoCRIpCgxvaWRjX3Nlc3Npb24YByABKAsyEy5zYW0udjEuT0lEQ1Nlc3Npb24iWQoRVHJ1c3RlZFNpZ25pbmdLZXkSEgoKcHVibGljX2tleRgBIAEoDBIwCgxyZWNlaXZlX3RpbWUYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIlkKC09JRENTZXNzaW9uEg4KBmlzc3VlchgBIAEoCRIRCgljbGllbnRfaWQYAiABKAkSEAoIYXVkaWVuY2UYAyABKAkSFQoNcmVmcmVzaF90b2tlbhgEIAEoCSKNAQoVVGFza0F1dGhvcml6YXRpb25SdWxlEgwKBG5hbWUYASABKAkSFAoMZGlzcGxheV9uYW1lGAIgASgJEh8KBXJ1bGVzGAMgAygLMhAuc2FtLnYxLlRhc2tSdWxlEi8KC2V4cGlyZV90aW1lGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCJ+CghUYXNrUnVsZRITCgtkZXNjcmlwdGlvbhgBIAEoCRIYChBhbGxvd2VkX3NlcnZpY2VzGAIgAygJEigKCW9wZXJhdGlvbhgDIAEoCzIVLnNhbS52MS5UYXNrT3BlcmF0aW9uEhkKEWFsbG93ZWRfcmVzb3VyY2VzGAQgAygJInMKDVRhc2tPcGVyYXRpb24SFQoNYWxsb3dlZF90b29scxgBIAMoCRIXCg9hbGxvd2VkX21ldGhvZHMYAiADKAkSFQoNYWxsb3dlZF9wYXRocxgDIAMoCRIbChNhbGxvd2VkX3Blcm1pc3Npb25zGAQgAygJIqUBChRUb2tlbkV4Y2hhbmdlUmVxdWVzdBIVCg1zdWJqZWN0X3Rva2VuGAEgASgJEjAKCXRhc2tfcnVsZRgCIAEoCzIdLnNhbS52MS5UYXNrQXV0aG9yaXphdGlvblJ1bGUSDAoEc2VhbBgDIAEoCBIZChFjaGFsbGVuZ2VfdW5peF9tcxgEIAEoAxIbChNjaGFsbGVuZ2Vfc2lnbmF0dXJlGAUgASgMIn8KFVRva2VuRXhjaGFuZ2VSZXNwb25zZRIVCg1iaXNjdWl0X3Rva2VuGAEgASgMEi8KC2V4cGlyZV90aW1lGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBINCgVyb2xlcxgDIAMoCRIPCgdzdWJqZWN0GAQgASgJIoEBCg9TVFNUb2tlblJlcXVlc3QSDwoHYmlzY3VpdBgBIAEoDBITCgtkZXN0aW5hdGlvbhgCIAEoCRIQCghhdWRpZW5jZRgDIAEoCRIZChFjaGFsbGVuZ2VfdW5peF9tcxgEIAEoAxIbChNjaGFsbGVuZ2Vfc2lnbmF0dXJlGAUgASgMIoMBChBTVFNUb2tlblJlc3BvbnNlEgsKA2p3dBgBIAEoCRIvCgtleHBpcmVfdGltZRgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASDwoHc3ViamVjdBgDIAEoCRINCgVyb2xlcxgEIAMoCRIRCgl0YXNrX25hbWUYBSABKAkiRgoTUmV2b2NhdGlvbnNSZXNwb25zZRIWCg5yZXZvY2F0aW9uX2lkcxgBIAMoCRIXCg9iYW5uZWRfcGVlcl9pZHMYAiADKAkqlAEKEEVucm9sbG1lbnRTdGF0dXMSIQodRU5ST0xMTUVOVF9TVEFUVVNfVU5TUEVDSUZJRUQQABIdChlFTlJPTExNRU5UX1NUQVRVU19QRU5ESU5HEAESHgoaRU5ST0xMTUVOVF9TVEFUVVNfQVBQUk9WRUQQAhIeChpFTlJPTExNRU5UX1NUQVRVU19SRUpFQ1RFRBADKowBCgtTZXJ2aWNlVHlwZRIcChhTRVJWSUNFX1RZUEVfVU5TUEVDSUZJRUQQABIUChBTRVJWSUNFX1RZUEVfTUNQEAESGgoWU0VSVklDRV9UWVBFX0lORkVSRU5DRRACEhQKEFNFUlZJQ0VfVFlQRV9BMkEQAxIXChNTRVJWSUNFX1RZUEVfRUdSRVNTEAQqNwoKRWdyZXNzTW9kZRIUChBFR1JFU1NfTU9ERV9IVFRQEAASEwoPRUdSRVNTX01PREVfVENQEAEqXAoSUmVzcG9uc2VJbnNwZWN0aW9uEiAKHFJFU1BPTlNFX0lOU1BFQ1RJT05fQlVGRkVSRUQQABIkCiBSRVNQT05TRV9JTlNQRUNUSU9OX1JFUVVFU1RfT05MWRABQhtaGWdpdGh1Yi5jb20vZ29vZ2xlL3NhbS9hcGliBnByb3RvMw", [file_google_protobuf_duration, file_google_protobuf_timestamp]); /** * @generated from message sam.v1.AuthFrame @@ -1586,6 +1586,549 @@ export type TokenRevokeResponse = Message<"sam.v1.TokenRevokeResponse"> & { export const TokenRevokeResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_sam, 39); +/** + * BootstrapTokenCreateRequest is the body of POST /admin/bootstrap-tokens + * (admin bearer) and POST /user/bootstrap-tokens (mesh user). + * + * @generated from message sam.v1.BootstrapTokenCreateRequest + */ +export type BootstrapTokenCreateRequest = Message<"sam.v1.BootstrapTokenCreateRequest"> & { + /** + * Role the token enrolls into, e.g. "sam:role:node". Required on the admin + * endpoint; the user endpoint defaults it to "sam:role:node". + * + * @generated from field: string role = 1; + */ + role: string; + + /** + * User the token is issued on behalf of. Honored by the user endpoint + * only, and only for admins; defaults to the caller. + * + * @generated from field: string owner_id = 2; + */ + ownerId: string; + + /** + * How long the token stays valid; unset or non-positive means 24. + * + * @generated from field: int32 ttl_hours = 3; + */ + ttlHours: number; + + /** + * How many enrollments the token admits; unset or non-positive means 1. + * + * @generated from field: int32 max_usages = 4; + */ + maxUsages: number; + + /** + * Free-form operator note stored with the token. + * + * @generated from field: string description = 5; + */ + description: string; + + /** + * Copied onto every node the token enrolls: such a node may still refresh + * its credential after the control plane's signing key rotated past its + * grace period, on proof of possession of its own key alone. Admin-only, + * because a node that can always recover holds a credential that never + * expires. + * + * @generated from field: bool autonomous_recovery = 6; + */ + autonomousRecovery: boolean; +}; + +/** + * Describes the message sam.v1.BootstrapTokenCreateRequest. + * Use `create(BootstrapTokenCreateRequestSchema)` to create a new message. + */ +export const BootstrapTokenCreateRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 40); + +/** + * BootstrapTokenCreateResponse is returned (201) when a token is minted. + * token is the plaintext and is shown exactly once; the control plane keeps + * only its hash, which is also the id. + * + * @generated from message sam.v1.BootstrapTokenCreateResponse + */ +export type BootstrapTokenCreateResponse = Message<"sam.v1.BootstrapTokenCreateResponse"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string token = 2; + */ + token: string; + + /** + * @generated from field: string role = 3; + */ + role: string; + + /** + * @generated from field: string owner_id = 4; + */ + ownerId: string; + + /** + * @generated from field: google.protobuf.Timestamp expire_time = 5; + */ + expireTime?: Timestamp | undefined; +}; + +/** + * Describes the message sam.v1.BootstrapTokenCreateResponse. + * Use `create(BootstrapTokenCreateResponseSchema)` to create a new message. + */ +export const BootstrapTokenCreateResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 41); + +/** + * BootstrapToken is a minted token as operators list it. + * + * @generated from message sam.v1.BootstrapToken + */ +export type BootstrapToken = Message<"sam.v1.BootstrapToken"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string role = 2; + */ + role: string; + + /** + * @generated from field: string owner_id = 3; + */ + ownerId: string; + + /** + * @generated from field: int32 max_usages = 4; + */ + maxUsages: number; + + /** + * @generated from field: int32 usages_count = 5; + */ + usagesCount: number; + + /** + * @generated from field: string description = 6; + */ + description: string; + + /** + * @generated from field: google.protobuf.Timestamp create_time = 7; + */ + createTime?: Timestamp | undefined; + + /** + * @generated from field: google.protobuf.Timestamp expire_time = 8; + */ + expireTime?: Timestamp | undefined; + + /** + * Set when an operator revoked the token, which is distinct from expiry + * or exhausted usages. Unset means never revoked. + * + * @generated from field: google.protobuf.Timestamp revoke_time = 9; + */ + revokeTime?: Timestamp | undefined; + + /** + * @generated from field: bool autonomous_recovery = 10; + */ + autonomousRecovery: boolean; +}; + +/** + * Describes the message sam.v1.BootstrapToken. + * Use `create(BootstrapTokenSchema)` to create a new message. + */ +export const BootstrapTokenSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 42); + +/** + * @generated from message sam.v1.BootstrapTokenListResponse + */ +export type BootstrapTokenListResponse = Message<"sam.v1.BootstrapTokenListResponse"> & { + /** + * @generated from field: repeated sam.v1.BootstrapToken tokens = 1; + */ + tokens: BootstrapToken[]; +}; + +/** + * Describes the message sam.v1.BootstrapTokenListResponse. + * Use `create(BootstrapTokenListResponseSchema)` to create a new message. + */ +export const BootstrapTokenListResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 43); + +/** + * EnrollmentRequest is a bootstrap enrollment awaiting or past an operator + * decision (see BootstrapEnrollRequest). + * + * @generated from message sam.v1.EnrollmentRequest + */ +export type EnrollmentRequest = Message<"sam.v1.EnrollmentRequest"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string peer_id = 2; + */ + peerId: string; + + /** + * The bootstrap token the request was made with. + * + * @generated from field: string token_id = 3; + */ + tokenId: string; + + /** + * @generated from field: sam.v1.EnrollmentStatus status = 4; + */ + status: EnrollmentStatus; + + /** + * Labels the node declared; approval attests them into its biscuit. + * + * @generated from field: map labels = 5; + */ + labels: { [key: string]: string }; + + /** + * @generated from field: google.protobuf.Timestamp create_time = 6; + */ + createTime?: Timestamp | undefined; + + /** + * Unset while the request is pending. + * + * @generated from field: google.protobuf.Timestamp resolve_time = 7; + */ + resolveTime?: Timestamp | undefined; + + /** + * @generated from field: string resolved_by = 8; + */ + resolvedBy: string; +}; + +/** + * Describes the message sam.v1.EnrollmentRequest. + * Use `create(EnrollmentRequestSchema)` to create a new message. + */ +export const EnrollmentRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 44); + +/** + * @generated from message sam.v1.EnrollmentRequestListResponse + */ +export type EnrollmentRequestListResponse = Message<"sam.v1.EnrollmentRequestListResponse"> & { + /** + * @generated from field: repeated sam.v1.EnrollmentRequest requests = 1; + */ + requests: EnrollmentRequest[]; +}; + +/** + * Describes the message sam.v1.EnrollmentRequestListResponse. + * Use `create(EnrollmentRequestListResponseSchema)` to create a new message. + */ +export const EnrollmentRequestListResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 45); + +/** + * User is a human identity known to the mesh. + * + * @generated from message sam.v1.User + */ +export type User = Message<"sam.v1.User"> & { + /** + * The identity provider's subject. + * + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string issuer = 2; + */ + issuer: string; + + /** + * @generated from field: string email = 3; + */ + email: string; + + /** + * "admin" or "user". + * + * @generated from field: string role = 4; + */ + role: string; + + /** + * @generated from field: google.protobuf.Timestamp create_time = 5; + */ + createTime?: Timestamp | undefined; +}; + +/** + * Describes the message sam.v1.User. + * Use `create(UserSchema)` to create a new message. + */ +export const UserSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 46); + +/** + * EnrolledNode is a member's enrollment record without its credential. + * + * @generated from message sam.v1.EnrolledNode + */ +export type EnrolledNode = Message<"sam.v1.EnrolledNode"> & { + /** + * @generated from field: string peer_id = 1; + */ + peerId: string; + + /** + * @generated from field: string role = 2; + */ + role: string; + + /** + * How the node enrolled, e.g. "oidc" or "bootstrap". + * + * @generated from field: string enrollment_type = 3; + */ + enrollmentType: string; + + /** + * The identity provider's claims as stored at enrollment. Admin-only. + * + * @generated from field: string claims_json = 4; + */ + claimsJson: string; + + /** + * @generated from field: string owner_id = 5; + */ + ownerId: string; + + /** + * @generated from field: map labels = 6; + */ + labels: { [key: string]: string }; + + /** + * @generated from field: google.protobuf.Timestamp enroll_time = 7; + */ + enrollTime?: Timestamp | undefined; + + /** + * When the enrollment session ends; unset means it does not expire. + * + * @generated from field: google.protobuf.Timestamp expire_time = 8; + */ + expireTime?: Timestamp | undefined; + + /** + * @generated from field: bool banned = 9; + */ + banned: boolean; + + /** + * @generated from field: bool autonomous_recovery = 10; + */ + autonomousRecovery: boolean; +}; + +/** + * Describes the message sam.v1.EnrolledNode. + * Use `create(EnrolledNodeSchema)` to create a new message. + */ +export const EnrolledNodeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 47); + +/** + * RouterLease is a router's current registration with the control plane. + * + * @generated from message sam.v1.RouterLease + */ +export type RouterLease = Message<"sam.v1.RouterLease"> & { + /** + * @generated from field: string peer_id = 1; + */ + peerId: string; + + /** + * Multiaddrs, `/p2p/` suffixed. + * + * @generated from field: repeated string addresses = 2; + */ + addresses: string[]; + + /** + * @generated from field: google.protobuf.Timestamp last_renewal_time = 3; + */ + lastRenewalTime?: Timestamp | undefined; + + /** + * @generated from field: google.protobuf.Timestamp expire_time = 4; + */ + expireTime?: Timestamp | undefined; + + /** + * @generated from field: repeated string connected_peers = 5; + */ + connectedPeers: string[]; + + /** + * @generated from field: int32 dht_size = 6; + */ + dhtSize: number; +}; + +/** + * Describes the message sam.v1.RouterLease. + * Use `create(RouterLeaseSchema)` to create a new message. + */ +export const RouterLeaseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 48); + +/** + * NodeServices is the services one node last reported (see + * NodeCatalogReport). + * + * @generated from message sam.v1.NodeServices + */ +export type NodeServices = Message<"sam.v1.NodeServices"> & { + /** + * @generated from field: repeated sam.v1.ServiceInfo services = 1; + */ + services: ServiceInfo[]; + + /** + * @generated from field: google.protobuf.Timestamp report_time = 2; + */ + reportTime?: Timestamp | undefined; +}; + +/** + * Describes the message sam.v1.NodeServices. + * Use `create(NodeServicesSchema)` to create a new message. + */ +export const NodeServicesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 49); + +/** + * AdminStatusResponse answers GET /admin/status: everything the console + * shows an administrator. + * + * @generated from message sam.v1.AdminStatusResponse + */ +export type AdminStatusResponse = Message<"sam.v1.AdminStatusResponse"> & { + /** + * @generated from field: repeated sam.v1.User users = 1; + */ + users: User[]; + + /** + * @generated from field: repeated sam.v1.RouterLease active_routers = 2; + */ + activeRouters: RouterLease[]; + + /** + * @generated from field: repeated sam.v1.EnrolledNode enrolled_nodes = 3; + */ + enrolledNodes: EnrolledNode[]; + + /** + * @generated from field: repeated sam.v1.EnrollmentRequest enrollment_requests = 4; + */ + enrollmentRequests: EnrollmentRequest[]; + + /** + * @generated from field: repeated sam.v1.BootstrapToken bootstrap_tokens = 5; + */ + bootstrapTokens: BootstrapToken[]; + + /** + * @generated from field: sam.v1.PolicyConfig policy = 6; + */ + policy?: PolicyConfig | undefined; + + /** + * Keyed by the reporting node's peer ID; admitted nodes only. + * + * @generated from field: map node_catalog = 7; + */ + nodeCatalog: { [key: string]: NodeServices }; +}; + +/** + * Describes the message sam.v1.AdminStatusResponse. + * Use `create(AdminStatusResponseSchema)` to create a new message. + */ +export const AdminStatusResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 50); + +/** + * UserStatusResponse answers GET /user/status: the caller and what it owns. + * The router fleet and the mesh policy describe the whole mesh and are set + * for an administrator only. + * + * @generated from message sam.v1.UserStatusResponse + */ +export type UserStatusResponse = Message<"sam.v1.UserStatusResponse"> & { + /** + * @generated from field: sam.v1.User user = 1; + */ + user?: User | undefined; + + /** + * @generated from field: repeated sam.v1.EnrolledNode enrolled_nodes = 2; + */ + enrolledNodes: EnrolledNode[]; + + /** + * @generated from field: repeated sam.v1.BootstrapToken bootstrap_tokens = 3; + */ + bootstrapTokens: BootstrapToken[]; + + /** + * @generated from field: repeated sam.v1.RouterLease active_routers = 4; + */ + activeRouters: RouterLease[]; + + /** + * @generated from field: sam.v1.PolicyConfig policy = 5; + */ + policy?: PolicyConfig | undefined; +}; + +/** + * Describes the message sam.v1.UserStatusResponse. + * Use `create(UserStatusResponseSchema)` to create a new message. + */ +export const UserStatusResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_sam, 51); + /** * @generated from message sam.v1.IdentityEvidenceResponse */ @@ -1628,7 +2171,7 @@ export type IdentityEvidenceResponse = Message<"sam.v1.IdentityEvidenceResponse" * Use `create(IdentityEvidenceResponseSchema)` to create a new message. */ export const IdentityEvidenceResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 40); + messageDesc(file_sam, 52); /** * @generated from message sam.v1.PeerEvidenceResponse @@ -1684,7 +2227,7 @@ export type PeerEvidenceResponse = Message<"sam.v1.PeerEvidenceResponse"> & { * Use `create(PeerEvidenceResponseSchema)` to create a new message. */ export const PeerEvidenceResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 41); + messageDesc(file_sam, 53); /** * @generated from message sam.v1.MemberCredential @@ -1749,7 +2292,7 @@ export type MemberCredential = Message<"sam.v1.MemberCredential"> & { * Use `create(MemberCredentialSchema)` to create a new message. */ export const MemberCredentialSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 42); + messageDesc(file_sam, 54); /** * @generated from message sam.v1.TrustedSigningKey @@ -1776,7 +2319,7 @@ export type TrustedSigningKey = Message<"sam.v1.TrustedSigningKey"> & { * Use `create(TrustedSigningKeySchema)` to create a new message. */ export const TrustedSigningKeySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 43); + messageDesc(file_sam, 55); /** * @generated from message sam.v1.OIDCSession @@ -1808,7 +2351,7 @@ export type OIDCSession = Message<"sam.v1.OIDCSession"> & { * Use `create(OIDCSessionSchema)` to create a new message. */ export const OIDCSessionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 44); + messageDesc(file_sam, 56); /** * TaskAuthorizationRule narrows a credential's authority for a specific task or @@ -1852,7 +2395,7 @@ export type TaskAuthorizationRule = Message<"sam.v1.TaskAuthorizationRule"> & { * Use `create(TaskAuthorizationRuleSchema)` to create a new message. */ export const TaskAuthorizationRuleSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 45); + messageDesc(file_sam, 57); /** * @generated from message sam.v1.TaskRule @@ -1896,7 +2439,7 @@ export type TaskRule = Message<"sam.v1.TaskRule"> & { * Use `create(TaskRuleSchema)` to create a new message. */ export const TaskRuleSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 46); + messageDesc(file_sam, 58); /** * @generated from message sam.v1.TaskOperation @@ -1938,7 +2481,7 @@ export type TaskOperation = Message<"sam.v1.TaskOperation"> & { * Use `create(TaskOperationSchema)` to create a new message. */ export const TaskOperationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 47); + messageDesc(file_sam, 59); /** * TokenExchangeRequest is the body of POST /token/exchange on the control @@ -1996,7 +2539,7 @@ export type TokenExchangeRequest = Message<"sam.v1.TokenExchangeRequest"> & { * Use `create(TokenExchangeRequestSchema)` to create a new message. */ export const TokenExchangeRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 48); + messageDesc(file_sam, 60); /** * @generated from message sam.v1.TokenExchangeResponse @@ -2028,7 +2571,7 @@ export type TokenExchangeResponse = Message<"sam.v1.TokenExchangeResponse"> & { * Use `create(TokenExchangeResponseSchema)` to create a new message. */ export const TokenExchangeResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 49); + messageDesc(file_sam, 61); /** * STSTokenRequest is the body of POST /sts/token on the control plane: an @@ -2082,7 +2625,7 @@ export type STSTokenRequest = Message<"sam.v1.STSTokenRequest"> & { * Use `create(STSTokenRequestSchema)` to create a new message. */ export const STSTokenRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 50); + messageDesc(file_sam, 62); /** * @generated from message sam.v1.STSTokenResponse @@ -2119,7 +2662,7 @@ export type STSTokenResponse = Message<"sam.v1.STSTokenResponse"> & { * Use `create(STSTokenResponseSchema)` to create a new message. */ export const STSTokenResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 51); + messageDesc(file_sam, 63); /** * RevocationsResponse answers GET /revocations on the control plane: the set of @@ -2145,7 +2688,7 @@ export type RevocationsResponse = Message<"sam.v1.RevocationsResponse"> & { * Use `create(RevocationsResponseSchema)` to create a new message. */ export const RevocationsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_sam, 52); + messageDesc(file_sam, 64); /** * @generated from enum sam.v1.EnrollmentStatus diff --git a/sdk/python/src/agent_mesh/_proto/sam_pb2.py b/sdk/python/src/agent_mesh/_proto/sam_pb2.py index 913ace80..c8261ab2 100644 --- a/sdk/python/src/agent_mesh/_proto/sam_pb2.py +++ b/sdk/python/src/agent_mesh/_proto/sam_pb2.py @@ -15,7 +15,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tsam.proto\x12\x06sam.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"4\n\tAuthFrame\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x16\n\x0etarget_service\x18\x02 \x01(\t\"?\n\x0c\x41uthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\"\xd6\x01\n\tMeshEvent\x12$\n\x04type\x18\x01 \x01(\x0e\x32\x16.sam.v1.MeshEvent.Type\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12.\n\nevent_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0enew_public_key\x18\x04 \x01(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"7\n\x04Type\x12\n\n\x06\x42\x41NNED\x10\x00\x12\x10\n\x0cKEY_ROTATION\x10\x01\x12\x11\n\rPOLICY_UPDATE\x10\x02\"\xf3\x01\n\rEnrollRequest\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12\x31\n\x06labels\x18\x05 \x03(\x0b\x32!.sam.v1.EnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xab\x01\n\x0e\x45nrollResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x03 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x91\x02\n\x16\x42ootstrapEnrollRequest\x12\x17\n\x0f\x62ootstrap_token\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12:\n\x06labels\x18\x05 \x03(\x0b\x32*.sam.v1.BootstrapEnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xfd\x01\n\x17\x42ootstrapEnrollResponse\x12(\n\x06status\x18\x01 \x01(\x0e\x32\x18.sam.v1.EnrollmentStatus\x12\x15\n\rbiscuit_token\x18\x02 \x01(\x0c\x12\x1d\n\x15poll_interval_seconds\x18\x03 \x01(\x05\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x05 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"S\n\x0bServiceInfo\x12!\n\x04type\x18\x01 \x01(\x0e\x32\x13.sam.v1.ServiceType\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\"{\n\x0e\x43ommandBackend\x12\x0f\n\x07\x63ommand\x18\x01 \x03(\t\x12,\n\x03\x65nv\x18\x02 \x03(\x0b\x32\x1f.sam.v1.CommandBackend.EnvEntry\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8a\x01\n\x16RegisterServiceRequest\x12$\n\x07service\x18\x01 \x01(\x0b\x32\x13.sam.v1.ServiceInfo\x12\x14\n\ntarget_url\x18\x02 \x01(\tH\x00\x12)\n\x07\x63ommand\x18\x03 \x01(\x0b\x32\x16.sam.v1.CommandBackendH\x00\x42\t\n\x07\x62\x61\x63kend\"i\n\x12\x44iscoveredProvider\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x17\n\x0flocal_proxy_url\x18\x02 \x01(\t\x12\x10\n\x08srv_name\x18\x03 \x01(\t\x12\x17\n\x0fsrv_description\x18\x04 \x01(\t\"\xb2\x02\n\x0fServiceAnnounce\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12!\n\x04type\x18\x02 \x01(\x0e\x32\x13.sam.v1.ServiceType\x12\x14\n\x0cservice_name\x18\x03 \x01(\t\x12\x0c\n\x04keys\x18\x04 \x03(\t\x12\x33\n\x06labels\x18\x05 \x03(\x0b\x32#.sam.v1.ServiceAnnounce.LabelsEntry\x12\x17\n\x0f\x61\x63tive_requests\x18\x06 \x01(\r\x12\x17\n\x0flatency_ewma_ms\x18\x07 \x01(\x01\x12\x31\n\rannounce_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x87\x01\n\x18\x43ontrolPlaneInfoResponse\x12\x13\n\x0boidc_issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x05 \x03(\t\"\xac\x01\n\x12RouterLeaseRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\x12\x17\n\x0f\x63onnected_peers\x18\x04 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x05 \x01(\x05\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\"f\n\x13RouterLeaseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x9e\x01\n\nPolicyRole\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0f\x61llowed_targets\x18\x02 \x03(\t\x12\x18\n\x10\x61llowed_services\x18\x03 \x03(\t\x12\x16\n\x0e\x63ustom_datalog\x18\x04 \x03(\t\x12\x16\n\x0e\x61llowed_labels\x18\x05 \x03(\t\x12\x1f\n\x04http\x18\x06 \x03(\x0b\x32\x11.sam.v1.HTTPGrant\"<\n\tHTTPGrant\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x0f\n\x07methods\x18\x02 \x03(\t\x12\r\n\x05paths\x18\x03 \x03(\t\"\x8f\x02\n\x11\x45gressDestination\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ntarget_url\x18\x02 \x01(\t\x12\x12\n\ncredential\x18\x03 \x01(\t\x12\x11\n\tserved_by\x18\x04 \x03(\t\x12(\n\x06\x62roker\x18\x05 \x01(\x0b\x32\x18.sam.v1.CredentialBroker\x12&\n\ninspection\x18\x06 \x01(\x0b\x32\x12.sam.v1.Inspection\x12 \n\x04mode\x18\x07 \x01(\x0e\x32\x12.sam.v1.EgressMode\x12\r\n\x05ports\x18\x08 \x03(\r\x12\x15\n\rpreserve_host\x18\t \x01(\x08\x12\x17\n\x0f\x66orward_context\x18\n \x01(\x08\"3\n\nInspection\x12%\n\ninspectors\x18\x01 \x03(\x0b\x32\x11.sam.v1.Inspector\"c\n\tInspector\x12)\n\x0bmodel_armor\x18\x01 \x01(\x0b\x32\x12.sam.v1.ModelArmorH\x00\x12#\n\x08\x65xt_proc\x18\x02 \x01(\x0b\x32\x0f.sam.v1.ExtProcH\x00\x42\x06\n\x04kind\"\x8b\x01\n\nModelArmor\x12\x10\n\x08template\x18\x01 \x01(\t\x12,\n\x08response\x18\x02 \x01(\x0e\x32\x1a.sam.v1.ResponseInspection\x12\x11\n\tfail_open\x18\x03 \x01(\x08\x12*\n\x07timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\"\x82\x02\n\x07\x45xtProc\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\n\n\x02\x63\x61\x18\x02 \x01(\t\x12\x1a\n\x12\x63lient_certificate\x18\x03 \x01(\t\x12\x36\n\x0fprocessing_mode\x18\x04 \x01(\x0b\x32\x1d.sam.v1.ExtProcProcessingMode\x12\x1b\n\x13\x61llow_mode_override\x18\x05 \x01(\x08\x12\x32\n\x0fmessage_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x1a\n\x12\x66\x61ilure_mode_allow\x18\x07 \x01(\x08\x12\x1a\n\x12max_buffered_bytes\x18\x08 \x01(\r\"\xdb\x04\n\x15\x45xtProcProcessingMode\x12\x45\n\x13request_header_mode\x18\x01 \x01(\x0e\x32(.sam.v1.ExtProcProcessingMode.HeaderMode\x12\x46\n\x14response_header_mode\x18\x02 \x01(\x0e\x32(.sam.v1.ExtProcProcessingMode.HeaderMode\x12\x41\n\x11request_body_mode\x18\x03 \x01(\x0e\x32&.sam.v1.ExtProcProcessingMode.BodyMode\x12\x42\n\x12response_body_mode\x18\x04 \x01(\x0e\x32&.sam.v1.ExtProcProcessingMode.BodyMode\x12\x46\n\x14request_trailer_mode\x18\x05 \x01(\x0e\x32(.sam.v1.ExtProcProcessingMode.HeaderMode\x12G\n\x15response_trailer_mode\x18\x06 \x01(\x0e\x32(.sam.v1.ExtProcProcessingMode.HeaderMode\"9\n\nHeaderMode\x12\x17\n\x13HEADER_MODE_DEFAULT\x10\x00\x12\x08\n\x04SEND\x10\x01\x12\x08\n\x04SKIP\x10\x02\"`\n\x08\x42odyMode\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08STREAMED\x10\x01\x12\x0c\n\x08\x42UFFERED\x10\x02\x12\x14\n\x10\x42UFFERED_PARTIAL\x10\x03\x12\x18\n\x14\x46ULL_DUPLEX_STREAMED\x10\x04\"\xcf\x01\n\x10\x43redentialBroker\x12\x17\n\rstatic_secret\x18\x01 \x01(\tH\x00\x12\x31\n\x0foidc_federation\x18\x02 \x01(\x0b\x32\x16.sam.v1.OIDCFederationH\x00\x12\x30\n\x0f\x61ws_assume_role\x18\x03 \x01(\x0b\x32\x15.sam.v1.AWSAssumeRoleH\x00\x12\x35\n\x11platform_identity\x18\x04 \x01(\x0b\x32\x18.sam.v1.PlatformIdentityH\x00\x42\x06\n\x04kind\"_\n\x0eOIDCFederation\x12\x16\n\x0etoken_endpoint\x18\x01 \x01(\t\x12\x10\n\x08\x61udience\x18\x02 \x01(\t\x12\x13\n\x0bimpersonate\x18\x03 \x01(\t\x12\x0e\n\x06scopes\x18\x04 \x03(\t\"9\n\rAWSAssumeRole\x12\x10\n\x08role_arn\x18\x01 \x01(\t\x12\x16\n\x0esession_policy\x18\x02 \x01(\t\"\"\n\x10PlatformIdentity\x12\x0e\n\x06scopes\x18\x01 \x03(\t\".\n\rPolicyBinding\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07members\x18\x02 \x03(\t\"\x85\x01\n\x0cPolicyConfig\x12!\n\x05roles\x18\x01 \x03(\x0b\x32\x12.sam.v1.PolicyRole\x12\'\n\x08\x62indings\x18\x02 \x03(\x0b\x32\x15.sam.v1.PolicyBinding\x12)\n\x06\x65gress\x18\x03 \x03(\x0b\x32\x19.sam.v1.EgressDestination\"\x18\n\x16PolicyConfigGetRequest\"0\n\x17PolicyConfigGetResponse\x12\x15\n\rdatalog_rules\x18\x01 \x03(\t\"<\n\x1aPolicyConfigUpdateResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1a\n\x18\x45gressAssignmentsRequest\"F\n\x19\x45gressAssignmentsResponse\x12)\n\x06\x65gress\x18\x01 \x03(\x0b\x32\x19.sam.v1.EgressDestination\"f\n\x0cKeysResponse\x12\x13\n\x0bpublic_keys\x18\x01 \x03(\x0c\x12-\n\tsign_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nsignatures\x18\x03 \x03(\x0c\"k\n\x13TokenRefreshRequest\x12\x1b\n\x13\x63hallenge_signature\x18\x01 \x01(\x0c\x12\x19\n\x11\x63hallenge_unix_ms\x18\x02 \x01(\x03\x12\x0f\n\x07peer_id\x18\x03 \x01(\t\x12\x0b\n\x03jwt\x18\x04 \x01(\t\"u\n\x14TokenRefreshResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rerror_message\x18\x03 \x01(\t\":\n\x11NodeCatalogReport\x12%\n\x08services\x18\x01 \x03(\x0b\x32\x13.sam.v1.ServiceInfo\"%\n\x12TokenRevokeRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"5\n\x13TokenRevokeResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\xe4\x01\n\x18IdentityEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x37\n\x13\x62iscuit_expire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x19\n\x11\x63ontrol_plane_url\x18\x04 \x01(\t\x12\"\n\x1atrusted_control_plane_keys\x18\x05 \x03(\x0c\x12.\n\ncheck_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc0\x02\n\x14PeerEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x15\n\rverifying_key\x18\x03 \x01(\x0c\x12\r\n\x05roles\x18\x04 \x03(\t\x12\x38\n\x06labels\x18\x05 \x03(\x0b\x32(.sam.v1.PeerEvidenceResponse.LabelsEntry\x12/\n\x0b\x65xpire_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0erevocation_ids\x18\x07 \x03(\t\x12.\n\ncheck_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x80\x02\n\x10MemberCredential\x12\x19\n\x11\x63ontrol_plane_url\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0ctrusted_keys\x18\x04 \x03(\x0b\x32\x19.sam.v1.TrustedSigningKey\x12\x19\n\x11issued_under_keys\x18\x05 \x03(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12)\n\x0coidc_session\x18\x07 \x01(\x0b\x32\x13.sam.v1.OIDCSession\"Y\n\x11TrustedSigningKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x30\n\x0creceive_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bOIDCSession\x12\x0e\n\x06issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x15\n\rrefresh_token\x18\x04 \x01(\t\"\x8d\x01\n\x15TaskAuthorizationRule\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x1f\n\x05rules\x18\x03 \x03(\x0b\x32\x10.sam.v1.TaskRule\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"~\n\x08TaskRule\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x18\n\x10\x61llowed_services\x18\x02 \x03(\t\x12(\n\toperation\x18\x03 \x01(\x0b\x32\x15.sam.v1.TaskOperation\x12\x19\n\x11\x61llowed_resources\x18\x04 \x03(\t\"s\n\rTaskOperation\x12\x15\n\rallowed_tools\x18\x01 \x03(\t\x12\x17\n\x0f\x61llowed_methods\x18\x02 \x03(\t\x12\x15\n\rallowed_paths\x18\x03 \x03(\t\x12\x1b\n\x13\x61llowed_permissions\x18\x04 \x03(\t\"\xa5\x01\n\x14TokenExchangeRequest\x12\x15\n\rsubject_token\x18\x01 \x01(\t\x12\x30\n\ttask_rule\x18\x02 \x01(\x0b\x32\x1d.sam.v1.TaskAuthorizationRule\x12\x0c\n\x04seal\x18\x03 \x01(\x08\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x7f\n\x15TokenExchangeResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\r\n\x05roles\x18\x03 \x03(\t\x12\x0f\n\x07subject\x18\x04 \x01(\t\"\x81\x01\n\x0fSTSTokenRequest\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x83\x01\n\x10STSTokenResponse\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07subject\x18\x03 \x01(\t\x12\r\n\x05roles\x18\x04 \x03(\t\x12\x11\n\ttask_name\x18\x05 \x01(\t\"F\n\x13RevocationsResponse\x12\x16\n\x0erevocation_ids\x18\x01 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x02 \x03(\t*\x94\x01\n\x10\x45nrollmentStatus\x12!\n\x1d\x45NROLLMENT_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x45NROLLMENT_STATUS_PENDING\x10\x01\x12\x1e\n\x1a\x45NROLLMENT_STATUS_APPROVED\x10\x02\x12\x1e\n\x1a\x45NROLLMENT_STATUS_REJECTED\x10\x03*\x8c\x01\n\x0bServiceType\x12\x1c\n\x18SERVICE_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10SERVICE_TYPE_MCP\x10\x01\x12\x1a\n\x16SERVICE_TYPE_INFERENCE\x10\x02\x12\x14\n\x10SERVICE_TYPE_A2A\x10\x03\x12\x17\n\x13SERVICE_TYPE_EGRESS\x10\x04*7\n\nEgressMode\x12\x14\n\x10\x45GRESS_MODE_HTTP\x10\x00\x12\x13\n\x0f\x45GRESS_MODE_TCP\x10\x01*\\\n\x12ResponseInspection\x12 \n\x1cRESPONSE_INSPECTION_BUFFERED\x10\x00\x12$\n RESPONSE_INSPECTION_REQUEST_ONLY\x10\x01\x42\x1bZ\x19github.com/google/sam/apib\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tsam.proto\x12\x06sam.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"4\n\tAuthFrame\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x16\n\x0etarget_service\x18\x02 \x01(\t\"?\n\x0c\x41uthResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\"\xd6\x01\n\tMeshEvent\x12$\n\x04type\x18\x01 \x01(\x0e\x32\x16.sam.v1.MeshEvent.Type\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12.\n\nevent_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0enew_public_key\x18\x04 \x01(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"7\n\x04Type\x12\n\n\x06\x42\x41NNED\x10\x00\x12\x10\n\x0cKEY_ROTATION\x10\x01\x12\x11\n\rPOLICY_UPDATE\x10\x02\"\xf3\x01\n\rEnrollRequest\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12\x31\n\x06labels\x18\x05 \x03(\x0b\x32!.sam.v1.EnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xab\x01\n\x0e\x45nrollResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x03 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x91\x02\n\x16\x42ootstrapEnrollRequest\x12\x17\n\x0f\x62ootstrap_token\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x12\n\npublic_key\x18\x03 \x01(\x0c\x12\x16\n\x0erequested_role\x18\x04 \x01(\t\x12:\n\x06labels\x18\x05 \x03(\x0b\x32*.sam.v1.BootstrapEnrollRequest.LabelsEntry\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xfd\x01\n\x17\x42ootstrapEnrollResponse\x12(\n\x06status\x18\x01 \x01(\x0e\x32\x18.sam.v1.EnrollmentStatus\x12\x15\n\rbiscuit_token\x18\x02 \x01(\x0c\x12\x1d\n\x15poll_interval_seconds\x18\x03 \x01(\x05\x12\x15\n\rerror_message\x18\x04 \x01(\t\x12 \n\x18\x63ontrol_plane_public_key\x18\x05 \x01(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12/\n\x0b\x65xpire_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"S\n\x0bServiceInfo\x12!\n\x04type\x18\x01 \x01(\x0e\x32\x13.sam.v1.ServiceType\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\"{\n\x0e\x43ommandBackend\x12\x0f\n\x07\x63ommand\x18\x01 \x03(\t\x12,\n\x03\x65nv\x18\x02 \x03(\x0b\x32\x1f.sam.v1.CommandBackend.EnvEntry\x1a*\n\x08\x45nvEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x8a\x01\n\x16RegisterServiceRequest\x12$\n\x07service\x18\x01 \x01(\x0b\x32\x13.sam.v1.ServiceInfo\x12\x14\n\ntarget_url\x18\x02 \x01(\tH\x00\x12)\n\x07\x63ommand\x18\x03 \x01(\x0b\x32\x16.sam.v1.CommandBackendH\x00\x42\t\n\x07\x62\x61\x63kend\"i\n\x12\x44iscoveredProvider\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x17\n\x0flocal_proxy_url\x18\x02 \x01(\t\x12\x10\n\x08srv_name\x18\x03 \x01(\t\x12\x17\n\x0fsrv_description\x18\x04 \x01(\t\"\xb2\x02\n\x0fServiceAnnounce\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12!\n\x04type\x18\x02 \x01(\x0e\x32\x13.sam.v1.ServiceType\x12\x14\n\x0cservice_name\x18\x03 \x01(\t\x12\x0c\n\x04keys\x18\x04 \x03(\t\x12\x33\n\x06labels\x18\x05 \x03(\x0b\x32#.sam.v1.ServiceAnnounce.LabelsEntry\x12\x17\n\x0f\x61\x63tive_requests\x18\x06 \x01(\r\x12\x17\n\x0flatency_ewma_ms\x18\x07 \x01(\x01\x12\x31\n\rannounce_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x87\x01\n\x18\x43ontrolPlaneInfoResponse\x12\x13\n\x0boidc_issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x18\n\x10router_addresses\x18\x04 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x05 \x03(\t\"\xac\x01\n\x12RouterLeaseRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x0f\n\x07\x62iscuit\x18\x03 \x01(\x0c\x12\x17\n\x0f\x63onnected_peers\x18\x04 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x05 \x01(\x05\x12\x19\n\x11\x63hallenge_unix_ms\x18\x06 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x07 \x01(\x0c\"f\n\x13RouterLeaseResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x9e\x01\n\nPolicyRole\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x17\n\x0f\x61llowed_targets\x18\x02 \x03(\t\x12\x18\n\x10\x61llowed_services\x18\x03 \x03(\t\x12\x16\n\x0e\x63ustom_datalog\x18\x04 \x03(\t\x12\x16\n\x0e\x61llowed_labels\x18\x05 \x03(\t\x12\x1f\n\x04http\x18\x06 \x03(\x0b\x32\x11.sam.v1.HTTPGrant\"<\n\tHTTPGrant\x12\x0f\n\x07service\x18\x01 \x01(\t\x12\x0f\n\x07methods\x18\x02 \x03(\t\x12\r\n\x05paths\x18\x03 \x03(\t\"\x8f\x02\n\x11\x45gressDestination\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\ntarget_url\x18\x02 \x01(\t\x12\x12\n\ncredential\x18\x03 \x01(\t\x12\x11\n\tserved_by\x18\x04 \x03(\t\x12(\n\x06\x62roker\x18\x05 \x01(\x0b\x32\x18.sam.v1.CredentialBroker\x12&\n\ninspection\x18\x06 \x01(\x0b\x32\x12.sam.v1.Inspection\x12 \n\x04mode\x18\x07 \x01(\x0e\x32\x12.sam.v1.EgressMode\x12\r\n\x05ports\x18\x08 \x03(\r\x12\x15\n\rpreserve_host\x18\t \x01(\x08\x12\x17\n\x0f\x66orward_context\x18\n \x01(\x08\"3\n\nInspection\x12%\n\ninspectors\x18\x01 \x03(\x0b\x32\x11.sam.v1.Inspector\"c\n\tInspector\x12)\n\x0bmodel_armor\x18\x01 \x01(\x0b\x32\x12.sam.v1.ModelArmorH\x00\x12#\n\x08\x65xt_proc\x18\x02 \x01(\x0b\x32\x0f.sam.v1.ExtProcH\x00\x42\x06\n\x04kind\"\x8b\x01\n\nModelArmor\x12\x10\n\x08template\x18\x01 \x01(\t\x12,\n\x08response\x18\x02 \x01(\x0e\x32\x1a.sam.v1.ResponseInspection\x12\x11\n\tfail_open\x18\x03 \x01(\x08\x12*\n\x07timeout\x18\x04 \x01(\x0b\x32\x19.google.protobuf.Duration\"\x82\x02\n\x07\x45xtProc\x12\x0e\n\x06target\x18\x01 \x01(\t\x12\n\n\x02\x63\x61\x18\x02 \x01(\t\x12\x1a\n\x12\x63lient_certificate\x18\x03 \x01(\t\x12\x36\n\x0fprocessing_mode\x18\x04 \x01(\x0b\x32\x1d.sam.v1.ExtProcProcessingMode\x12\x1b\n\x13\x61llow_mode_override\x18\x05 \x01(\x08\x12\x32\n\x0fmessage_timeout\x18\x06 \x01(\x0b\x32\x19.google.protobuf.Duration\x12\x1a\n\x12\x66\x61ilure_mode_allow\x18\x07 \x01(\x08\x12\x1a\n\x12max_buffered_bytes\x18\x08 \x01(\r\"\xdb\x04\n\x15\x45xtProcProcessingMode\x12\x45\n\x13request_header_mode\x18\x01 \x01(\x0e\x32(.sam.v1.ExtProcProcessingMode.HeaderMode\x12\x46\n\x14response_header_mode\x18\x02 \x01(\x0e\x32(.sam.v1.ExtProcProcessingMode.HeaderMode\x12\x41\n\x11request_body_mode\x18\x03 \x01(\x0e\x32&.sam.v1.ExtProcProcessingMode.BodyMode\x12\x42\n\x12response_body_mode\x18\x04 \x01(\x0e\x32&.sam.v1.ExtProcProcessingMode.BodyMode\x12\x46\n\x14request_trailer_mode\x18\x05 \x01(\x0e\x32(.sam.v1.ExtProcProcessingMode.HeaderMode\x12G\n\x15response_trailer_mode\x18\x06 \x01(\x0e\x32(.sam.v1.ExtProcProcessingMode.HeaderMode\"9\n\nHeaderMode\x12\x17\n\x13HEADER_MODE_DEFAULT\x10\x00\x12\x08\n\x04SEND\x10\x01\x12\x08\n\x04SKIP\x10\x02\"`\n\x08\x42odyMode\x12\x08\n\x04NONE\x10\x00\x12\x0c\n\x08STREAMED\x10\x01\x12\x0c\n\x08\x42UFFERED\x10\x02\x12\x14\n\x10\x42UFFERED_PARTIAL\x10\x03\x12\x18\n\x14\x46ULL_DUPLEX_STREAMED\x10\x04\"\xcf\x01\n\x10\x43redentialBroker\x12\x17\n\rstatic_secret\x18\x01 \x01(\tH\x00\x12\x31\n\x0foidc_federation\x18\x02 \x01(\x0b\x32\x16.sam.v1.OIDCFederationH\x00\x12\x30\n\x0f\x61ws_assume_role\x18\x03 \x01(\x0b\x32\x15.sam.v1.AWSAssumeRoleH\x00\x12\x35\n\x11platform_identity\x18\x04 \x01(\x0b\x32\x18.sam.v1.PlatformIdentityH\x00\x42\x06\n\x04kind\"_\n\x0eOIDCFederation\x12\x16\n\x0etoken_endpoint\x18\x01 \x01(\t\x12\x10\n\x08\x61udience\x18\x02 \x01(\t\x12\x13\n\x0bimpersonate\x18\x03 \x01(\t\x12\x0e\n\x06scopes\x18\x04 \x03(\t\"9\n\rAWSAssumeRole\x12\x10\n\x08role_arn\x18\x01 \x01(\t\x12\x16\n\x0esession_policy\x18\x02 \x01(\t\"\"\n\x10PlatformIdentity\x12\x0e\n\x06scopes\x18\x01 \x03(\t\".\n\rPolicyBinding\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07members\x18\x02 \x03(\t\"\x85\x01\n\x0cPolicyConfig\x12!\n\x05roles\x18\x01 \x03(\x0b\x32\x12.sam.v1.PolicyRole\x12\'\n\x08\x62indings\x18\x02 \x03(\x0b\x32\x15.sam.v1.PolicyBinding\x12)\n\x06\x65gress\x18\x03 \x03(\x0b\x32\x19.sam.v1.EgressDestination\"\x18\n\x16PolicyConfigGetRequest\"0\n\x17PolicyConfigGetResponse\x12\x15\n\rdatalog_rules\x18\x01 \x03(\t\"<\n\x1aPolicyConfigUpdateResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x1a\n\x18\x45gressAssignmentsRequest\"F\n\x19\x45gressAssignmentsResponse\x12)\n\x06\x65gress\x18\x01 \x03(\x0b\x32\x19.sam.v1.EgressDestination\"f\n\x0cKeysResponse\x12\x13\n\x0bpublic_keys\x18\x01 \x03(\x0c\x12-\n\tsign_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nsignatures\x18\x03 \x03(\x0c\"k\n\x13TokenRefreshRequest\x12\x1b\n\x13\x63hallenge_signature\x18\x01 \x01(\x0c\x12\x19\n\x11\x63hallenge_unix_ms\x18\x02 \x01(\x03\x12\x0f\n\x07peer_id\x18\x03 \x01(\t\x12\x0b\n\x03jwt\x18\x04 \x01(\t\"u\n\x14TokenRefreshResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x15\n\rerror_message\x18\x03 \x01(\t\":\n\x11NodeCatalogReport\x12%\n\x08services\x18\x01 \x03(\x0b\x32\x13.sam.v1.ServiceInfo\"%\n\x12TokenRevokeRequest\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\"5\n\x13TokenRevokeResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"\x96\x01\n\x1b\x42ootstrapTokenCreateRequest\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x10\n\x08owner_id\x18\x02 \x01(\t\x12\x11\n\tttl_hours\x18\x03 \x01(\x05\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\x12\x1b\n\x13\x61utonomous_recovery\x18\x06 \x01(\x08\"\x8a\x01\n\x1c\x42ootstrapTokenCreateResponse\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0c\n\x04role\x18\x03 \x01(\t\x12\x10\n\x08owner_id\x18\x04 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xab\x02\n\x0e\x42ootstrapToken\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x10\n\x08owner_id\x18\x03 \x01(\t\x12\x12\n\nmax_usages\x18\x04 \x01(\x05\x12\x14\n\x0cusages_count\x18\x05 \x01(\x05\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12/\n\x0b\x63reate_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0brevoke_time\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\"D\n\x1a\x42ootstrapTokenListResponse\x12&\n\x06tokens\x18\x01 \x03(\x0b\x32\x16.sam.v1.BootstrapToken\"\xca\x02\n\x11\x45nrollmentRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07peer_id\x18\x02 \x01(\t\x12\x10\n\x08token_id\x18\x03 \x01(\t\x12(\n\x06status\x18\x04 \x01(\x0e\x32\x18.sam.v1.EnrollmentStatus\x12\x35\n\x06labels\x18\x05 \x03(\x0b\x32%.sam.v1.EnrollmentRequest.LabelsEntry\x12/\n\x0b\x63reate_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0cresolve_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x13\n\x0bresolved_by\x18\x08 \x01(\t\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"L\n\x1d\x45nrollmentRequestListResponse\x12+\n\x08requests\x18\x01 \x03(\x0b\x32\x19.sam.v1.EnrollmentRequest\"p\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06issuer\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x0c\n\x04role\x18\x04 \x01(\t\x12/\n\x0b\x63reate_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xdd\x02\n\x0c\x45nrolledNode\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x17\n\x0f\x65nrollment_type\x18\x03 \x01(\t\x12\x13\n\x0b\x63laims_json\x18\x04 \x01(\t\x12\x10\n\x08owner_id\x18\x05 \x01(\t\x12\x30\n\x06labels\x18\x06 \x03(\x0b\x32 .sam.v1.EnrolledNode.LabelsEntry\x12/\n\x0b\x65nroll_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x62\x61nned\x18\t \x01(\x08\x12\x1b\n\x13\x61utonomous_recovery\x18\n \x01(\x08\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc4\x01\n\x0bRouterLease\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x11\n\taddresses\x18\x02 \x03(\t\x12\x35\n\x11last_renewal_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\x0f\x63onnected_peers\x18\x05 \x03(\t\x12\x10\n\x08\x64ht_size\x18\x06 \x01(\x05\"f\n\x0cNodeServices\x12%\n\x08services\x18\x01 \x03(\x0b\x32\x13.sam.v1.ServiceInfo\x12/\n\x0breport_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xab\x03\n\x13\x41\x64minStatusResponse\x12\x1b\n\x05users\x18\x01 \x03(\x0b\x32\x0c.sam.v1.User\x12+\n\x0e\x61\x63tive_routers\x18\x02 \x03(\x0b\x32\x13.sam.v1.RouterLease\x12,\n\x0e\x65nrolled_nodes\x18\x03 \x03(\x0b\x32\x14.sam.v1.EnrolledNode\x12\x36\n\x13\x65nrollment_requests\x18\x04 \x03(\x0b\x32\x19.sam.v1.EnrollmentRequest\x12\x30\n\x10\x62ootstrap_tokens\x18\x05 \x03(\x0b\x32\x16.sam.v1.BootstrapToken\x12$\n\x06policy\x18\x06 \x01(\x0b\x32\x14.sam.v1.PolicyConfig\x12\x42\n\x0cnode_catalog\x18\x07 \x03(\x0b\x32,.sam.v1.AdminStatusResponse.NodeCatalogEntry\x1aH\n\x10NodeCatalogEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.sam.v1.NodeServices:\x02\x38\x01\"\xe3\x01\n\x12UserStatusResponse\x12\x1a\n\x04user\x18\x01 \x01(\x0b\x32\x0c.sam.v1.User\x12,\n\x0e\x65nrolled_nodes\x18\x02 \x03(\x0b\x32\x14.sam.v1.EnrolledNode\x12\x30\n\x10\x62ootstrap_tokens\x18\x03 \x03(\x0b\x32\x16.sam.v1.BootstrapToken\x12+\n\x0e\x61\x63tive_routers\x18\x04 \x03(\x0b\x32\x13.sam.v1.RouterLease\x12$\n\x06policy\x18\x05 \x01(\x0b\x32\x14.sam.v1.PolicyConfig\"\xe4\x01\n\x18IdentityEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x37\n\x13\x62iscuit_expire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x19\n\x11\x63ontrol_plane_url\x18\x04 \x01(\t\x12\"\n\x1atrusted_control_plane_keys\x18\x05 \x03(\x0c\x12.\n\ncheck_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc0\x02\n\x14PeerEvidenceResponse\x12\x0f\n\x07peer_id\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12\x15\n\rverifying_key\x18\x03 \x01(\x0c\x12\r\n\x05roles\x18\x04 \x03(\t\x12\x38\n\x06labels\x18\x05 \x03(\x0b\x32(.sam.v1.PeerEvidenceResponse.LabelsEntry\x12/\n\x0b\x65xpire_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x16\n\x0erevocation_ids\x18\x07 \x03(\t\x12.\n\ncheck_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x80\x02\n\x10MemberCredential\x12\x19\n\x11\x63ontrol_plane_url\x18\x01 \x01(\t\x12\x0f\n\x07\x62iscuit\x18\x02 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12/\n\x0ctrusted_keys\x18\x04 \x03(\x0b\x32\x19.sam.v1.TrustedSigningKey\x12\x19\n\x11issued_under_keys\x18\x05 \x03(\x0c\x12\x18\n\x10router_addresses\x18\x06 \x03(\t\x12)\n\x0coidc_session\x18\x07 \x01(\x0b\x32\x13.sam.v1.OIDCSession\"Y\n\x11TrustedSigningKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x30\n\x0creceive_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"Y\n\x0bOIDCSession\x12\x0e\n\x06issuer\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x15\n\rrefresh_token\x18\x04 \x01(\t\"\x8d\x01\n\x15TaskAuthorizationRule\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x1f\n\x05rules\x18\x03 \x03(\x0b\x32\x10.sam.v1.TaskRule\x12/\n\x0b\x65xpire_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"~\n\x08TaskRule\x12\x13\n\x0b\x64\x65scription\x18\x01 \x01(\t\x12\x18\n\x10\x61llowed_services\x18\x02 \x03(\t\x12(\n\toperation\x18\x03 \x01(\x0b\x32\x15.sam.v1.TaskOperation\x12\x19\n\x11\x61llowed_resources\x18\x04 \x03(\t\"s\n\rTaskOperation\x12\x15\n\rallowed_tools\x18\x01 \x03(\t\x12\x17\n\x0f\x61llowed_methods\x18\x02 \x03(\t\x12\x15\n\rallowed_paths\x18\x03 \x03(\t\x12\x1b\n\x13\x61llowed_permissions\x18\x04 \x03(\t\"\xa5\x01\n\x14TokenExchangeRequest\x12\x15\n\rsubject_token\x18\x01 \x01(\t\x12\x30\n\ttask_rule\x18\x02 \x01(\x0b\x32\x1d.sam.v1.TaskAuthorizationRule\x12\x0c\n\x04seal\x18\x03 \x01(\x08\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x7f\n\x15TokenExchangeResponse\x12\x15\n\rbiscuit_token\x18\x01 \x01(\x0c\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\r\n\x05roles\x18\x03 \x03(\t\x12\x0f\n\x07subject\x18\x04 \x01(\t\"\x81\x01\n\x0fSTSTokenRequest\x12\x0f\n\x07\x62iscuit\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x10\n\x08\x61udience\x18\x03 \x01(\t\x12\x19\n\x11\x63hallenge_unix_ms\x18\x04 \x01(\x03\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\"\x83\x01\n\x10STSTokenResponse\x12\x0b\n\x03jwt\x18\x01 \x01(\t\x12/\n\x0b\x65xpire_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0f\n\x07subject\x18\x03 \x01(\t\x12\r\n\x05roles\x18\x04 \x03(\t\x12\x11\n\ttask_name\x18\x05 \x01(\t\"F\n\x13RevocationsResponse\x12\x16\n\x0erevocation_ids\x18\x01 \x03(\t\x12\x17\n\x0f\x62\x61nned_peer_ids\x18\x02 \x03(\t*\x94\x01\n\x10\x45nrollmentStatus\x12!\n\x1d\x45NROLLMENT_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x45NROLLMENT_STATUS_PENDING\x10\x01\x12\x1e\n\x1a\x45NROLLMENT_STATUS_APPROVED\x10\x02\x12\x1e\n\x1a\x45NROLLMENT_STATUS_REJECTED\x10\x03*\x8c\x01\n\x0bServiceType\x12\x1c\n\x18SERVICE_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10SERVICE_TYPE_MCP\x10\x01\x12\x1a\n\x16SERVICE_TYPE_INFERENCE\x10\x02\x12\x14\n\x10SERVICE_TYPE_A2A\x10\x03\x12\x17\n\x13SERVICE_TYPE_EGRESS\x10\x04*7\n\nEgressMode\x12\x14\n\x10\x45GRESS_MODE_HTTP\x10\x00\x12\x13\n\x0f\x45GRESS_MODE_TCP\x10\x01*\\\n\x12ResponseInspection\x12 \n\x1cRESPONSE_INSPECTION_BUFFERED\x10\x00\x12$\n RESPONSE_INSPECTION_REQUEST_ONLY\x10\x01\x42\x1bZ\x19github.com/google/sam/apib\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'sam_pb2', globals()) @@ -31,16 +31,22 @@ _COMMANDBACKEND_ENVENTRY._serialized_options = b'8\001' _SERVICEANNOUNCE_LABELSENTRY._options = None _SERVICEANNOUNCE_LABELSENTRY._serialized_options = b'8\001' + _ENROLLMENTREQUEST_LABELSENTRY._options = None + _ENROLLMENTREQUEST_LABELSENTRY._serialized_options = b'8\001' + _ENROLLEDNODE_LABELSENTRY._options = None + _ENROLLEDNODE_LABELSENTRY._serialized_options = b'8\001' + _ADMINSTATUSRESPONSE_NODECATALOGENTRY._options = None + _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_options = b'8\001' _PEEREVIDENCERESPONSE_LABELSENTRY._options = None _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_options = b'8\001' - _ENROLLMENTSTATUS._serialized_start=7548 - _ENROLLMENTSTATUS._serialized_end=7696 - _SERVICETYPE._serialized_start=7699 - _SERVICETYPE._serialized_end=7839 - _EGRESSMODE._serialized_start=7841 - _EGRESSMODE._serialized_end=7896 - _RESPONSEINSPECTION._serialized_start=7898 - _RESPONSEINSPECTION._serialized_end=7990 + _ENROLLMENTSTATUS._serialized_start=10054 + _ENROLLMENTSTATUS._serialized_end=10202 + _SERVICETYPE._serialized_start=10205 + _SERVICETYPE._serialized_end=10345 + _EGRESSMODE._serialized_start=10347 + _EGRESSMODE._serialized_end=10402 + _RESPONSEINSPECTION._serialized_start=10404 + _RESPONSEINSPECTION._serialized_end=10496 _AUTHFRAME._serialized_start=86 _AUTHFRAME._serialized_end=138 _AUTHRESPONSE._serialized_start=140 @@ -135,32 +141,62 @@ _TOKENREVOKEREQUEST._serialized_end=5471 _TOKENREVOKERESPONSE._serialized_start=5473 _TOKENREVOKERESPONSE._serialized_end=5526 - _IDENTITYEVIDENCERESPONSE._serialized_start=5529 - _IDENTITYEVIDENCERESPONSE._serialized_end=5757 - _PEEREVIDENCERESPONSE._serialized_start=5760 - _PEEREVIDENCERESPONSE._serialized_end=6080 + _BOOTSTRAPTOKENCREATEREQUEST._serialized_start=5529 + _BOOTSTRAPTOKENCREATEREQUEST._serialized_end=5679 + _BOOTSTRAPTOKENCREATERESPONSE._serialized_start=5682 + _BOOTSTRAPTOKENCREATERESPONSE._serialized_end=5820 + _BOOTSTRAPTOKEN._serialized_start=5823 + _BOOTSTRAPTOKEN._serialized_end=6122 + _BOOTSTRAPTOKENLISTRESPONSE._serialized_start=6124 + _BOOTSTRAPTOKENLISTRESPONSE._serialized_end=6192 + _ENROLLMENTREQUEST._serialized_start=6195 + _ENROLLMENTREQUEST._serialized_end=6525 + _ENROLLMENTREQUEST_LABELSENTRY._serialized_start=621 + _ENROLLMENTREQUEST_LABELSENTRY._serialized_end=666 + _ENROLLMENTREQUESTLISTRESPONSE._serialized_start=6527 + _ENROLLMENTREQUESTLISTRESPONSE._serialized_end=6603 + _USER._serialized_start=6605 + _USER._serialized_end=6717 + _ENROLLEDNODE._serialized_start=6720 + _ENROLLEDNODE._serialized_end=7069 + _ENROLLEDNODE_LABELSENTRY._serialized_start=621 + _ENROLLEDNODE_LABELSENTRY._serialized_end=666 + _ROUTERLEASE._serialized_start=7072 + _ROUTERLEASE._serialized_end=7268 + _NODESERVICES._serialized_start=7270 + _NODESERVICES._serialized_end=7372 + _ADMINSTATUSRESPONSE._serialized_start=7375 + _ADMINSTATUSRESPONSE._serialized_end=7802 + _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_start=7730 + _ADMINSTATUSRESPONSE_NODECATALOGENTRY._serialized_end=7802 + _USERSTATUSRESPONSE._serialized_start=7805 + _USERSTATUSRESPONSE._serialized_end=8032 + _IDENTITYEVIDENCERESPONSE._serialized_start=8035 + _IDENTITYEVIDENCERESPONSE._serialized_end=8263 + _PEEREVIDENCERESPONSE._serialized_start=8266 + _PEEREVIDENCERESPONSE._serialized_end=8586 _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_start=621 _PEEREVIDENCERESPONSE_LABELSENTRY._serialized_end=666 - _MEMBERCREDENTIAL._serialized_start=6083 - _MEMBERCREDENTIAL._serialized_end=6339 - _TRUSTEDSIGNINGKEY._serialized_start=6341 - _TRUSTEDSIGNINGKEY._serialized_end=6430 - _OIDCSESSION._serialized_start=6432 - _OIDCSESSION._serialized_end=6521 - _TASKAUTHORIZATIONRULE._serialized_start=6524 - _TASKAUTHORIZATIONRULE._serialized_end=6665 - _TASKRULE._serialized_start=6667 - _TASKRULE._serialized_end=6793 - _TASKOPERATION._serialized_start=6795 - _TASKOPERATION._serialized_end=6910 - _TOKENEXCHANGEREQUEST._serialized_start=6913 - _TOKENEXCHANGEREQUEST._serialized_end=7078 - _TOKENEXCHANGERESPONSE._serialized_start=7080 - _TOKENEXCHANGERESPONSE._serialized_end=7207 - _STSTOKENREQUEST._serialized_start=7210 - _STSTOKENREQUEST._serialized_end=7339 - _STSTOKENRESPONSE._serialized_start=7342 - _STSTOKENRESPONSE._serialized_end=7473 - _REVOCATIONSRESPONSE._serialized_start=7475 - _REVOCATIONSRESPONSE._serialized_end=7545 + _MEMBERCREDENTIAL._serialized_start=8589 + _MEMBERCREDENTIAL._serialized_end=8845 + _TRUSTEDSIGNINGKEY._serialized_start=8847 + _TRUSTEDSIGNINGKEY._serialized_end=8936 + _OIDCSESSION._serialized_start=8938 + _OIDCSESSION._serialized_end=9027 + _TASKAUTHORIZATIONRULE._serialized_start=9030 + _TASKAUTHORIZATIONRULE._serialized_end=9171 + _TASKRULE._serialized_start=9173 + _TASKRULE._serialized_end=9299 + _TASKOPERATION._serialized_start=9301 + _TASKOPERATION._serialized_end=9416 + _TOKENEXCHANGEREQUEST._serialized_start=9419 + _TOKENEXCHANGEREQUEST._serialized_end=9584 + _TOKENEXCHANGERESPONSE._serialized_start=9586 + _TOKENEXCHANGERESPONSE._serialized_end=9713 + _STSTOKENREQUEST._serialized_start=9716 + _STSTOKENREQUEST._serialized_end=9845 + _STSTOKENRESPONSE._serialized_start=9848 + _STSTOKENRESPONSE._serialized_end=9979 + _REVOCATIONSRESPONSE._serialized_start=9981 + _REVOCATIONSRESPONSE._serialized_end=10051 # @@protoc_insertion_point(module_scope) diff --git a/sdk/python/src/agent_mesh/_proto/sam_pb2.pyi b/sdk/python/src/agent_mesh/_proto/sam_pb2.pyi index 613bfc43..728b0e17 100644 --- a/sdk/python/src/agent_mesh/_proto/sam_pb2.pyi +++ b/sdk/python/src/agent_mesh/_proto/sam_pb2.pyi @@ -29,6 +29,31 @@ class AWSAssumeRole(_message.Message): session_policy: str def __init__(self, role_arn: _Optional[str] = ..., session_policy: _Optional[str] = ...) -> None: ... +class AdminStatusResponse(_message.Message): + __slots__ = ["active_routers", "bootstrap_tokens", "enrolled_nodes", "enrollment_requests", "node_catalog", "policy", "users"] + class NodeCatalogEntry(_message.Message): + __slots__ = ["key", "value"] + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: NodeServices + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[NodeServices, _Mapping]] = ...) -> None: ... + ACTIVE_ROUTERS_FIELD_NUMBER: _ClassVar[int] + BOOTSTRAP_TOKENS_FIELD_NUMBER: _ClassVar[int] + ENROLLED_NODES_FIELD_NUMBER: _ClassVar[int] + ENROLLMENT_REQUESTS_FIELD_NUMBER: _ClassVar[int] + NODE_CATALOG_FIELD_NUMBER: _ClassVar[int] + POLICY_FIELD_NUMBER: _ClassVar[int] + USERS_FIELD_NUMBER: _ClassVar[int] + active_routers: _containers.RepeatedCompositeFieldContainer[RouterLease] + bootstrap_tokens: _containers.RepeatedCompositeFieldContainer[BootstrapToken] + enrolled_nodes: _containers.RepeatedCompositeFieldContainer[EnrolledNode] + enrollment_requests: _containers.RepeatedCompositeFieldContainer[EnrollmentRequest] + node_catalog: _containers.MessageMap[str, NodeServices] + policy: PolicyConfig + users: _containers.RepeatedCompositeFieldContainer[User] + def __init__(self, users: _Optional[_Iterable[_Union[User, _Mapping]]] = ..., active_routers: _Optional[_Iterable[_Union[RouterLease, _Mapping]]] = ..., enrolled_nodes: _Optional[_Iterable[_Union[EnrolledNode, _Mapping]]] = ..., enrollment_requests: _Optional[_Iterable[_Union[EnrollmentRequest, _Mapping]]] = ..., bootstrap_tokens: _Optional[_Iterable[_Union[BootstrapToken, _Mapping]]] = ..., policy: _Optional[_Union[PolicyConfig, _Mapping]] = ..., node_catalog: _Optional[_Mapping[str, NodeServices]] = ...) -> None: ... + class AuthFrame(_message.Message): __slots__ = ["biscuit", "target_service"] BISCUIT_FIELD_NUMBER: _ClassVar[int] @@ -90,6 +115,66 @@ class BootstrapEnrollResponse(_message.Message): status: EnrollmentStatus def __init__(self, status: _Optional[_Union[EnrollmentStatus, str]] = ..., biscuit_token: _Optional[bytes] = ..., poll_interval_seconds: _Optional[int] = ..., error_message: _Optional[str] = ..., control_plane_public_key: _Optional[bytes] = ..., router_addresses: _Optional[_Iterable[str]] = ..., expire_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... +class BootstrapToken(_message.Message): + __slots__ = ["autonomous_recovery", "create_time", "description", "expire_time", "id", "max_usages", "owner_id", "revoke_time", "role", "usages_count"] + AUTONOMOUS_RECOVERY_FIELD_NUMBER: _ClassVar[int] + CREATE_TIME_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + EXPIRE_TIME_FIELD_NUMBER: _ClassVar[int] + ID_FIELD_NUMBER: _ClassVar[int] + MAX_USAGES_FIELD_NUMBER: _ClassVar[int] + OWNER_ID_FIELD_NUMBER: _ClassVar[int] + REVOKE_TIME_FIELD_NUMBER: _ClassVar[int] + ROLE_FIELD_NUMBER: _ClassVar[int] + USAGES_COUNT_FIELD_NUMBER: _ClassVar[int] + autonomous_recovery: bool + create_time: _timestamp_pb2.Timestamp + description: str + expire_time: _timestamp_pb2.Timestamp + id: str + max_usages: int + owner_id: str + revoke_time: _timestamp_pb2.Timestamp + role: str + usages_count: int + def __init__(self, id: _Optional[str] = ..., role: _Optional[str] = ..., owner_id: _Optional[str] = ..., max_usages: _Optional[int] = ..., usages_count: _Optional[int] = ..., description: _Optional[str] = ..., create_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., expire_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., revoke_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., autonomous_recovery: bool = ...) -> None: ... + +class BootstrapTokenCreateRequest(_message.Message): + __slots__ = ["autonomous_recovery", "description", "max_usages", "owner_id", "role", "ttl_hours"] + AUTONOMOUS_RECOVERY_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + MAX_USAGES_FIELD_NUMBER: _ClassVar[int] + OWNER_ID_FIELD_NUMBER: _ClassVar[int] + ROLE_FIELD_NUMBER: _ClassVar[int] + TTL_HOURS_FIELD_NUMBER: _ClassVar[int] + autonomous_recovery: bool + description: str + max_usages: int + owner_id: str + role: str + ttl_hours: int + def __init__(self, role: _Optional[str] = ..., owner_id: _Optional[str] = ..., ttl_hours: _Optional[int] = ..., max_usages: _Optional[int] = ..., description: _Optional[str] = ..., autonomous_recovery: bool = ...) -> None: ... + +class BootstrapTokenCreateResponse(_message.Message): + __slots__ = ["expire_time", "id", "owner_id", "role", "token"] + EXPIRE_TIME_FIELD_NUMBER: _ClassVar[int] + ID_FIELD_NUMBER: _ClassVar[int] + OWNER_ID_FIELD_NUMBER: _ClassVar[int] + ROLE_FIELD_NUMBER: _ClassVar[int] + TOKEN_FIELD_NUMBER: _ClassVar[int] + expire_time: _timestamp_pb2.Timestamp + id: str + owner_id: str + role: str + token: str + def __init__(self, id: _Optional[str] = ..., token: _Optional[str] = ..., role: _Optional[str] = ..., owner_id: _Optional[str] = ..., expire_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class BootstrapTokenListResponse(_message.Message): + __slots__ = ["tokens"] + TOKENS_FIELD_NUMBER: _ClassVar[int] + tokens: _containers.RepeatedCompositeFieldContainer[BootstrapToken] + def __init__(self, tokens: _Optional[_Iterable[_Union[BootstrapToken, _Mapping]]] = ...) -> None: ... + class CommandBackend(_message.Message): __slots__ = ["command", "env"] class EnvEntry(_message.Message): @@ -216,6 +301,70 @@ class EnrollResponse(_message.Message): router_addresses: _containers.RepeatedScalarFieldContainer[str] def __init__(self, biscuit_token: _Optional[bytes] = ..., error_message: _Optional[str] = ..., control_plane_public_key: _Optional[bytes] = ..., router_addresses: _Optional[_Iterable[str]] = ..., expire_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... +class EnrolledNode(_message.Message): + __slots__ = ["autonomous_recovery", "banned", "claims_json", "enroll_time", "enrollment_type", "expire_time", "labels", "owner_id", "peer_id", "role"] + class LabelsEntry(_message.Message): + __slots__ = ["key", "value"] + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: str + def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + AUTONOMOUS_RECOVERY_FIELD_NUMBER: _ClassVar[int] + BANNED_FIELD_NUMBER: _ClassVar[int] + CLAIMS_JSON_FIELD_NUMBER: _ClassVar[int] + ENROLLMENT_TYPE_FIELD_NUMBER: _ClassVar[int] + ENROLL_TIME_FIELD_NUMBER: _ClassVar[int] + EXPIRE_TIME_FIELD_NUMBER: _ClassVar[int] + LABELS_FIELD_NUMBER: _ClassVar[int] + OWNER_ID_FIELD_NUMBER: _ClassVar[int] + PEER_ID_FIELD_NUMBER: _ClassVar[int] + ROLE_FIELD_NUMBER: _ClassVar[int] + autonomous_recovery: bool + banned: bool + claims_json: str + enroll_time: _timestamp_pb2.Timestamp + enrollment_type: str + expire_time: _timestamp_pb2.Timestamp + labels: _containers.ScalarMap[str, str] + owner_id: str + peer_id: str + role: str + def __init__(self, peer_id: _Optional[str] = ..., role: _Optional[str] = ..., enrollment_type: _Optional[str] = ..., claims_json: _Optional[str] = ..., owner_id: _Optional[str] = ..., labels: _Optional[_Mapping[str, str]] = ..., enroll_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., expire_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., banned: bool = ..., autonomous_recovery: bool = ...) -> None: ... + +class EnrollmentRequest(_message.Message): + __slots__ = ["create_time", "id", "labels", "peer_id", "resolve_time", "resolved_by", "status", "token_id"] + class LabelsEntry(_message.Message): + __slots__ = ["key", "value"] + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: str + def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + CREATE_TIME_FIELD_NUMBER: _ClassVar[int] + ID_FIELD_NUMBER: _ClassVar[int] + LABELS_FIELD_NUMBER: _ClassVar[int] + PEER_ID_FIELD_NUMBER: _ClassVar[int] + RESOLVED_BY_FIELD_NUMBER: _ClassVar[int] + RESOLVE_TIME_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + TOKEN_ID_FIELD_NUMBER: _ClassVar[int] + create_time: _timestamp_pb2.Timestamp + id: str + labels: _containers.ScalarMap[str, str] + peer_id: str + resolve_time: _timestamp_pb2.Timestamp + resolved_by: str + status: EnrollmentStatus + token_id: str + def __init__(self, id: _Optional[str] = ..., peer_id: _Optional[str] = ..., token_id: _Optional[str] = ..., status: _Optional[_Union[EnrollmentStatus, str]] = ..., labels: _Optional[_Mapping[str, str]] = ..., create_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., resolve_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., resolved_by: _Optional[str] = ...) -> None: ... + +class EnrollmentRequestListResponse(_message.Message): + __slots__ = ["requests"] + REQUESTS_FIELD_NUMBER: _ClassVar[int] + requests: _containers.RepeatedCompositeFieldContainer[EnrollmentRequest] + def __init__(self, requests: _Optional[_Iterable[_Union[EnrollmentRequest, _Mapping]]] = ...) -> None: ... + class ExtProc(_message.Message): __slots__ = ["allow_mode_override", "ca", "client_certificate", "failure_mode_allow", "max_buffered_bytes", "message_timeout", "processing_mode", "target"] ALLOW_MODE_OVERRIDE_FIELD_NUMBER: _ClassVar[int] @@ -369,6 +518,14 @@ class NodeCatalogReport(_message.Message): services: _containers.RepeatedCompositeFieldContainer[ServiceInfo] def __init__(self, services: _Optional[_Iterable[_Union[ServiceInfo, _Mapping]]] = ...) -> None: ... +class NodeServices(_message.Message): + __slots__ = ["report_time", "services"] + REPORT_TIME_FIELD_NUMBER: _ClassVar[int] + SERVICES_FIELD_NUMBER: _ClassVar[int] + report_time: _timestamp_pb2.Timestamp + services: _containers.RepeatedCompositeFieldContainer[ServiceInfo] + def __init__(self, services: _Optional[_Iterable[_Union[ServiceInfo, _Mapping]]] = ..., report_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + class OIDCFederation(_message.Message): __slots__ = ["audience", "impersonate", "scopes", "token_endpoint"] AUDIENCE_FIELD_NUMBER: _ClassVar[int] @@ -496,6 +653,22 @@ class RevocationsResponse(_message.Message): revocation_ids: _containers.RepeatedScalarFieldContainer[str] def __init__(self, revocation_ids: _Optional[_Iterable[str]] = ..., banned_peer_ids: _Optional[_Iterable[str]] = ...) -> None: ... +class RouterLease(_message.Message): + __slots__ = ["addresses", "connected_peers", "dht_size", "expire_time", "last_renewal_time", "peer_id"] + ADDRESSES_FIELD_NUMBER: _ClassVar[int] + CONNECTED_PEERS_FIELD_NUMBER: _ClassVar[int] + DHT_SIZE_FIELD_NUMBER: _ClassVar[int] + EXPIRE_TIME_FIELD_NUMBER: _ClassVar[int] + LAST_RENEWAL_TIME_FIELD_NUMBER: _ClassVar[int] + PEER_ID_FIELD_NUMBER: _ClassVar[int] + addresses: _containers.RepeatedScalarFieldContainer[str] + connected_peers: _containers.RepeatedScalarFieldContainer[str] + dht_size: int + expire_time: _timestamp_pb2.Timestamp + last_renewal_time: _timestamp_pb2.Timestamp + peer_id: str + def __init__(self, peer_id: _Optional[str] = ..., addresses: _Optional[_Iterable[str]] = ..., last_renewal_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., expire_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., connected_peers: _Optional[_Iterable[str]] = ..., dht_size: _Optional[int] = ...) -> None: ... + class RouterLeaseRequest(_message.Message): __slots__ = ["addresses", "biscuit", "challenge_signature", "challenge_unix_ms", "connected_peers", "dht_size", "peer_id"] ADDRESSES_FIELD_NUMBER: _ClassVar[int] @@ -695,6 +868,34 @@ class TrustedSigningKey(_message.Message): receive_time: _timestamp_pb2.Timestamp def __init__(self, public_key: _Optional[bytes] = ..., receive_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... +class User(_message.Message): + __slots__ = ["create_time", "email", "id", "issuer", "role"] + CREATE_TIME_FIELD_NUMBER: _ClassVar[int] + EMAIL_FIELD_NUMBER: _ClassVar[int] + ID_FIELD_NUMBER: _ClassVar[int] + ISSUER_FIELD_NUMBER: _ClassVar[int] + ROLE_FIELD_NUMBER: _ClassVar[int] + create_time: _timestamp_pb2.Timestamp + email: str + id: str + issuer: str + role: str + def __init__(self, id: _Optional[str] = ..., issuer: _Optional[str] = ..., email: _Optional[str] = ..., role: _Optional[str] = ..., create_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class UserStatusResponse(_message.Message): + __slots__ = ["active_routers", "bootstrap_tokens", "enrolled_nodes", "policy", "user"] + ACTIVE_ROUTERS_FIELD_NUMBER: _ClassVar[int] + BOOTSTRAP_TOKENS_FIELD_NUMBER: _ClassVar[int] + ENROLLED_NODES_FIELD_NUMBER: _ClassVar[int] + POLICY_FIELD_NUMBER: _ClassVar[int] + USER_FIELD_NUMBER: _ClassVar[int] + active_routers: _containers.RepeatedCompositeFieldContainer[RouterLease] + bootstrap_tokens: _containers.RepeatedCompositeFieldContainer[BootstrapToken] + enrolled_nodes: _containers.RepeatedCompositeFieldContainer[EnrolledNode] + policy: PolicyConfig + user: User + def __init__(self, user: _Optional[_Union[User, _Mapping]] = ..., enrolled_nodes: _Optional[_Iterable[_Union[EnrolledNode, _Mapping]]] = ..., bootstrap_tokens: _Optional[_Iterable[_Union[BootstrapToken, _Mapping]]] = ..., active_routers: _Optional[_Iterable[_Union[RouterLease, _Mapping]]] = ..., policy: _Optional[_Union[PolicyConfig, _Mapping]] = ...) -> None: ... + class EnrollmentStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = [] diff --git a/site/content/docs/guides/headless-enrollment.md b/site/content/docs/guides/headless-enrollment.md index 82c16c0f..67c39e03 100644 --- a/site/content/docs/guides/headless-enrollment.md +++ b/site/content/docs/guides/headless-enrollment.md @@ -124,7 +124,7 @@ curl -fsS -X POST https://mesh.example.com/admin/bootstrap-tokens \ ``` ```json -{"id":"62e92ffca…","token":"sam-bt-72fb0175788dee0…","role":"sam:role:node","expires_at":"2026-09-20T15:00:00Z"} +{"id":"62e92ffca…","token":"sam-bt-72fb0175788dee0…","role":"sam:role:node","expire_time":"2026-09-20T15:00:00Z"} ``` The plaintext `token` is shown once. The control plane keeps only its hash. diff --git a/site/content/docs/reference/control-plane.md b/site/content/docs/reference/control-plane.md index 2641c08d..710bdd19 100644 --- a/site/content/docs/reference/control-plane.md +++ b/site/content/docs/reference/control-plane.md @@ -89,16 +89,19 @@ names. Every instant in a response (`expire_time` and the like) is a ### Admin -All routes require `Authorization: Bearer ` and use JSON. +All routes require `Authorization: Bearer `. Request and +response bodies are protojson of the messages named below, with proto field +names; unknown fields are rejected. | Route | Purpose | |---|---| -| `GET /admin/status` | Everything the console shows: routers, nodes, enrollment requests, tokens, users, and the policy as JSON. | -| `GET /admin/policy` | The mesh policy as protojson of `PolicyConfig`, the same document `POST /policies` takes. | -| `POST /policies`, `PUT /policies` | Replace the mesh policy. The JSON body is protojson of `PolicyConfig`. Unknown fields are rejected, so a misspelt grant fails instead of being dropped. | -| `POST /admin/bootstrap-tokens` | Mint a token. Body: `role` (required), `ttl_hours` (default 24), `max_usages` (default 1), `description`, `autonomous_recovery` (default false). Returns `201` with `id`, `token` (shown once), `role`, `expires_at`. | -| `DELETE /admin/bootstrap-tokens/{id}` | Revoke a token. Can be repeated. The token stays in the list, marked as revoked. | -| `GET /admin/enrollments` | Pending bootstrap enrollment requests. | +| `GET /admin/status` | `AdminStatusResponse`: everything the console shows: routers, nodes, enrollment requests, tokens, users, the policy and the node service catalog. | +| `GET /admin/policy` | The mesh policy as `PolicyConfig`, the same document `POST /policies` takes. | +| `POST /policies`, `PUT /policies` | Replace the mesh policy. The body is `PolicyConfig`, so a misspelt grant fails instead of being dropped. | +| `GET /admin/bootstrap-tokens` | `BootstrapTokenListResponse`: every token, including revoked and expired ones. | +| `POST /admin/bootstrap-tokens` | Mint a token. Body `BootstrapTokenCreateRequest`: `role` (required), `ttl_hours` (default 24), `max_usages` (default 1), `description`, `autonomous_recovery` (default false). Returns `201` with `BootstrapTokenCreateResponse`: `id`, `token` (shown once), `role`, `expire_time`. | +| `DELETE /admin/bootstrap-tokens/{id}` | Revoke a token. Can be repeated. The token stays in the list with `revoke_time` set. | +| `GET /admin/enrollments` | `EnrollmentRequestListResponse`: bootstrap enrollment requests, pending and decided. | | `POST /admin/enrollments/{id}/approve` | Approve. Spends a token usage. Checks the token again, and checks the labels against the role's `allowed_labels`. | | `POST /admin/enrollments/{id}/reject` | Reject. | | `POST /admin/revoke` | Body `{"peer_id": "..."}`. Ban the node and, for an OIDC enrollment, the identity behind it. | @@ -113,8 +116,8 @@ with `403`). The console uses these routes. | Route | Purpose | |---|---| -| `GET /user/status` | The caller's enrolled nodes and tokens. | -| `POST /user/bootstrap-tokens` | Mint a token owned by the caller. `role` defaults to `sam:role:node`. Banning the owner disables the tokens they minted. | +| `GET /user/status` | `UserStatusResponse`: the caller, their enrolled nodes and tokens. For an administrator it also carries the routers and the policy. | +| `POST /user/bootstrap-tokens` | Mint a token owned by the caller; body and response as on the admin route. `role` defaults to `sam:role:node`. Banning the owner disables the tokens they minted. | | `POST /user/revoke` | Ban one of the caller's own nodes. | ## Notes diff --git a/tests/integration/enroll_status_polling_test.go b/tests/integration/enroll_status_polling_test.go index 7ea1ebe6..e88dbb50 100644 --- a/tests/integration/enroll_status_polling_test.go +++ b/tests/integration/enroll_status_polling_test.go @@ -33,6 +33,7 @@ import ( "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-msgio" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -193,15 +194,15 @@ func TestEnrollStatusPollingCollectsBiscuit(t *testing.T) { time.Sleep(50 * time.Millisecond) continue } - var enrollList []storage.EnrollmentRequest - if err := json.NewDecoder(listResp.Body).Decode(&enrollList); err != nil { - _ = listResp.Body.Close() + listBody, readErr := io.ReadAll(listResp.Body) + _ = listResp.Body.Close() + enrollList := &api.EnrollmentRequestListResponse{} + if readErr != nil || protojson.Unmarshal(listBody, enrollList) != nil { time.Sleep(50 * time.Millisecond) continue } - _ = listResp.Body.Close() - if len(enrollList) == 1 { - reqID = enrollList[0].ID + if len(enrollList.GetRequests()) == 1 { + reqID = enrollList.GetRequests()[0].GetId() break } time.Sleep(50 * time.Millisecond) diff --git a/tests/integration/login_test.go b/tests/integration/login_test.go index 28b7c5b1..91e1340a 100644 --- a/tests/integration/login_test.go +++ b/tests/integration/login_test.go @@ -16,6 +16,7 @@ package integration_test import ( "bytes" + "context" "encoding/json" "fmt" "os" @@ -26,6 +27,7 @@ import ( "github.com/google/sam/api" "github.com/google/sam/internal/node" + "github.com/google/sam/internal/storage" ) // These tests cover how a node comes to hold, keep and lose its mesh @@ -60,9 +62,14 @@ func startMesh(t *testing.T, dir, oidcURL string, mintToken func(map[string]inte t.Helper() cpPort, cleanup := startControlPlaneAndRouter(t, dir, oidcURL, mintToken, meshPolicyFile(t, dir)) t.Cleanup(cleanup) + meshDirs[cpPort] = dir return cpPort, fmt.Sprintf("http://127.0.0.1:%d", cpPort) } +// meshDirs maps a control plane started by startMesh to its directory, so a +// test can open the control plane's store (see enrolledAs). +var meshDirs = map[int]string{} + // nodeHome is the environment of a node whose files live under home, and // the store that environment resolves to. func nodeHome(home string) ([]string, string) { @@ -101,26 +108,42 @@ func join(t *testing.T, nodeBin string, env []string, cpURL string, extra ...str } // enrolledAs fails unless the control plane holds an enrollment for peerID -// made by subject, and returns it. +// made by subject, and returns the biscuit it issued. The operator plane +// never carries a credential, so the biscuit comes from the control plane's +// own store (sqlite in WAL mode, readable beside the running process). func enrolledAs(t *testing.T, cpPort int, peerID string, subject string) []byte { t.Helper() record := fetchAdminStatus(t, cpPort, testAdminToken).enrolledNode(peerID) if record == nil { t.Fatalf("control plane :%d has no enrollment for %s", cpPort, peerID) } - if record.Role != api.RoleNode { - t.Fatalf("enrollment role = %q, want %q", record.Role, api.RoleNode) + if record.GetRole() != api.RoleNode { + t.Fatalf("enrollment role = %q, want %q", record.GetRole(), api.RoleNode) } var claims struct { Sub string `json:"sub"` } - if err := json.Unmarshal([]byte(record.ClaimsJSON), &claims); err != nil { - t.Fatalf("enrollment claims %q: %v", record.ClaimsJSON, err) + if err := json.Unmarshal([]byte(record.GetClaimsJson()), &claims); err != nil { + t.Fatalf("enrollment claims %q: %v", record.GetClaimsJson(), err) } if claims.Sub != subject { t.Fatalf("enrolled by %q, want %q", claims.Sub, subject) } - return record.Biscuit + + dir, ok := meshDirs[cpPort] + if !ok { + t.Fatalf("control plane :%d was not started by startMesh", cpPort) + } + cpStore, err := storage.NewSQLStore("sqlite", filepath.Join(dir, "cp-keys.db")) + if err != nil { + t.Fatalf("open control plane store: %v", err) + } + defer func() { _ = cpStore.Close() }() + stored, err := cpStore.GetNode(context.Background(), peerID) + if err != nil { + t.Fatalf("control plane store has no node %s: %v", peerID, err) + } + return stored.Biscuit } func TestSamNodeJoin(t *testing.T) { diff --git a/tests/integration/minimal_helpers_test.go b/tests/integration/minimal_helpers_test.go index e383ba05..2c994d9f 100644 --- a/tests/integration/minimal_helpers_test.go +++ b/tests/integration/minimal_helpers_test.go @@ -41,13 +41,13 @@ import ( "github.com/biscuit-auth/biscuit-go/v2" "github.com/google/sam/api" - "github.com/google/sam/internal/storage" "github.com/libp2p/go-libp2p" dht "github.com/libp2p/go-libp2p-kad-dht" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-msgio" "github.com/modelcontextprotocol/go-sdk/mcp" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -543,11 +543,10 @@ func getFreePort(t *testing.T) int { return 0 } -// adminStatus is the control plane's view of the mesh, in the types it -// serializes on /admin/status. +// adminStatus is the control plane's view of the mesh as /admin/status +// serves it. type adminStatus struct { - EnrolledNodes []storage.EnrolledNode `json:"enrolled_nodes"` - ActiveRouters []storage.RouterLease `json:"active_routers"` + *api.AdminStatusResponse } func fetchAdminStatus(t *testing.T, cpPort int, adminToken string) adminStatus { @@ -566,29 +565,33 @@ func fetchAdminStatus(t *testing.T, cpPort int, adminToken string) adminStatus { if resp.StatusCode != http.StatusOK { t.Fatalf("GET /admin/status: %s", resp.Status) } - var status adminStatus - if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("read /admin/status: %v", err) + } + status := &api.AdminStatusResponse{} + if err := protojson.Unmarshal(body, status); err != nil { t.Fatalf("decode /admin/status: %v", err) } - return status + return adminStatus{status} } // enrolledNode is the control plane's record of peerID, or nil. -func (s adminStatus) enrolledNode(peerID string) *storage.EnrolledNode { - for i := range s.EnrolledNodes { - if s.EnrolledNodes[i].PeerID == peerID { - return &s.EnrolledNodes[i] +func (s adminStatus) enrolledNode(peerID string) *api.EnrolledNode { + for _, n := range s.GetEnrolledNodes() { + if n.GetPeerId() == peerID { + return n } } return nil } // routerWith is the lease of a router that reports peerID connected, or nil. -func (s adminStatus) routerWith(peerID string) *storage.RouterLease { - for i := range s.ActiveRouters { - for _, p := range s.ActiveRouters[i].ConnectedPeers { +func (s adminStatus) routerWith(peerID string) *api.RouterLease { + for _, lease := range s.GetActiveRouters() { + for _, p := range lease.GetConnectedPeers() { if p == peerID { - return &s.ActiveRouters[i] + return lease } } } @@ -597,7 +600,7 @@ func (s adminStatus) routerWith(peerID string) *storage.RouterLease { // waitForPeerOnRouter returns the lease of the router peerID is connected // to, as the control plane learns it from the router's lease renewals. -func waitForPeerOnRouter(t *testing.T, cpPort int, adminToken string, peerID string, timeout time.Duration) *storage.RouterLease { +func waitForPeerOnRouter(t *testing.T, cpPort int, adminToken string, peerID string, timeout time.Duration) *api.RouterLease { t.Helper() deadline := time.Now().Add(timeout) for { diff --git a/tests/integration/rotation_test.go b/tests/integration/rotation_test.go index 432be162..4651cc66 100644 --- a/tests/integration/rotation_test.go +++ b/tests/integration/rotation_test.go @@ -284,7 +284,7 @@ func waitForLeaseRenewedAfter(t *testing.T, cpPort int, after time.Time) { deadline := time.Now().Add(10 * time.Second) for { for _, lease := range fetchAdminStatus(t, cpPort, "test-admin-token").ActiveRouters { - if lease.LastRenewal.After(after) { + if lease.GetLastRenewalTime().AsTime().After(after) { return } } diff --git a/tests/integration/sdk_enroll_test.go b/tests/integration/sdk_enroll_test.go index 9db0261d..1909d050 100644 --- a/tests/integration/sdk_enroll_test.go +++ b/tests/integration/sdk_enroll_test.go @@ -36,6 +36,7 @@ import ( "github.com/google/sam/internal/storage" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -304,9 +305,9 @@ func verifySDKReport(t *testing.T, ctx context.Context, store storage.Store, cpP func mintBootstrapToken(t *testing.T, baseURL, adminToken string) string { t.Helper() - body, err := json.Marshal(api.BootstrapTokenRequest{ + body, err := protojson.Marshal(&api.BootstrapTokenCreateRequest{ Role: api.RoleNode, - TTLHours: 1, + TtlHours: 1, MaxUsages: 1, Description: "native sdk conformance", }) @@ -357,18 +358,18 @@ func approvePendingEnrollment(t *testing.T, baseURL, adminToken string, done <-c time.Sleep(50 * time.Millisecond) continue } - var pending []storage.EnrollmentRequest - decodeErr := json.NewDecoder(resp.Body).Decode(&pending) + body, readErr := io.ReadAll(resp.Body) _ = resp.Body.Close() - if decodeErr != nil || resp.StatusCode != http.StatusOK { + pending := &api.EnrollmentRequestListResponse{} + if readErr != nil || resp.StatusCode != http.StatusOK || protojson.Unmarshal(body, pending) != nil { time.Sleep(50 * time.Millisecond) continue } - for _, e := range pending { - if e.Status != api.EnrollmentStatus_ENROLLMENT_STATUS_PENDING { + for _, e := range pending.GetRequests() { + if e.GetStatus() != api.EnrollmentStatus_ENROLLMENT_STATUS_PENDING { continue } - approve, _ := http.NewRequest(http.MethodPost, baseURL+"/admin/enrollments/"+e.ID+"/approve", nil) + approve, _ := http.NewRequest(http.MethodPost, baseURL+"/admin/enrollments/"+e.GetId()+"/approve", nil) approve.Header.Set("Authorization", "Bearer "+adminToken) approveResp, err := client.Do(approve) if err != nil { @@ -379,9 +380,9 @@ func approvePendingEnrollment(t *testing.T, baseURL, adminToken string, done <-c if approveResp.StatusCode != http.StatusOK { t.Fatalf("failed to approve enrollment: %s body %s", approveResp.Status, msg) } - pid, err := peer.Decode(e.PeerID) + pid, err := peer.Decode(e.GetPeerId()) if err != nil { - t.Fatalf("control plane recorded an undecodable peer ID %q: %v", e.PeerID, err) + t.Fatalf("control plane recorded an undecodable peer ID %q: %v", e.GetPeerId(), err) } return pid } diff --git a/tests/ui/console.spec.js b/tests/ui/console.spec.js index 58b30e52..6732f794 100644 --- a/tests/ui/console.spec.js +++ b/tests/ui/console.spec.js @@ -352,20 +352,20 @@ test('reported services render with type, labels and report time', async ({ page const response = await route.fetch(); const status = await response.json(); status.enrolled_nodes = [...(status.enrolled_nodes || []), { - PeerID: PEER, - Role: 'sam:role:node', - OwnerID: 'root-admin', - Labels: { component: 'stvv', region: 'eu-west' }, + peer_id: PEER, + role: 'sam:role:node', + owner_id: 'root-admin', + labels: { component: 'stvv', region: 'eu-west' }, }]; status.node_catalog = { [PEER]: { services: [ - { name: 'compliance-docs', type: 'mcp', description: 'doc lookup' }, + { name: 'compliance-docs', type: 'SERVICE_TYPE_MCP', description: 'doc lookup' }, // Reported names and descriptions are node-controlled input to an // admin page; markup in them must render inert. - { name: 'llama', type: 'inference', description: '' }, + { name: 'llama', type: 'SERVICE_TYPE_INFERENCE', description: '' }, ], - reported_at: '2026-09-15T08:00:00Z', + report_time: '2026-09-15T08:00:00Z', }, }; await route.fulfill({ response, json: status }); @@ -384,7 +384,7 @@ test('reported services render with type, labels and report time', async ({ page await expect(first).toContainText(PEER); // The node's labels ride along as the mnemonic under the peer ID. await expect(first.locator('.cell-subtext')).toHaveText('component=stvv, region=eu-west'); - // reported_at renders as a local time, not the raw RFC 3339 string. + // report_time renders as a local time, not the raw RFC 3339 string. await expect(first.locator('td').nth(4)).not.toHaveText(/2026-09-15T08:00:00Z|-/); await expect(rows.nth(1)).toContainText('inference'); @@ -408,8 +408,8 @@ test('revoked nodes disappear from the Nodes view and the node count', async ({ const response = await route.fetch(); const status = await response.json(); status.enrolled_nodes = [ - { PeerID: LIVE, Role: 'sam:role:node', OwnerID: 'root-admin', Banned: false }, - { PeerID: REVOKED, Role: 'sam:role:node', OwnerID: 'root-admin', Banned: true }, + { peer_id: LIVE, role: 'sam:role:node', owner_id: 'root-admin' }, + { peer_id: REVOKED, role: 'sam:role:node', owner_id: 'root-admin', banned: true }, ]; await route.fulfill({ response, json: status }); }); From 4d5484854e648ccae17ac453832cc5cf060fec26 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 6 Oct 2026 18:39:31 +0000 Subject: [PATCH 04/11] storage: store every instant as unix milliseconds bootstrap_tokens, enrollment_requests, users and banned_identities held unix seconds while every other table held milliseconds. Each table was consistent with itself, so nothing was wrong on the wire, but a query that compared one of these columns against a millisecond clock would have been off by a factor of a thousand with no test to notice. Migration 15 multiplies the existing rows by 1000 behind a magnitude guard (10^11 is the year 5138 in seconds and 1973 in milliseconds), so a row is converted exactly once and a database that already holds milliseconds is left alone. The round-trip test now compares at millisecond precision, so a table falling back to seconds fails there. --- internal/storage/migration_test.go | 149 ++++++++++++++++++++++++++++ internal/storage/round_trip_test.go | 8 +- internal/storage/sql_store.go | 58 +++++++---- 3 files changed, 191 insertions(+), 24 deletions(-) create mode 100644 internal/storage/migration_test.go diff --git a/internal/storage/migration_test.go b/internal/storage/migration_test.go new file mode 100644 index 00000000..fba61d46 --- /dev/null +++ b/internal/storage/migration_test.go @@ -0,0 +1,149 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storage + +import ( + "context" + "database/sql" + "path/filepath" + "testing" + "time" + + "github.com/google/sam/api" +) + +// openAtSchemaVersion creates a sqlite database migrated up to and including +// version, the way a control plane of that era left it. +func openAtSchemaVersion(t *testing.T, path string, version int) *sql.DB { + t.Helper() + db, err := sql.Open("sqlite", path) + if err != nil { + t.Fatal(err) + } + if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY)`); err != nil { + t.Fatal(err) + } + for _, m := range migrations { + if m.version > version { + break + } + for _, q := range m.sqlite { + if _, err := db.Exec(q); err != nil { + t.Fatalf("migration %d: %v", m.version, err) + } + } + if _, err := db.Exec(`INSERT INTO schema_migrations (version) VALUES (?)`, m.version); err != nil { + t.Fatal(err) + } + } + return db +} + +// Before schema version 15 four tables stored instants as unix seconds while +// the rest of the schema used milliseconds. The migration rewrites those +// rows once, and leaves rows that already hold milliseconds alone. +func TestMigrationConvertsSecondsToMillis(t *testing.T) { + path := filepath.Join(t.TempDir(), "cp.db") + created := time.Date(2026, 9, 1, 12, 0, 0, 0, time.UTC) + expires := created.Add(48 * time.Hour) + revoked := created.Add(time.Hour) + resolved := created.Add(2 * time.Minute) + + db := openAtSchemaVersion(t, path, 14) + exec := func(q string, args ...any) { + t.Helper() + if _, err := db.Exec(q, args...); err != nil { + t.Fatalf("%s: %v", q, err) + } + } + exec(`INSERT INTO users (id, issuer, email, role, created_at) VALUES ('alice', 'https://idp', 'alice@example.com', 'user', ?)`, created.Unix()) + exec(`INSERT INTO bootstrap_tokens (id, token_hash, role, max_usages, usages_count, description, created_at, expires_at, revoked_at) + VALUES ('old', 'old', 'sam:role:node', 2, 0, 'seconds era', ?, ?, NULL)`, created.Unix(), expires.Unix()) + exec(`INSERT INTO bootstrap_tokens (id, token_hash, role, max_usages, usages_count, description, created_at, expires_at, revoked_at) + VALUES ('old-revoked', 'old-revoked', 'sam:role:node', 1, 0, 'seconds era', ?, ?, ?)`, created.Unix(), expires.Unix(), revoked.Unix()) + // A row written in milliseconds must come through the guard untouched. + exec(`INSERT INTO bootstrap_tokens (id, token_hash, role, max_usages, usages_count, description, created_at, expires_at, revoked_at) + VALUES ('new', 'new', 'sam:role:node', 1, 0, 'millis era', ?, ?, NULL)`, created.UnixMilli(), expires.UnixMilli()) + exec(`INSERT INTO enrollment_requests (id, peer_id, public_key, token_id, status, created_at, resolved_at, resolved_by) + VALUES ('req', '12D3KooWPeer', X'00', 'old', ?, ?, ?, 'admin')`, int(api.EnrollmentStatus_ENROLLMENT_STATUS_APPROVED), created.Unix(), resolved.Unix()) + exec(`INSERT INTO banned_identities (identity, banned_at) VALUES ('https://idp|mallory', ?)`, created.Unix()) + if err := db.Close(); err != nil { + t.Fatal(err) + } + + store, err := NewSQLStore("sqlite", path) + if err != nil { + t.Fatalf("opening the store runs the migration: %v", err) + } + defer func() { _ = store.Close() }() + ctx := context.Background() + + var version int + if err := store.db.QueryRow(`SELECT MAX(version) FROM schema_migrations`).Scan(&version); err != nil { + t.Fatal(err) + } + if version < 15 { + t.Fatalf("schema version = %d, want at least 15", version) + } + + user, err := store.GetUser(ctx, "alice") + if err != nil { + t.Fatal(err) + } + if !user.CreatedAt.Equal(created) { + t.Errorf("user created_at = %v, want %v", user.CreatedAt, created) + } + + for _, id := range []string{"old", "new"} { + tok, err := store.GetBootstrapToken(ctx, id) + if err != nil { + t.Fatal(err) + } + if !tok.CreatedAt.Equal(created) || !tok.ExpiresAt.Equal(expires) || tok.RevokedAt != nil { + t.Errorf("token %s = created %v expires %v revoked %v; want %v %v nil", id, tok.CreatedAt, tok.ExpiresAt, tok.RevokedAt, created, expires) + } + } + tok, err := store.GetBootstrapToken(ctx, "old-revoked") + if err != nil { + t.Fatal(err) + } + if tok.RevokedAt == nil || !tok.RevokedAt.Equal(revoked) { + t.Errorf("token old-revoked revoked_at = %v, want %v", tok.RevokedAt, revoked) + } + // The usability check compares expires_at against the clock in the same + // unit; a token still valid in its own era stays valid. + if err := store.ConsumeBootstrapTokenUsage(ctx, "old", expires.Add(-time.Minute)); err != nil { + t.Errorf("consuming a converted token before its expiry: %v", err) + } + if err := store.ConsumeBootstrapTokenUsage(ctx, "old", expires.Add(time.Minute)); err == nil { + t.Error("consuming a converted token after its expiry succeeded") + } + + req, err := store.GetEnrollmentRequestByID(ctx, "req") + if err != nil { + t.Fatal(err) + } + if !req.CreatedAt.Equal(created) || req.ResolvedAt == nil || !req.ResolvedAt.Equal(resolved) { + t.Errorf("enrollment request = created %v resolved %v; want %v %v", req.CreatedAt, req.ResolvedAt, created, resolved) + } + + var bannedAt int64 + if err := store.db.QueryRow(`SELECT banned_at FROM banned_identities WHERE identity = 'https://idp|mallory'`).Scan(&bannedAt); err != nil { + t.Fatal(err) + } + if bannedAt != created.UnixMilli() { + t.Errorf("banned_at = %d, want %d", bannedAt, created.UnixMilli()) + } +} diff --git a/internal/storage/round_trip_test.go b/internal/storage/round_trip_test.go index 23f206b8..312d9d48 100644 --- a/internal/storage/round_trip_test.go +++ b/internal/storage/round_trip_test.go @@ -88,19 +88,19 @@ func requireFieldsRoundTrip(t *testing.T, want, got any, skip ...string) { } } -// valuesEqual compares at the coarsest precision any of these columns store, -// which is whole seconds for the timestamps written with Unix(). +// valuesEqual compares timestamps at the precision the columns store, unix +// milliseconds, so a table that fell back to seconds would fail here. func valuesEqual(w, g reflect.Value) bool { switch wv := w.Interface().(type) { case time.Time: gv, ok := g.Interface().(time.Time) - return ok && wv.Unix() == gv.Unix() + return ok && wv.UnixMilli() == gv.UnixMilli() case *time.Time: gv, ok := g.Interface().(*time.Time) if !ok || (wv == nil) != (gv == nil) { return false } - return wv == nil || wv.Unix() == gv.Unix() + return wv == nil || wv.UnixMilli() == gv.UnixMilli() } return reflect.DeepEqual(w.Interface(), g.Interface()) } diff --git a/internal/storage/sql_store.go b/internal/storage/sql_store.go index a93d0fda..31811905 100644 --- a/internal/storage/sql_store.go +++ b/internal/storage/sql_store.go @@ -519,6 +519,24 @@ var migrations = []migration{ )`, }, }, + { + // Every BIGINT instant is unix milliseconds. These four tables held + // seconds; the guard tells the two apart (10^11 seconds is the year + // 5138, 10^11 milliseconds is 1973), so a row is converted once. + version: 15, + postgres: secondsToMillisMigration, + sqlite: secondsToMillisMigration, + }, +} + +var secondsToMillisMigration = []string{ + `UPDATE bootstrap_tokens SET created_at = created_at * 1000 WHERE created_at < 100000000000`, + `UPDATE bootstrap_tokens SET expires_at = expires_at * 1000 WHERE expires_at < 100000000000`, + `UPDATE bootstrap_tokens SET revoked_at = revoked_at * 1000 WHERE revoked_at IS NOT NULL AND revoked_at < 100000000000`, + `UPDATE enrollment_requests SET created_at = created_at * 1000 WHERE created_at < 100000000000`, + `UPDATE enrollment_requests SET resolved_at = resolved_at * 1000 WHERE resolved_at IS NOT NULL AND resolved_at < 100000000000`, + `UPDATE users SET created_at = created_at * 1000 WHERE created_at < 100000000000`, + `UPDATE banned_identities SET banned_at = banned_at * 1000 WHERE banned_at < 100000000000`, } func (s *SQLStore) initSchema() error { @@ -938,7 +956,7 @@ func (s *SQLStore) SetIdentityBanned(ctx context.Context, identity string, banne } if banned { query := s.rebind(`INSERT INTO banned_identities (identity, banned_at) VALUES (?, ?) ON CONFLICT (identity) DO NOTHING`) - _, err := s.db.ExecContext(ctx, query, identity, time.Now().Unix()) + _, err := s.db.ExecContext(ctx, query, identity, time.Now().UnixMilli()) return err } query := s.rebind(`DELETE FROM banned_identities WHERE identity = ?`) @@ -1337,8 +1355,8 @@ func (s *SQLStore) SaveBootstrapToken(ctx context.Context, token *BootstrapToken token.MaxUsages, token.UsagesCount, token.Description, - token.CreatedAt.Unix(), - token.ExpiresAt.Unix(), + token.CreatedAt.UnixMilli(), + token.ExpiresAt.UnixMilli(), token.AutonomousRecovery, ) return err @@ -1373,10 +1391,10 @@ func (s *SQLStore) GetBootstrapToken(ctx context.Context, id string) (*Bootstrap if ownerID.Valid { t.OwnerID = ownerID.String } - t.CreatedAt = time.Unix(created, 0) - t.ExpiresAt = time.Unix(expires, 0) + t.CreatedAt = time.UnixMilli(created) + t.ExpiresAt = time.UnixMilli(expires) if revokedAt.Valid { - rt := time.Unix(revokedAt.Int64, 0) + rt := time.UnixMilli(revokedAt.Int64) t.RevokedAt = &rt } return &t, nil @@ -1387,7 +1405,7 @@ func (s *SQLStore) GetBootstrapToken(ctx context.Context, id string) (*Bootstrap func (s *SQLStore) ConsumeBootstrapTokenUsage(ctx context.Context, id string, now time.Time) error { query := `UPDATE bootstrap_tokens SET usages_count = usages_count + 1 WHERE id = ? AND usages_count < max_usages AND revoked_at IS NULL AND expires_at > ?` - res, err := s.db.ExecContext(ctx, s.rebind(query), id, now.Unix()) + res, err := s.db.ExecContext(ctx, s.rebind(query), id, now.UnixMilli()) if err != nil { return fmt.Errorf("failed to consume token usage: %w", err) } @@ -1406,7 +1424,7 @@ func (s *SQLStore) ConsumeBootstrapTokenUsage(ctx context.Context, id string, no // a no-op rather than clobbering the original revocation time. func (s *SQLStore) RevokeBootstrapToken(ctx context.Context, id string) error { query := `UPDATE bootstrap_tokens SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL` - _, err := s.db.ExecContext(ctx, s.rebind(query), time.Now().Unix(), id) + _, err := s.db.ExecContext(ctx, s.rebind(query), time.Now().UnixMilli(), id) if err != nil { return fmt.Errorf("failed to revoke bootstrap token: %w", err) } @@ -1423,7 +1441,7 @@ func (s *SQLStore) CreateEnrollmentRequest(ctx context.Context, req *EnrollmentR VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` var resAt sql.NullInt64 if req.ResolvedAt != nil { - resAt = sql.NullInt64{Int64: req.ResolvedAt.Unix(), Valid: true} + resAt = sql.NullInt64{Int64: req.ResolvedAt.UnixMilli(), Valid: true} } _, err = s.db.ExecContext(ctx, s.rebind(query), req.ID, @@ -1433,7 +1451,7 @@ func (s *SQLStore) CreateEnrollmentRequest(ctx context.Context, req *EnrollmentR int(req.Status), string(labelsJSON), req.BiscuitToken, - req.CreatedAt.Unix(), + req.CreatedAt.UnixMilli(), resAt, req.ResolvedBy, ) @@ -1491,10 +1509,10 @@ func (s *SQLStore) scanEnrollmentRequest(row scannable) (*EnrollmentRequest, err } } - req.CreatedAt = time.Unix(created, 0) + req.CreatedAt = time.UnixMilli(created) req.Status = api.EnrollmentStatus(statusVal) if resAt.Valid { - t := time.Unix(resAt.Int64, 0) + t := time.UnixMilli(resAt.Int64) req.ResolvedAt = &t } @@ -1539,7 +1557,7 @@ func (s *SQLStore) UpdateEnrollmentRequest(ctx context.Context, id string, statu _, err := s.db.ExecContext(ctx, s.rebind(query), int(status), biscuit, - time.Now().Unix(), + time.Now().UnixMilli(), resolvedBy, id, ) @@ -1556,7 +1574,7 @@ func (s *SQLStore) ResolveEnrollmentRequest(ctx context.Context, id string, stat res, err := s.db.ExecContext(ctx, s.rebind(query), int(status), biscuit, - time.Now().Unix(), + time.Now().UnixMilli(), resolvedBy, id, int(api.EnrollmentStatus_ENROLLMENT_STATUS_PENDING), @@ -1673,10 +1691,10 @@ func (s *SQLStore) ListBootstrapTokens(ctx context.Context) ([]BootstrapToken, e if ownerID.Valid { t.OwnerID = ownerID.String } - t.CreatedAt = time.Unix(created, 0) - t.ExpiresAt = time.Unix(expires, 0) + t.CreatedAt = time.UnixMilli(created) + t.ExpiresAt = time.UnixMilli(expires) if revokedAt.Valid { - rt := time.Unix(revokedAt.Int64, 0) + rt := time.UnixMilli(revokedAt.Int64) t.RevokedAt = &rt } tokens = append(tokens, t) @@ -1707,7 +1725,7 @@ func (s *SQLStore) SaveUser(ctx context.Context, user *User) error { user.Issuer, user.Email, user.Role, - user.CreatedAt.Unix(), + user.CreatedAt.UnixMilli(), ) return err } @@ -1730,7 +1748,7 @@ func (s *SQLStore) GetUser(ctx context.Context, id string) (*User, error) { if err != nil { return nil, err } - user.CreatedAt = time.Unix(created, 0) + user.CreatedAt = time.UnixMilli(created) return &user, nil } @@ -1756,7 +1774,7 @@ func (s *SQLStore) ListUsers(ctx context.Context) ([]User, error) { ); err != nil { return nil, err } - user.CreatedAt = time.Unix(created, 0) + user.CreatedAt = time.UnixMilli(created) users = append(users, user) } if err := rows.Err(); err != nil { From f72eba5f6646dd5548c945c7d4c728fd2e107848 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 6 Oct 2026 18:54:53 +0000 Subject: [PATCH 05/11] e2e: read /admin/status with its proto field names The bats helpers selected enrolled nodes and routers by the Go field names the control plane no longer serializes. --- tests/e2e/auth_flows.bats | 6 +++--- tests/e2e/lib/container_mesh.bash | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/e2e/auth_flows.bats b/tests/e2e/auth_flows.bats index 2de16103..7d21f8c3 100644 --- a/tests/e2e/auth_flows.bats +++ b/tests/e2e/auth_flows.bats @@ -23,8 +23,8 @@ assert_enrolled() { echo "control plane has no enrollment for ${peer_id}" >&2 return 1 fi - [[ "$(jq -r '.EnrollmentType' <<<"${record}")" == "${enrollment_type}" ]] - [[ "$(jq -r '.Role' <<<"${record}")" == "sam:role:node" ]] + [[ "$(jq -r '.enrollment_type' <<<"${record}")" == "${enrollment_type}" ]] + [[ "$(jq -r '.role' <<<"${record}")" == "sam:role:node" ]] echo "${record}" } @@ -107,7 +107,7 @@ assert_enrolled() { local record record="$(assert_enrolled login OIDC)" echo "enrollment: ${record}" - [[ "$(jq -r '.ClaimsJSON | fromjson | .sub' <<<"${record}")" == "test-user" ]] + [[ "$(jq -r '.claims_json | fromjson | .sub' <<<"${record}")" == "test-user" ]] # The labels join declared must come back attested. /sam/identity hands back # the raw biscuit rather than decoded claims, so read the signed diff --git a/tests/e2e/lib/container_mesh.bash b/tests/e2e/lib/container_mesh.bash index de8f91d3..c561721f 100644 --- a/tests/e2e/lib/container_mesh.bash +++ b/tests/e2e/lib/container_mesh.bash @@ -246,7 +246,7 @@ if [[ -z "${MESH_HELPERS_LOADED:-}" ]]; then # peer, or nothing. mesh_enrolled_node() { local peer_id="$1" - mesh_admin_status | jq -c --arg id "${peer_id}" '.enrolled_nodes // [] | .[] | select(.PeerID == $id)' + mesh_admin_status | jq -c --arg id "${peer_id}" '.enrolled_nodes // [] | .[] | select(.peer_id == $id)' } # POST /debug/connect-peer on node ; the REST endpoint that replaced @@ -484,11 +484,11 @@ if [[ -z "${MESH_HELPERS_LOADED:-}" ]]; then local router_peer_id="" local lease_deadline=$((SECONDS + 60)) while [[ -z "${router_peer_id}" ]]; do - # active_routers is null, not [], until the first lease lands. + # active_routers is absent until the first lease lands. router_peer_id=$(docker run --rm --network "${MESH_NETWORK:-kind}" "${MESH_RUNTIME_IMAGE}" \ curl -sf --max-time 5 -H "Authorization: Bearer super-secret-admin-token" \ "http://${router_node_ip}:8080/admin/status" 2>/dev/null | - jq -r '.active_routers // [] | sort_by(.LastRenewal) | last | .PeerID // empty') + jq -r '.active_routers // [] | sort_by(.last_renewal_time) | last | .peer_id // empty') if [[ -z "${router_peer_id}" ]] && ((SECONDS >= lease_deadline)); then echo "router lease did not reach the control plane within 60s" >&2 return 1 From 5c31e457048c854634f75b8a84d36b36813e0ec9 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 6 Oct 2026 18:54:54 +0000 Subject: [PATCH 06/11] sdk/js: run credential refreshes one after another The control plane redeems only the last biscuit it issued, so two refreshes in flight leave the loser holding a spent one and the member re-enrolling. sam-node holds refreshMu and the Python SDK a lock; the JS SDK let a scheduled refresh and a manual one race. refresh() now queues behind the refresh in flight, and a failed refresh does not block the next. The test drives eight concurrent refreshes against a fake control plane that, like the real one, rejects a refresh presenting a stale biscuit. --- sdk/js/src/mesh.test.ts | 48 ++++++++++++++++++++++++++++++++++++++++- sdk/js/src/mesh.ts | 17 +++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/sdk/js/src/mesh.test.ts b/sdk/js/src/mesh.test.ts index b9605c36..5b122029 100644 --- a/sdk/js/src/mesh.test.ts +++ b/sdk/js/src/mesh.test.ts @@ -42,7 +42,10 @@ function proto(bytes: Uint8Array): Response { } /** A control plane that approves everything and hands out numbered biscuits. */ -function fakeControlPlane(keysOk = true): { fetch: typeof fetch; issued: number; lastRefreshJwt: string; keys: Identity[]; keysOk: boolean } { +function fakeControlPlane( + keysOk = true, + opts: { strictRefresh?: boolean; refreshDelayMs?: number; rejectNextRefresh?: boolean } = {}, +): { fetch: typeof fetch; issued: number; lastRefreshJwt: string; keys: Identity[]; keysOk: boolean } { // keys is what /keys serves and signs with; a test rotates by replacing it. const state = { issued: 0, lastRefreshJwt: "", keys: [cpKey], keysOk }; const current = () => (state.keys[state.keys.length - 1] as Identity).publicKeyRaw; @@ -71,6 +74,20 @@ function fakeControlPlane(keysOk = true): { fetch: typeof fetch; issued: number; ); case "POST /refresh": { const refreshReq = fromBinary(TokenRefreshRequestSchema, new Uint8Array(await req.arrayBuffer())); + if (opts.rejectNextRefresh) { + opts.rejectNextRefresh = false; + return new Response("control plane unavailable", { status: 503 }); + } + if (opts.strictRefresh) { + // The real control plane redeems only the last biscuit it issued. + const presented = Buffer.from(req.headers.get("Authorization")?.replace(/^Bearer /, "") ?? "", "base64").toString(); + if (presented !== `biscuit-${state.issued}`) { + return new Response(`stale biscuit ${presented}`, { status: 401 }); + } + } + if (opts.refreshDelayMs) { + await new Promise((resolve) => setTimeout(resolve, opts.refreshDelayMs)); + } state.lastRefreshJwt = refreshReq.jwt; state.issued++; return proto( @@ -248,6 +265,35 @@ test("enroll works without a state directory and keeps the enrollment key when / assert.deepEqual(mesh.credential.biscuit, text("biscuit-2")); }); +// A session refreshes on its own schedule while a pull from the control +// plane may refresh too. The control plane redeems only the last biscuit it +// issued and both write the same state file, so refreshes must run one +// after another (sam-node's refreshMu, the Python SDK's lock). +test("concurrent refreshes run one after the other", async () => { + const dir = await mkdtemp(join(tmpdir(), "sam-sdk-")); + try { + const opts = { strictRefresh: true, refreshDelayMs: 10, rejectNextRefresh: false }; + const cp = fakeControlPlane(true, opts); + const mesh = await AgentMesh.enroll({ controlPlaneUrl: "http://127.0.0.1:1", bootstrapToken: "sbt_secret", stateDir: join(dir, "state"), fetch: cp.fetch }); + + const results = await Promise.allSettled(Array.from({ length: 8 }, () => mesh.refresh())); + const failures = results.filter((r) => r.status === "rejected"); + assert.deepEqual(failures, []); + assert.deepEqual(mesh.credential.biscuit, text("biscuit-9")); + const onDisk = JSON.parse(await readFile(join(dir, "state", "credential.json"), "utf8")) as Record; + assert.equal(Buffer.from(onDisk.biscuit as string, "base64").toString(), "biscuit-9"); + + // A failed refresh does not block the one queued behind it. + opts.rejectNextRefresh = true; + const [failed, next] = await Promise.allSettled([mesh.refresh(), mesh.refresh()]); + assert.equal(failed?.status, "rejected"); + assert.equal(next?.status, "fulfilled"); + assert.deepEqual(mesh.credential.biscuit, text("biscuit-10")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + test("enroll refuses ambiguous credentials", async () => { const cp = fakeControlPlane(); await assert.rejects(AgentMesh.enroll({ controlPlaneUrl: "http://127.0.0.1:1", fetch: cp.fetch }), /exactly one of/); diff --git a/sdk/js/src/mesh.ts b/sdk/js/src/mesh.ts index ae00d709..9b996e95 100644 --- a/sdk/js/src/mesh.ts +++ b/sdk/js/src/mesh.ts @@ -143,6 +143,10 @@ export class AgentMesh { #credential: MeshCredential; readonly #state: StateStore | undefined; readonly #jwtSource: (() => Promise) | undefined; + // Refreshes run one after another, as sam-node's refreshMu: the control + // plane redeems only the last biscuit it issued, so two in flight would + // leave the loser holding a spent one. + #refreshChain: Promise = Promise.resolve(); private constructor( identity: Identity, @@ -297,8 +301,21 @@ export class AgentMesh { * Trades the current biscuit for a fresh one and persists it. The control * plane redeems only the last biscuit it issued, so a lost refresh result * means re-enrolling; persisting before returning keeps that rare. + * Concurrent calls wait for each other. */ async refresh(): Promise { + const run = this.#refreshChain.then( + () => this.#refreshOnce(), + () => this.#refreshOnce(), + ); + this.#refreshChain = run.then( + () => undefined, + () => undefined, + ); + return run; + } + + async #refreshOnce(): Promise { let jwt: string | undefined; if (this.#jwtSource !== undefined) { try { From 2a986b5785a525c0578fd5e1177907f182e35aab Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 6 Oct 2026 18:54:54 +0000 Subject: [PATCH 07/11] release readiness: bounded drain, one JSON style, --version everywhere, console warning - The control plane's Close drained its HTTP server without a deadline; a stuck connection held the pod until the kubelet killed it. The drain is now bounded to 20s, under the default termination grace period. - /sam/identity and /sam/peer/{id}/evidence rendered protojson with lowerCamelCase names while every other SAM JSON surface uses proto field names; they now match. - Only sam-node reported its version. sam-control-plane, sam-router, sam-one, sam-console and mcp-client answer --version, every goreleaser build and every image build stamps the tag, and mcp-client identifies itself to MCP servers with the build version instead of a constant. - sam-console warns at startup when --external-url is unset, since the OIDC redirect_uri and the cookie Secure flag then follow request headers. --- .github/workflows/deploy.yaml | 2 ++ .goreleaser.yaml | 6 +++--- Dockerfile.sam-console | 3 ++- Dockerfile.sam-control-plane | 3 ++- Makefile | 4 ++-- cmd/mcp-client/main.go | 8 +++++++- cmd/sam-console/main.go | 10 ++++++++++ cmd/sam-control-plane/main.go | 6 ++++-- cmd/sam-one/main.go | 6 ++++-- cmd/sam-router/main.go | 6 ++++-- internal/controlplane/server.go | 10 +++++++++- internal/node/identity_evidence.go | 2 +- 12 files changed, 50 insertions(+), 16 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 90804bb3..d7ef6187 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -68,6 +68,7 @@ jobs: with: context: . file: Dockerfile.sam-control-plane + build-args: VERSION=${{ github.ref_type == 'tag' && github.ref_name || github.sha }} platforms: ${{ github.ref_type == 'tag' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} push: true tags: ${{ steps.meta-cp.outputs.tags }} @@ -136,6 +137,7 @@ jobs: with: context: . file: Dockerfile.sam-console + build-args: VERSION=${{ github.ref_type == 'tag' && github.ref_name || github.sha }} platforms: ${{ github.ref_type == 'tag' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} push: true tags: ${{ steps.meta-console.outputs.tags }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 6a3ed65d..c8bc8e0f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -31,7 +31,7 @@ builds: - amd64 - arm64 ldflags: - - -s -w + - -s -w -X github.com/google/sam/internal/version.Version={{ .Tag }} - id: sam-router main: ./cmd/sam-router binary: sam-router @@ -59,7 +59,7 @@ builds: - amd64 - arm64 ldflags: - - -s -w + - -s -w -X github.com/google/sam/internal/version.Version={{ .Tag }} - id: sam-console main: ./cmd/sam-console binary: sam-console @@ -73,7 +73,7 @@ builds: - amd64 - arm64 ldflags: - - -s -w + - -s -w -X github.com/google/sam/internal/version.Version={{ .Tag }} - id: sam-one main: ./cmd/sam-one binary: sam-one diff --git a/Dockerfile.sam-console b/Dockerfile.sam-console index dd807dbd..84c0ee98 100644 --- a/Dockerfile.sam-console +++ b/Dockerfile.sam-console @@ -5,7 +5,8 @@ COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 go build -o /bin/sam-console ./cmd/sam-console +ARG VERSION=devel +RUN CGO_ENABLED=0 go build -buildvcs=false -ldflags="-X github.com/google/sam/internal/version.Version=${VERSION}" -o /bin/sam-console ./cmd/sam-console FROM gcr.io/distroless/static-debian12:nonroot@sha256:afa5c872c891853ca7fcf1f12c3edb23f7eeef36189728842dd51042ff57f7ab diff --git a/Dockerfile.sam-control-plane b/Dockerfile.sam-control-plane index f7a92eab..21c39c3d 100644 --- a/Dockerfile.sam-control-plane +++ b/Dockerfile.sam-control-plane @@ -4,7 +4,8 @@ WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -buildvcs=false -o sam-control-plane ./cmd/sam-control-plane +ARG VERSION=devel +RUN CGO_ENABLED=0 GOOS=linux go build -buildvcs=false -ldflags="-X github.com/google/sam/internal/version.Version=${VERSION}" -o sam-control-plane ./cmd/sam-control-plane # Pre-create the sqlite data dir owned by the nonroot UID so that a fresh # named/anonymous volume mounted over it inherits writable ownership instead # of defaulting to root. diff --git a/Makefile b/Makefile index 6312e16c..6a135644 100644 --- a/Makefile +++ b/Makefile @@ -259,7 +259,7 @@ update: go mod tidy docker-build-control-plane: - docker build --load -t sam-control-plane:local -f Dockerfile.sam-control-plane . + docker build --load --build-arg VERSION="$(VERSION)" -t sam-control-plane:local -f Dockerfile.sam-control-plane . docker-build-router: docker build --load --build-arg VERSION="$(VERSION)" -t sam-router:local -f Dockerfile.sam-router . @@ -274,7 +274,7 @@ docker-build-e2e-runtime: docker build --load -t sam-e2e-runtime:local -f tests/e2e/docker/Dockerfile.sam-runtime . docker-build-sam-console: - docker build --load -t sam-console:local -f Dockerfile.sam-console . + docker build --load --build-arg VERSION="$(VERSION)" -t sam-console:local -f Dockerfile.sam-console . docker-build: docker-build-control-plane docker-build-router docker-build-node docker-build-mock-oidc docker-build-e2e-runtime docker-build-sam-console diff --git a/cmd/mcp-client/main.go b/cmd/mcp-client/main.go index 39d89b24..182d5813 100644 --- a/cmd/mcp-client/main.go +++ b/cmd/mcp-client/main.go @@ -32,6 +32,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/google/sam/api" + "github.com/google/sam/internal/version" ) func main() { @@ -42,7 +43,12 @@ func main() { listTools := flag.Bool("list", false, "List available tools and exit") streamOpt := flag.Bool("stream", false, "Enable streaming mode for service discovery HTTP API") tokenOpt := flag.String("token", "", "Authorization Bearer token for protected sidecar endpoints") + showVersion := flag.Bool("version", false, "Print the version and exit") flag.Parse() + if *showVersion { + fmt.Println(version.String()) + return + } if *serverURL == "" { log.Fatal("Must specify -url") @@ -143,7 +149,7 @@ func main() { // Create MCP client client := mcp.NewClient(&mcp.Implementation{ Name: "mcp-test-client", - Version: "0.1.0", + Version: version.String(), }, nil) // Connect to server using the URL diff --git a/cmd/sam-console/main.go b/cmd/sam-console/main.go index b96056a6..e431f2c0 100644 --- a/cmd/sam-console/main.go +++ b/cmd/sam-console/main.go @@ -17,6 +17,7 @@ package main import ( "context" "flag" + "fmt" "log" "net/http" "os" @@ -26,6 +27,7 @@ import ( "github.com/google/sam/internal/console" "github.com/google/sam/internal/secrets" + "github.com/google/sam/internal/version" ) func main() { @@ -36,8 +38,13 @@ func main() { staticDir = flag.String("static-dir", "", "Directory containing static frontend files (default: assets embedded in the binary)") basePath = flag.String("base-path", "", "Base path prefix for the console (e.g. /console)") externalURL = flag.String("external-url", "", "Origin browsers reach this console on, e.g. https://console.example. Sets the OIDC redirect_uri and cookie Secure flag instead of trusting the Host and X-Forwarded-Proto headers") + showVersion = flag.Bool("version", false, "Print the version and exit") ) flag.Parse() + if *showVersion { + fmt.Println(version.String()) + return + } adminToken, err := secrets.FromPathOrEnv("admin-token", *adminTokenPath, "SAM_ADMIN_TOKEN") if err != nil { @@ -46,6 +53,9 @@ func main() { if adminToken == "" { log.Fatal("Admin token is required (via --admin-token-path or env SAM_ADMIN_TOKEN)") } + if *externalURL == "" { + log.Print("WARNING: --external-url is not set; the OIDC redirect_uri and the session cookie's Secure flag follow the Host and X-Forwarded-Proto request headers, which a client controls. Set --external-url when the console is reachable beyond localhost.") + } srv, err := console.NewServer(console.Config{ ControlPlaneURL: *controlPlaneURL, diff --git a/cmd/sam-control-plane/main.go b/cmd/sam-control-plane/main.go index aa3e1208..c84c038b 100644 --- a/cmd/sam-control-plane/main.go +++ b/cmd/sam-control-plane/main.go @@ -27,6 +27,7 @@ import ( "github.com/google/sam/internal/controlplane" "github.com/google/sam/internal/secrets" "github.com/google/sam/internal/storage" + "github.com/google/sam/internal/version" golog "github.com/ipfs/go-log/v2" "github.com/libp2p/go-libp2p/core/peer" "github.com/spf13/cobra" @@ -60,8 +61,9 @@ var logger = golog.Logger("sam-control-plane-cli") func main() { rootCmd := &cobra.Command{ - Use: "sam-control-plane", - Short: "Sovereign Agent Mesh - Control Plane", + Use: "sam-control-plane", + Short: "Sovereign Agent Mesh - Control Plane", + Version: version.String(), // Resolve the DB DSN (may embed a password) before any subcommand runs. PersistentPreRunE: func(cmd *cobra.Command, args []string) error { resolved, err := secrets.FromPathOrEnv("db-dsn", dbDSNPath, "SAM_DB_DSN") diff --git a/cmd/sam-one/main.go b/cmd/sam-one/main.go index af489c90..1c9e8775 100644 --- a/cmd/sam-one/main.go +++ b/cmd/sam-one/main.go @@ -31,6 +31,7 @@ import ( "github.com/google/sam/api" "github.com/google/sam/internal/standalone" "github.com/google/sam/internal/tunnel" + "github.com/google/sam/internal/version" golog "github.com/ipfs/go-log/v2" "github.com/spf13/cobra" ) @@ -68,8 +69,9 @@ func main() { ) rootCmd := &cobra.Command{ - Use: "sam-one", - Short: "Sovereign Agent Mesh - all-in-one standalone server", + Use: "sam-one", + Short: "Sovereign Agent Mesh - all-in-one standalone server", + Version: version.String(), Run: func(cmd *cobra.Command, args []string) { if os.Getenv("LOG_FORMAT") == "json" { _ = os.Setenv("GOLOG_LOG_FMT", "json") diff --git a/cmd/sam-router/main.go b/cmd/sam-router/main.go index 168ea3a0..25863e92 100644 --- a/cmd/sam-router/main.go +++ b/cmd/sam-router/main.go @@ -22,6 +22,7 @@ import ( "time" "github.com/google/sam/internal/router" + "github.com/google/sam/internal/version" golog "github.com/ipfs/go-log/v2" "github.com/spf13/cobra" ) @@ -54,8 +55,9 @@ var logger = golog.Logger("sam-router-cli") func main() { rootCmd := &cobra.Command{ - Use: "sam-router", - Short: "Sovereign Agent Mesh - libp2p Router Node", + Use: "sam-router", + Short: "Sovereign Agent Mesh - libp2p Router Node", + Version: version.String(), Run: func(cmd *cobra.Command, args []string) { // Initialize logging if os.Getenv("LOG_FORMAT") == "json" { diff --git a/internal/controlplane/server.go b/internal/controlplane/server.go index 31b65b72..df8389f9 100644 --- a/internal/controlplane/server.go +++ b/internal/controlplane/server.go @@ -1768,7 +1768,11 @@ func (s *Server) Close() error { var errs []error if s.httpServer != nil { - if err := s.httpServer.Shutdown(context.Background()); err != nil { + // Bounded: a stuck connection must not hold the drain past what the + // orchestrator allows before it kills the process anyway. + ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + defer cancel() + if err := s.httpServer.Shutdown(ctx); err != nil { errs = append(errs, err) } } @@ -1776,6 +1780,10 @@ func (s *Server) Close() error { return errors.Join(errs...) } +// shutdownTimeout bounds the HTTP drain in Close; it stays under the 30s +// Kubernetes grants a pod by default. +const shutdownTimeout = 20 * time.Second + // Addr returns the network address the server is listening on. func (s *Server) Addr() string { if s.listener == nil { diff --git a/internal/node/identity_evidence.go b/internal/node/identity_evidence.go index 9a435c85..b489cfe6 100644 --- a/internal/node/identity_evidence.go +++ b/internal/node/identity_evidence.go @@ -112,7 +112,7 @@ func requireIdentityEvidenceTransport(next http.Handler) http.Handler { } func writeEvidenceProtoJSON(w http.ResponseWriter, status int, value proto.Message) { - body, err := protojson.Marshal(value) + body, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(value) if err != nil { logger.Errorf("[IdentityEvidence] Failed to encode response: %v", err) writeEvidenceError(w, http.StatusInternalServerError, "Failed to encode response") From e034093818f349b69116acb2e40fcf3009ab459b Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 6 Oct 2026 19:52:48 +0000 Subject: [PATCH 08/11] controlplane: atomic bans, indexed scans, bounded revocation cache - A ban wrote the node row and the identity row in two statements; a failure between them left the device banned while its owner could enroll a new one, or the reverse on unban. SetNodeAndIdentityBanned does both in one transaction and SetNodeBan uses it. - Migration 16 indexes the scans that run on every bootstrap (/info's ban set), every lease lookup, every revocation pull and the node GC. nodes is the one table that grows with the mesh. - The in-memory revocation set dropped expired entries only when /revocations was read; it now drops them on write as well. --- internal/controlplane/server.go | 21 +++++-------- internal/controlplane/sts.go | 8 +++++ internal/controlplane/sts_test.go | 22 ++++++++++++++ internal/storage/migration_test.go | 34 +++++++++++++++++++++ internal/storage/sql_store.go | 47 ++++++++++++++++++++++++++++++ internal/storage/sql_store_test.go | 42 ++++++++++++++++++++++++++ internal/storage/storage.go | 6 ++++ 7 files changed, 167 insertions(+), 13 deletions(-) diff --git a/internal/controlplane/server.go b/internal/controlplane/server.go index df8389f9..ab5dafb1 100644 --- a/internal/controlplane/server.go +++ b/internal/controlplane/server.go @@ -3247,20 +3247,15 @@ func (s *Server) banNode(ctx context.Context, node *storage.EnrolledNode) error // is shed with a new keypair, and an unban that lifts only the peer id leaves // the human locked out of /register. func SetNodeBan(ctx context.Context, store storage.Store, node *storage.EnrolledNode, banned bool) error { - if err := store.SetNodeBanned(ctx, node.PeerID, banned); err != nil { - return err - } - if node.ClaimsJSON == "" { - return nil - } - var claims jwt.MapClaims - if err := json.Unmarshal([]byte(node.ClaimsJSON), &claims); err != nil { - return fmt.Errorf("stored claims for %s are unreadable: %w", node.PeerID, err) - } - if key := oidcIdentityKey(claims); key != "" { - return store.SetIdentityBanned(ctx, key, banned) + var identity string + if node.ClaimsJSON != "" { + var claims jwt.MapClaims + if err := json.Unmarshal([]byte(node.ClaimsJSON), &claims); err != nil { + return fmt.Errorf("stored claims for %s are unreadable: %w", node.PeerID, err) + } + identity = oidcIdentityKey(claims) } - return nil + return store.SetNodeAndIdentityBanned(ctx, node.PeerID, identity, banned) } // allowedLabelPatterns collects the label grants of every role an identity diff --git a/internal/controlplane/sts.go b/internal/controlplane/sts.go index b6cd076a..c9833da9 100644 --- a/internal/controlplane/sts.go +++ b/internal/controlplane/sts.go @@ -429,7 +429,15 @@ func (s *Server) RevokeBiscuitID(revocationID string, expiry time.Time) { if expiry.IsZero() { expiry = time.Now().Add(s.config.BiscuitTTL) } + now := time.Now() s.revokedBiscuitsMu.Lock() + // Expired entries also go on read; dropping them here too keeps the map + // bounded when nothing polls /revocations. + for id, exp := range s.revokedBiscuits { + if !now.Before(exp) { + delete(s.revokedBiscuits, id) + } + } s.revokedBiscuits[revocationID] = expiry s.revokedBiscuitsMu.Unlock() if s.store != nil { diff --git a/internal/controlplane/sts_test.go b/internal/controlplane/sts_test.go index 9a074d0c..840a8b8c 100644 --- a/internal/controlplane/sts_test.go +++ b/internal/controlplane/sts_test.go @@ -932,3 +932,25 @@ func TestOAuthAndControlPlaneHardening(t *testing.T) { t.Fatalf("expected 415 for text/plain POST /policies, got %d", polResp.StatusCode) } } + +// A revocation that expired leaves the in-memory set on the next write, not +// only when /revocations is read. +func TestRevokeBiscuitIDPrunesExpiredEntries(t *testing.T) { + srv, store, _ := setupTestServer(t, "") + defer func() { + _ = srv.Close() + _ = store.Close() + }() + + srv.RevokeBiscuitID("expired", time.Now().Add(-time.Minute)) + srv.RevokeBiscuitID("live", time.Now().Add(time.Hour)) + + srv.revokedBiscuitsMu.RLock() + defer srv.revokedBiscuitsMu.RUnlock() + if _, ok := srv.revokedBiscuits["expired"]; ok { + t.Error("expired revocation still cached") + } + if _, ok := srv.revokedBiscuits["live"]; !ok { + t.Error("live revocation missing") + } +} diff --git a/internal/storage/migration_test.go b/internal/storage/migration_test.go index fba61d46..45d670dd 100644 --- a/internal/storage/migration_test.go +++ b/internal/storage/migration_test.go @@ -18,6 +18,7 @@ import ( "context" "database/sql" "path/filepath" + "reflect" "testing" "time" @@ -147,3 +148,36 @@ func TestMigrationConvertsSecondsToMillis(t *testing.T) { t.Errorf("banned_at = %d, want %d", bannedAt, created.UnixMilli()) } } + +// Migration 16 indexes the scans that run on every bootstrap and every GC +// pass; the index list is the contract the planner relies on. +func TestMigrationCreatesIndexes(t *testing.T) { + store, err := NewSQLStore("sqlite", filepath.Join(t.TempDir(), "cp.db")) + if err != nil { + t.Fatal(err) + } + defer func() { _ = store.Close() }() + + rows, err := store.db.Query(`SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE 'idx_%' ORDER BY name`) + if err != nil { + t.Fatal(err) + } + defer func() { _ = rows.Close() }() + var got []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + t.Fatal(err) + } + got = append(got, name) + } + want := []string{ + "idx_enrollment_requests_created_at", + "idx_nodes_banned_expires_at", + "idx_revoked_biscuits_expires_at", + "idx_routers_expires_at", + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("indexes = %v, want %v", got, want) + } +} diff --git a/internal/storage/sql_store.go b/internal/storage/sql_store.go index 31811905..913d886d 100644 --- a/internal/storage/sql_store.go +++ b/internal/storage/sql_store.go @@ -527,6 +527,21 @@ var migrations = []migration{ postgres: secondsToMillisMigration, sqlite: secondsToMillisMigration, }, + { + // The scans that run on every bootstrap (/info's ban set), every + // lease lookup, every revocation pull and the node GC; nodes is the + // one table that grows with the mesh. + version: 16, + postgres: indexMigration, + sqlite: indexMigration, + }, +} + +var indexMigration = []string{ + `CREATE INDEX IF NOT EXISTS idx_nodes_banned_expires_at ON nodes (banned, expires_at)`, + `CREATE INDEX IF NOT EXISTS idx_routers_expires_at ON routers (expires_at)`, + `CREATE INDEX IF NOT EXISTS idx_revoked_biscuits_expires_at ON revoked_biscuits (expires_at)`, + `CREATE INDEX IF NOT EXISTS idx_enrollment_requests_created_at ON enrollment_requests (created_at)`, } var secondsToMillisMigration = []string{ @@ -964,6 +979,38 @@ func (s *SQLStore) SetIdentityBanned(ctx context.Context, identity string, banne return err } +// SetNodeAndIdentityBanned implements Store. +func (s *SQLStore) SetNodeAndIdentityBanned(ctx context.Context, peerID, identity string, banned bool) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + + res, err := tx.ExecContext(ctx, s.rebind(`UPDATE nodes SET banned = ? WHERE peer_id = ?`), banned, peerID) + if err != nil { + return err + } + n, err := res.RowsAffected() + if err != nil { + return err + } + if n == 0 { + return ErrNotFound + } + if identity != "" { + if banned { + _, err = tx.ExecContext(ctx, s.rebind(`INSERT INTO banned_identities (identity, banned_at) VALUES (?, ?) ON CONFLICT (identity) DO NOTHING`), identity, time.Now().UnixMilli()) + } else { + _, err = tx.ExecContext(ctx, s.rebind(`DELETE FROM banned_identities WHERE identity = ?`), identity) + } + if err != nil { + return err + } + } + return tx.Commit() +} + // IsIdentityBanned implements Store. func (s *SQLStore) IsIdentityBanned(ctx context.Context, identity string) (bool, error) { query := s.rebind(`SELECT 1 FROM banned_identities WHERE identity = ?`) diff --git a/internal/storage/sql_store_test.go b/internal/storage/sql_store_test.go index 87bd0ec4..e44f1f19 100644 --- a/internal/storage/sql_store_test.go +++ b/internal/storage/sql_store_test.go @@ -421,6 +421,48 @@ func TestIdentityBanOps(t *testing.T) { } } +// A ban names the device and the identity behind it in one transaction: an +// unknown peer rolls the identity ban back, so the two never disagree. +func TestSetNodeAndIdentityBannedIsAtomic(t *testing.T) { + store := newTestStore(t) + defer func() { _ = store.Close() }() + ctx := context.Background() + const identity = "http://issuer.example|atomic-sub" + + err := store.SetNodeAndIdentityBanned(ctx, "12D3KooWNoSuchNode", identity, true) + if err != ErrNotFound { + t.Fatalf("banning an unknown node: got %v, want ErrNotFound", err) + } + if banned, _ := store.IsIdentityBanned(ctx, identity); banned { + t.Fatal("identity ban survived the rolled-back node ban") + } + + node := &EnrolledNode{PeerID: "12D3KooWAtomicNode", PublicKey: []byte("pk"), Biscuit: []byte("b"), Role: api.RoleNode, EnrollmentType: "oidc", EnrolledAt: time.Now(), ExpiresAt: time.Now().Add(time.Hour)} + if err := store.EnrollNode(ctx, node); err != nil { + t.Fatal(err) + } + if err := store.SetNodeAndIdentityBanned(ctx, node.PeerID, identity, true); err != nil { + t.Fatalf("ban: %v", err) + } + nodeBanned, _ := store.IsNodeBanned(ctx, node.PeerID) + identityBanned, _ := store.IsIdentityBanned(ctx, identity) + if !nodeBanned || !identityBanned { + t.Fatalf("after ban: node=%v identity=%v, want both true", nodeBanned, identityBanned) + } + if err := store.SetNodeAndIdentityBanned(ctx, node.PeerID, identity, false); err != nil { + t.Fatalf("unban: %v", err) + } + nodeBanned, _ = store.IsNodeBanned(ctx, node.PeerID) + identityBanned, _ = store.IsIdentityBanned(ctx, identity) + if nodeBanned || identityBanned { + t.Fatalf("after unban: node=%v identity=%v, want both false", nodeBanned, identityBanned) + } + // A bootstrap-enrolled node has no identity to ban. + if err := store.SetNodeAndIdentityBanned(ctx, node.PeerID, "", true); err != nil { + t.Fatalf("ban without identity: %v", err) + } +} + func TestRouterLeaseOps(t *testing.T) { store := newTestStore(t) defer func() { _ = store.Close() }() diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 0221fdec..aaae9290 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -231,6 +231,12 @@ type Store interface { // this is what makes a ban survive keypair regeneration. SetIdentityBanned(ctx context.Context, identity string, banned bool) error + // SetNodeAndIdentityBanned is SetNodeBanned and SetIdentityBanned in one + // transaction, so a failure cannot leave the device banned while its + // identity may still enroll a new one, or the other way round. An empty + // identity bans the node alone. + SetNodeAndIdentityBanned(ctx context.Context, peerID, identity string, banned bool) error + // IsIdentityBanned checks if an identity is currently banned. IsIdentityBanned(ctx context.Context, identity string) (bool, error) From 06d0ffd082ba1b49d04814de633a55da6419125e Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 6 Oct 2026 19:52:48 +0000 Subject: [PATCH 09/11] fail closed on missing wiring, warn on insecure TLS, pin the tar_block grammar - The inference facade served a local model without any authorization when its authorizer seam was unset. Missing wiring is now a 503, the same convention as the provider label verifier. - sam-control-plane warns at startup when --insecure-skip-tls-verify is set. - Both SDKs refuse an appended block that carries a rule, a check or a second fact beside tar_block; the tests now say so explicitly. --- cmd/sam-control-plane/main.go | 3 +++ internal/node/openai_facade.go | 13 ++++++---- internal/node/openai_facade_test.go | 29 +++++++++++++++++++-- sdk/js/src/authorizer.test.ts | 40 +++++++++++++++++++++++++++++ sdk/python/tests/test_authorizer.py | 38 +++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 7 deletions(-) diff --git a/cmd/sam-control-plane/main.go b/cmd/sam-control-plane/main.go index c84c038b..0d7779ef 100644 --- a/cmd/sam-control-plane/main.go +++ b/cmd/sam-control-plane/main.go @@ -91,6 +91,9 @@ func main() { if oidcIssuer == "" && workloadIssuer == "" { logger.Fatalf("OIDC issuer is required (use --issuer or --workload-issuer flag)") } + if insecureSkipTLSVerify { + logger.Warnf("--insecure-skip-tls-verify is set: identity provider TLS certificates are not verified, so anyone on the path to the provider can forge the identities this control plane admits. For development only.") + } adminToken, err := secrets.FromPathOrEnv("admin-token", adminTokenPath, "SAM_ADMIN_TOKEN") if err != nil { diff --git a/internal/node/openai_facade.go b/internal/node/openai_facade.go index d6ff9875..07c6b6a8 100644 --- a/internal/node/openai_facade.go +++ b/internal/node/openai_facade.go @@ -467,11 +467,14 @@ func (f *openAIFacade) handleCompletions(w http.ResponseWriter, r *http.Request) } func (f *openAIFacade) serveLocal(w http.ResponseWriter, r *http.Request, serviceName string) { - if f.authorizeLocal != nil { - if err := f.authorizeLocal(r.Context(), serviceName, r.Method, r.URL.Path); err != nil { - writeOpenAIError(w, http.StatusForbidden, "permission_denied", fmt.Sprintf("authorization failed: %v", err)) - return - } + if f.authorizeLocal == nil { + writeOpenAIError(w, http.StatusServiceUnavailable, "authorization_unavailable", + "local authorization is unavailable on this node") + return + } + if err := f.authorizeLocal(r.Context(), serviceName, r.Method, r.URL.Path); err != nil { + writeOpenAIError(w, http.StatusForbidden, "permission_denied", fmt.Sprintf("authorization failed: %v", err)) + return } for _, svc := range f.localServices() { if svc.Info().GetName() != serviceName || svc.Handler() == nil { diff --git a/internal/node/openai_facade_test.go b/internal/node/openai_facade_test.go index 9818ca6e..93678150 100644 --- a/internal/node/openai_facade_test.go +++ b/internal/node/openai_facade_test.go @@ -50,8 +50,9 @@ func newFakeModelService(name string, handler http.Handler, models ...string) *f } // newTestFacade builds a facade with inert seams; tests override as needed. -// The provider verifier accepts everyone: tests that care about the gate -// replace it, and tests of the fail-closed path set it to nil explicitly. +// The provider verifier and the local authorizer accept everyone: tests that +// care about a gate replace it, and tests of the fail-closed path set it to +// nil explicitly. func newTestFacade() *openAIFacade { return &openAIFacade{ ttl: time.Minute, @@ -64,6 +65,7 @@ func newTestFacade() *openAIFacade { return nil, nil }, verifyPeerLabels: func(context.Context, string, map[string]string) error { return nil }, + authorizeLocal: func(context.Context, string, string, string) error { return nil }, } } @@ -237,6 +239,29 @@ func TestFacade_Completions_PrefersLocal(t *testing.T) { } } +// A facade without a local authorizer must not serve local models: the +// seam is wiring, and missing wiring is a refusal, not an allow. +func TestFacade_Completions_LocalWithoutAuthorizerFailsClosed(t *testing.T) { + f := newTestFacade() + f.authorizeLocal = nil + localHit := false + local := newFakeModelService("local-llm", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + localHit = true + }), "m1") + f.localServices = func() []Service { return []Service{local} } + + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{"model":"m1"}`)) + rec := httptest.NewRecorder() + f.handleCompletions(rec, req) + + if localHit { + t.Fatal("local service was invoked without authorization") + } + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusServiceUnavailable, rec.Body.String()) + } +} + func TestFacade_Completions_UnknownModel(t *testing.T) { f := newTestFacade() req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{"model":"nope"}`)) diff --git a/sdk/js/src/authorizer.test.ts b/sdk/js/src/authorizer.test.ts index 87308dac..b77b4251 100644 --- a/sdk/js/src/authorizer.test.ts +++ b/sdk/js/src/authorizer.test.ts @@ -23,6 +23,7 @@ import { before, test } from "node:test"; import { AuthorizationError, authorizeCaller, type AuthorizeRequest } from "./authorizer.ts"; import { BiscuitVerificationError, attenuateBiscuit, loadBiscuit, sealBiscuit } from "./biscuit.ts"; import { BASELINE_DATALOG } from "./gen/datalog.ts"; +import { encodeTARBlockFact } from "./tar.ts"; type Wasm = Awaited>; @@ -284,3 +285,42 @@ test("attenuateBiscuit and sealBiscuit narrow authority across hops", async () = ); }); +// An appended block may carry exactly one tar_block fact. Anything else a +// holder writes there (a rule, a check, a second fact) is refused before the +// Datalog runs, whatever the authority block grants. +test("appended blocks carrying anything but one tar_block fact are refused", async () => { + const root = nodeToken(CALLER, [`granted_service_all_types(true)`, `target_unrestricted(true)`]); + const tarFact = encodeTARBlockFact({ name: "hop", rules: [{ allowedServices: ["mcp://calc"] }] }); + const append = (build: (block: ReturnType) => void): Uint8Array => { + const token = wasm.Biscuit.fromBytes(root, cpKeyPair.getPublicKey()); + const block = wasm.Biscuit.block_builder(); + build(block); + return token.appendBlock(block).toBytes(); + }; + + const malformed: Record = { + rule: append((b) => b.addRule(wasm.Rule.fromString(`role("sam:role:router") <- role("sam:role:node")`))), + check: append((b) => b.addCheck(wasm.Check.fromString(`check if true`))), + "extra fact": append((b) => { + b.addFact(wasm.Fact.fromString(tarFact)); + b.addFact(wasm.Fact.fromString(`node(${JSON.stringify(CALLER)})`)); + }), + "rule beside tar_block": append((b) => { + b.addFact(wasm.Fact.fromString(tarFact)); + b.addRule(wasm.Rule.fromString(`granted_service_all_types(true) <- role("sam:role:node")`)); + }), + }; + for (const [name, token] of Object.entries(malformed)) { + await assert.rejects( + authorizeCaller(request(token, "mcp://calc"), options([])), + (err: unknown) => err instanceof AuthorizationError && /tar_block/.test(err.message), + name, + ); + } + + // The well-formed block is the control. + const ok = append((b) => b.addFact(wasm.Fact.fromString(tarFact))); + const verified = await authorizeCaller(request(ok, "mcp://calc"), options([])); + assert.equal(verified.taskRules.length, 1); +}); + diff --git a/sdk/python/tests/test_authorizer.py b/sdk/python/tests/test_authorizer.py index 8341e3b8..b436181e 100644 --- a/sdk/python/tests/test_authorizer.py +++ b/sdk/python/tests/test_authorizer.py @@ -266,3 +266,41 @@ def test_attenuate_biscuit_narrows_authority_across_hops(): [CP_KEY], ) + +def test_appended_blocks_carrying_anything_but_one_tar_block_fact_are_refused(): + """An appended block may carry exactly one tar_block fact. Anything else a + holder writes there (a rule, a check, a second fact) is refused before the + Datalog runs, whatever the authority block grants.""" + from agent_mesh.tar import encode_tar_block_fact + + root = node_token(CALLER, ["granted_service_all_types(true)", "target_unrestricted(true)"]) + tar_fact = encode_tar_block_fact(sam_pb2.TaskAuthorizationRule(name="hop", rules=[sam_pb2.TaskRule(allowed_services=["mcp://calc"])])) + + def append(build) -> bytes: + token = ba.Biscuit.from_bytes(root, CP.public_key) + bb = ba.BlockBuilder() + build(bb) + return token.append(bb).to_bytes() + + def rule_only(bb): + bb.add_rule(ba.Rule('role("sam:role:router") <- role("sam:role:node")')) + + def check_only(bb): + bb.add_check(ba.Check("check if true")) + + def extra_fact(bb): + bb.add_fact(ba.Fact(tar_fact)) + bb.add_fact(ba.Fact(f'node("{CALLER}")')) + + def rule_beside_tar_block(bb): + bb.add_fact(ba.Fact(tar_fact)) + bb.add_rule(ba.Rule('granted_service_all_types(true) <- role("sam:role:node")')) + + for build in (rule_only, check_only, extra_fact, rule_beside_tar_block): + with pytest.raises(AuthorizationError, match="tar_block"): + authorize_caller(request(append(build)), options([])) + + # The well-formed block is the control. + verified = authorize_caller(request(append(lambda bb: bb.add_fact(ba.Fact(tar_fact)))), options([])) + assert len(verified.task_rules) == 1 + From 711d1df4e53942d0741cf82492ee47e85bd0bd9c Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 6 Oct 2026 20:12:29 +0000 Subject: [PATCH 10/11] controlplane: sweep the revocation set at a doubling threshold Review feedback on #589: a full scan of the in-memory revocation set on every write is O(N) in live revocations. The sweep now runs when the set reaches a threshold that doubles after each pass, so a write costs O(1) amortized and the set never holds more than twice its live entries. --- internal/controlplane/server.go | 7 ++++++- internal/controlplane/sts.go | 13 ++++++++++--- internal/controlplane/sts_test.go | 13 ++++++++----- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/internal/controlplane/server.go b/internal/controlplane/server.go index ab5dafb1..d275fbc4 100644 --- a/internal/controlplane/server.go +++ b/internal/controlplane/server.go @@ -102,7 +102,11 @@ type Server struct { revokedBiscuitsMu sync.RWMutex revokedBiscuits map[string]time.Time - bannedNodeRevIDs map[string]string + // revokedPruneAt is the size at which RevokeBiscuitID next drops expired + // entries; it doubles after each pass, so a write costs O(1) amortized and + // the map holds at most twice its live entries. + revokedPruneAt int + bannedNodeRevIDs map[string]string oauthCodesMu sync.Mutex oauthCodes map[string]*oauthAuthCode @@ -188,6 +192,7 @@ func NewServer(config Options, store storage.Store) (*Server, error) { stsLimiter: stsLimiter, oidcSigner: signer, revokedBiscuits: make(map[string]time.Time), + revokedPruneAt: revokedPruneMin, bannedNodeRevIDs: make(map[string]string), oauthCodes: make(map[string]*oauthAuthCode), providers: make(map[string]*oidc.Provider), diff --git a/internal/controlplane/sts.go b/internal/controlplane/sts.go index c9833da9..e27e72ee 100644 --- a/internal/controlplane/sts.go +++ b/internal/controlplane/sts.go @@ -420,6 +420,10 @@ func (s *Server) allowNodeSTSRequest(peerID string) bool { return s.stsLimiter.Allow(peerID) } +// revokedPruneMin is the smallest revocation set RevokeBiscuitID bothers to +// sweep for expired entries. +const revokedPruneMin = 64 + // RevokeBiscuitID records a root Biscuit revocation ID (base64url-encoded) as // revoked until expiry. func (s *Server) RevokeBiscuitID(revocationID string, expiry time.Time) { @@ -433,10 +437,13 @@ func (s *Server) RevokeBiscuitID(revocationID string, expiry time.Time) { s.revokedBiscuitsMu.Lock() // Expired entries also go on read; dropping them here too keeps the map // bounded when nothing polls /revocations. - for id, exp := range s.revokedBiscuits { - if !now.Before(exp) { - delete(s.revokedBiscuits, id) + if len(s.revokedBiscuits) >= s.revokedPruneAt { + for id, exp := range s.revokedBiscuits { + if !now.Before(exp) { + delete(s.revokedBiscuits, id) + } } + s.revokedPruneAt = max(revokedPruneMin, 2*len(s.revokedBiscuits)) } s.revokedBiscuits[revocationID] = expiry s.revokedBiscuitsMu.Unlock() diff --git a/internal/controlplane/sts_test.go b/internal/controlplane/sts_test.go index 840a8b8c..4f44ef5a 100644 --- a/internal/controlplane/sts_test.go +++ b/internal/controlplane/sts_test.go @@ -24,6 +24,7 @@ import ( "crypto/sha256" "encoding/base64" "encoding/json" + "fmt" "io" "net/http" "net/url" @@ -933,8 +934,8 @@ func TestOAuthAndControlPlaneHardening(t *testing.T) { } } -// A revocation that expired leaves the in-memory set on the next write, not -// only when /revocations is read. +// Expired revocations leave the in-memory set as writes accumulate, not only +// when /revocations is read, and the set stays bounded by its live entries. func TestRevokeBiscuitIDPrunesExpiredEntries(t *testing.T) { srv, store, _ := setupTestServer(t, "") defer func() { @@ -942,13 +943,15 @@ func TestRevokeBiscuitIDPrunesExpiredEntries(t *testing.T) { _ = store.Close() }() - srv.RevokeBiscuitID("expired", time.Now().Add(-time.Minute)) + for i := 0; i < 4*revokedPruneMin; i++ { + srv.RevokeBiscuitID(fmt.Sprintf("expired-%d", i), time.Now().Add(-time.Minute)) + } srv.RevokeBiscuitID("live", time.Now().Add(time.Hour)) srv.revokedBiscuitsMu.RLock() defer srv.revokedBiscuitsMu.RUnlock() - if _, ok := srv.revokedBiscuits["expired"]; ok { - t.Error("expired revocation still cached") + if n := len(srv.revokedBiscuits); n > revokedPruneMin+1 { + t.Errorf("revocation set holds %d entries after %d expired writes, want at most %d", n, 4*revokedPruneMin, revokedPruneMin+1) } if _, ok := srv.revokedBiscuits["live"]; !ok { t.Error("live revocation missing") From f8f4f03271a14b1b540117b320b08459da08a283 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 6 Oct 2026 20:23:28 +0000 Subject: [PATCH 11/11] controlplane: refuse enrollment without a subject, answer 413 for oversized bodies Review feedback on #589. Bans and ownership are keyed on issuer|subject, so a token without a sub claim would enroll an identity that can never be banned; /register now refuses it (OIDC ID tokens, Kubernetes service account tokens and JWT-SVIDs all carry one). SetNodeBan still bans the device when a record from before that check yields no identity key, and says so in the log instead of silently. readProtoJSON answers 413 rather than 400 when the body exceeds the limit. --- internal/controlplane/server.go | 17 +++++++++++++++ internal/controlplane/server_test.go | 31 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/internal/controlplane/server.go b/internal/controlplane/server.go index d275fbc4..b0843c00 100644 --- a/internal/controlplane/server.go +++ b/internal/controlplane/server.go @@ -698,6 +698,13 @@ func (s *Server) HandleRegister(w http.ResponseWriter, r *http.Request) { http.Error(w, "JWT validation failed: "+err.Error(), http.StatusUnauthorized) return } + // Bans and ownership are keyed on issuer|subject; a token without a + // subject would enroll an identity that can never be banned. + if oidcIdentityKey(claims) == "" { + logger.Warnw("JWT without a subject refused", "peer_id", req.PeerId) + http.Error(w, "JWT validation failed: token has no sub claim", http.StatusUnauthorized) + return + } // An email the issuer marks unverified must not resolve bindings or be // minted as an email() fact. if verifiedEmail(claims) == "" { @@ -1756,6 +1763,11 @@ func readProtoJSON(w http.ResponseWriter, r *http.Request, msg proto.Message) bo defer func() { _ = r.Body.Close() }() body, err := io.ReadAll(r.Body) if err != nil { + var tooLarge *http.MaxBytesError + if errors.As(err, &tooLarge) { + http.Error(w, "Request body too large", http.StatusRequestEntityTooLarge) + return false + } http.Error(w, "Failed to read body", http.StatusBadRequest) return false } @@ -3259,6 +3271,11 @@ func SetNodeBan(ctx context.Context, store storage.Store, node *storage.Enrolled return fmt.Errorf("stored claims for %s are unreadable: %w", node.PeerID, err) } identity = oidcIdentityKey(claims) + if identity == "" { + // /register refuses a token without a subject, so this is a record + // from before that check. The device ban must still land. + logger.Warnw("Enrollment record carries claims without a subject; banning the device only", "peer_id", node.PeerID) + } } return store.SetNodeAndIdentityBanned(ctx, node.PeerID, identity, banned) } diff --git a/internal/controlplane/server_test.go b/internal/controlplane/server_test.go index c2129993..bb917958 100644 --- a/internal/controlplane/server_test.go +++ b/internal/controlplane/server_test.go @@ -2586,6 +2586,37 @@ func TestAuthDenialPaths(t *testing.T) { } }) + t.Run("oversized operator-plane body answers 413", func(t *testing.T) { + oversized := bytes.Repeat([]byte("a"), maxRequestBodyBytes+1) + req, _ := http.NewRequest(http.MethodPost, baseURL+"/admin/bootstrap-tokens", bytes.NewReader(oversized)) + req.Header.Set("Authorization", "Bearer super-secret-admin-token") + req.Header.Set("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusRequestEntityTooLarge { + t.Errorf("expected 413 for oversized JSON body, got %d", resp.StatusCode) + } + }) + + t.Run("JWT without a subject is rejected", func(t *testing.T) { + // Bans are keyed on issuer|subject; an identity without one could + // never be banned, so it never enrolls. + noSubJWT := mintToken(map[string]interface{}{ + "email": "nobody@example.com", + }) + resp, err := client.Post(baseURL+"/register", "application/x-protobuf", bytes.NewReader(newEnrollBody(noSubJWT))) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusUnauthorized { + t.Errorf("expected 401 for a JWT without sub, got %d", resp.StatusCode) + } + }) + t.Run("admin endpoint rejects wrong token", func(t *testing.T) { req, _ := http.NewRequest("GET", baseURL+"/admin/status", nil) req.Header.Set("Authorization", "Bearer this-is-not-the-admin-token")