Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions module/GATEWAY_ROUTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,41 @@ correspondence check.
The generated and explicit route catalogs own the complete module REST surface.
Unknown paths return 404. Module workloads cannot add a proxy target at runtime.

A composed module declared in the solution registry is reached at
`/v1/<alias>/*`, only after both catalogs found no route, with the path
forwarded unchanged (`handleDeclaredModule`). An edge-inspected Work Context
there may name the alias or `module-capabilities`; anything else is 401
`invalid work context`. The route then has two arms:

- **The person's bearer path.** ext_authz verifies the bearer and stamps the
person; accounts admits the viewer to the module's installation per request;
the bearer travels (SA-F-BEARER), the cookie never does;
`x-codefly-installation-id` names the target's admitting installation.
- **The bearer-less arm, on the workload endpoint only**, for a headless
operation context. Selected by: no `Authorization` and no `Cookie` header, the
workload listener, and a capability whose audience is the alias. Then: a
presenter when the gateway is deployed; exactly one capability header and a
non-empty signed tenant and seal (else 401 `invalid work context`); every
identity header stripped, `Authorization`, `Proxy-Authorization` and
`X-Forwarded-Client-Cert` dropped; the budget keyed
`workload-operation:<tenant>`, failing closed; and accounts'
`ListInstallations` for that tenant (ACTIVE, uncached) must show an
installation of the target (else 403) and the seal as an active installation
that is none of the target's and whose registry principal is known and is not
the alias's module principal (else 401 `authentication required`, the answer
a person's capability has without its bearer). The principal check is what
holds when a module's alias and principal differ: accounts seals a person's
capability by principal, the route resolves by alias. 503 when the authority cannot answer or
is not wired. Admitted requests carry no bearer and no identity header, and
`x-codefly-installation-id` is set to the seal, which names the CALLER's
installation, not consent on the target's side. An upstream failure is a
static 502. Every admit and refusal is logged with alias, tenant, task, seal
and presenter, never the token.

Elsewhere the same request is a bearer-less request: 401 on `rest`, 401 with a
`module-capabilities` capability, 401 for a capability sealed to the target's own
installation.

`saas.frontend.plugins.v1` catalogs all 36 Next.js pages and the admin plugin
catch-all. The environment topology generator owns the deployed ingress
VirtualService, so the target-neutral route catalog does not carry a namespace,
Expand Down
38 changes: 38 additions & 0 deletions module/WORK_CONTEXTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,30 @@ answered by one set of functions for minting and verifying alike
is sealed to the organization's installation of it, held while the person is an
active member; an organization that never installed the audience is minted
nothing. A module's is sealed to the installation its mint resolved.
This is an invariant, not a default: every capability a person's own session
or a module's re-mint for that person issues is sealed to the installation of
the audience it addresses. `StartChildSession` is the one derivation core
cannot re-seal (a child keeps its parent's seal), so it refuses a person's
child for an audience whose installation is not the parent's seal.
Two kinds of person-owned capability are sealed elsewhere, by design, and
each has its own mint gate:
- **Source-operation contexts** (`startSourceOperation`,
`pkg/adapters/source_delegation.go`): owned by the person, sealed to the
installation of the module USING the person's source delegation. Only that
module's identity, exercising a delegation the person granted to one of its
bindings, mints one.
- **Installation tasks** (`mintInstallationTask`,
`pkg/adapters/work_context_rpcs.go`): owned by the installation's owner of
record, sealed to the installation the authority is drawn from. Only an
`EXPOSURE_INTERNAL` credential reaches `StartInstallationTask`.

The gateway's bearer-less module arm admits a capability whose seal is an
active installation other than the target's, so it admits these two kinds as
well as module operation contexts: that is the unattended-work family it is
for, and no person's interactive session can produce one.
`TestAPersonsCapabilityIsSealedToItsAudiencesInstallation` pins the rule over
every person mint, and `TestEveryWorkContextMintIsClassifiedBySeal` fails on
any mint RPC that names neither where it seals nor why.
- **Epoch.** The owner's authorization revision in the organization. Membership
and role changes advance a person's; installing a module and every change to
its installation's revision advance a module principal's (migration 35).
Expand Down Expand Up @@ -553,6 +577,20 @@ A composed module reaches it through the gateway's `POST
/modules/_operation-context` ([services/auth-gateway/AGENTS.md](./services/auth-gateway/AGENTS.md)),
which brokers to accounts exactly as `/modules/_work-context` does.

The minted context is presented at `/v1/<audience>/*` on the same workload
endpoint, with no bearer: the declared module route's bearer-less arm. A
composed workload with no person present, such as a solution's release
publisher, reaches the module it is bound to this way; modules keep calling
their peers in-mesh. The edge checks what core's `Inspector` can (signature,
issuer, window, the route's audience, shape), then the signed tenant's ACTIVE
installations: the target must be installed there, and the seal must name a
different active installation, which is what unattended work carries and what a
person's capability never does (see the seal rule above). It strips every
identity header, meters on the tenant, forwards the capability as presented and
stamps only `x-codefly-installation-id` with the seal. The callee verifies the
capability at full strength, as for any other carrier. The route's rules are in
[GATEWAY_ROUTES.md](./GATEWAY_ROUTES.md#module-rest-routes).

## Operation contexts from a source delegation

A datasource source's sync is neither of the cases above. It runs as a module
Expand Down
Binary file modified module/contracts/api/accounts/authority/contract.binpb
Binary file not shown.
11 changes: 11 additions & 0 deletions module/contracts/api/accounts/authority/proto/buf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,23 @@ breaking:
# SolutionBackendBinding (#1118): the reported upstream half, deleted
# with the report — a solution's address is resolved, never recorded.
- saas/accounts/v1/solution_registry.proto
# Installation, ListInstallationsRequest, InstallationSummary and
# ListInstallationsResponse moved to installation_listing.proto so the
# auth-gateway can generate the ListInstallations messages from the same
# source file without vendoring the policy options and google.api this
# file imports. Same package, same fully-qualified names, same wire
# format; every RPC stays where it was.
- saas/accounts/v1/installations.proto
# ModuleMintRegistrationRequest/Response and
# SolutionMintRegistrationRequest/Response (#1056): the registration
# credentials and the secrets that minted them are deleted. Presence is
# delivered, so nothing registers at runtime and nothing verifies either
# credential.
- saas/accounts/v1/module_registration.proto
# InstallationStatus and InstallationHealth moved to
# installation_listing.proto with the messages above, for the same reason.
ENUM_NO_DELETE:
- saas/accounts/v1/installations.proto
# ListMyAccessibleScopes moved from PermissionService to the new
# AccessibleScopeService in accessible_scopes.proto (issue #577), which changes
# its Connect/gRPC procedure path. Its REST route (GET /v1/accessible-scopes) is
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
syntax = "proto3";

package saas.accounts.v1;

import "buf/validate/validate.proto";
import "google/protobuf/timestamp.proto";

// The installation listing: an installation, its lifecycle and health, and the
// messages InstallationService/ListInstallations takes and answers.
//
// These definitions live in their own file, with deliberately minimal imports,
// so the auth-gateway can generate the same types from the same source proto
// and call ListInstallations over the internal listener, as it does for
// module_registration.proto and solution_entitlements.proto. installations.proto
// imports this file; the service and every other installation message stay
// there. Sharing the generated definition rather than restating the wire shape
// is what keeps the two sides from drifting: the gateway's copy is this file,
// byte for byte.

// InstallationStatus is the lifecycle of an installation row. An uninstall is a
// soft delete (status=revoked, revoked_at set) so the agent, grants, and audit
// trail remain resolvable.
enum InstallationStatus {
INSTALLATION_STATUS_UNSPECIFIED = 0;
INSTALLATION_STATUS_ACTIVE = 1;
INSTALLATION_STATUS_REVOKED = 2;
// Installed, and waiting for the solution to acknowledge its standard install
// step. Nothing can use a pending installation.
INSTALLATION_STATUS_PENDING = 3;
// The solution refused its install step, or never answered within the
// attempt budget. Nothing can use it; an administrator may retry it.
INSTALLATION_STATUS_FAILED = 4;
}

// InstallationHealth is resolved live from current authority facts, never
// stored. It is the observable form of the fail-closed mint rule: a
// StartInstallationTask fails exactly when health is not HEALTHY, and transfer
// or re-enable restores it. NO_ELIGIBLE_OWNER means neither the owner of record
// nor any co-owner is currently an org admin (the Zapier-class "connection owner
// left the org" failure), surfaced rather than silently reassigned.
enum InstallationHealth {
INSTALLATION_HEALTH_UNSPECIFIED = 0;
INSTALLATION_HEALTH_HEALTHY = 1;
INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER = 2;
INSTALLATION_HEALTH_AGENT_REVOKED = 3;
INSTALLATION_HEALTH_AGENT_DISABLED = 4;
INSTALLATION_HEALTH_STANDING_GRANT_MISSING = 5;
}

// Installation is one solution's bound identity in an org. Server-assigned ids;
// the browser cannot attest to tenancy, the agent principal, or the scope node.
message Installation {
string id = 1 [(buf.validate.field).string.uuid = true];
string org_id = 2 [(buf.validate.field).string.uuid = true];
string agent_principal_id = 3 [(buf.validate.field).string.uuid = true];
// Was `solution_identifier`: the free-text route alias. It was the join key
// every reader used, and because an alias is deliberately reusable by a later
// binding, an installation that named one inherited whatever took it next.
// Replaced by target_id below. Reserved rather than reused so no consumer can
// read an alias out of a field that now carries an identity.
reserved 4;
reserved "solution_identifier";
string owner_principal_id = 5 [(buf.validate.field).string.uuid = true];
repeated string co_owner_principal_ids = 6;
string root_scope_node_id = 7 [(buf.validate.field).string.uuid = true];
InstallationStatus status = 8;
google.protobuf.Timestamp created_at = 9;
optional google.protobuf.Timestamp revoked_at = 10;
// The immutable solution target this installation was consented to: one
// continuous period of one binding's presence on this host. It is never
// reused, so a replacement binding — even one claiming the same route alias —
// is a different target and inherits nothing.
//
// Deliberately NOT accompanied by the route alias. The alias is a property of
// the target that a later generation may move, and resolving it here would
// mean joining the host's presence state into a request-path listing — which
// would require granting request traffic read over every organisation's
// presence to answer about one. A consumer that needs the alias asks
// ListAvailableSolutions, which serves accepted applied state, and joins on
// this id.
//
// Empty only on a revoked historical row whose target could not be
// established at the cutover; an active installation always names one.
string target_id = 11 [
(buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
(buf.validate.field).string.uuid = true
];
// Why a revoked installation was revoked, when it was not an administrator
// uninstalling: the installation-identity cutover, or a tenancy narrowing that
// withdrew this organisation. Empty on an active row.
string revoked_reason = 12 [(buf.validate.field).string.max_len = 512];
// Why the install step failed: the solution's bounded refusal reason, or a
// fixed reason when it never answered. Empty unless status is FAILED.
string failure_reason = 13 [(buf.validate.field).string.max_len = 512];
// The module principal this installation was installed under: the registry
// principal whose delivered authority named the target's binding, written at
// install and never changed. A person's capability for a module audience is
// sealed to the active installation whose registry principal is that
// audience's, so a reader that must tell a person's seal from unattended work
// compares this, not the route alias. Every install through InstallSolution,
// a module's or a solution's, records one. Empty only on a row written before
// the column existed (it is deliberately not backfilled: no principal is
// inferred) or one the store wrote with no registry principal. A capability
// a module or solution identity mints is sealed only to a row carrying its
// principal. A reader treats empty as unknown, never as "not this principal".
string registry_principal_id = 14 [
(buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
(buf.validate.field).string.uuid = true
];
}

// ListInstallationsRequest asks what one organization has installed. Every other
// RPC on this service names a single installation, so until this existed nothing
// could answer "what has this org installed?" — which is the authority read a
// per-organization solution projection is built on (issue #949).
message ListInstallationsRequest {
string org_id = 1 [(buf.validate.field).string.uuid = true];
// Restrict to one lifecycle status. UNSPECIFIED returns every status, so a
// caller that must see only live installs asks for ACTIVE explicitly rather
// than leaning on a default that would silently widen if the enum grew.
InstallationStatus status = 2;
// Page bound. 0 -> server default; capped so an organization with many
// installations can never return an unbounded result in one call.
int32 page_size = 3 [(buf.validate.field).int32 = {
gte: 0
lte: 500
}];
// Opaque cursor from a previous response's next_page_token; empty for page one.
string page_token = 4;
}

// InstallationSummary is one row of the listing: the installation, plus the health
// resolved live beside it exactly as GetInstallation resolves it for one. Health
// travels with the row because a consumer that renders an installed solution as
// unavailable would otherwise need one GetInstallation per installation.
message InstallationSummary {
Installation installation = 1;
InstallationHealth health = 2;
}

message ListInstallationsResponse {
repeated InstallationSummary installations = 1;
// Set when more installations remain; pass it back as page_token. Empty on the
// last page.
string next_page_token = 2;
}
Loading
Loading