Resource Access & Sharing API
Base path: /api/v1/release-orchestrator (a named alias /api/release-orchestrator is also mapped).
Part of the “Airline and the Flight” grant fabric (F1–F3). Releases and environments are owned, shareable resources: this family lists, grants, revokes, and transfers the role grants that back the console Access tab and the promotion wizard’s access-intersection panel.
Model
Roles (ascending)
| Role | Can |
|---|---|
viewer | See the resource and its access roster |
deployer | Deploy (with an environment-side deployer too — see intersection) |
editor | Update versions / edit the resource |
owner | Everything above + manage grants + transfer ownership |
Effective role resolution
A caller’s effective role on a resource is the maximum of three independent arms:
- Implicit owner — the resource creator. For a release this is
release_json.createdBy(the creating caller’s subject); an environment has no implicit owner. - Explicit grant — an active
resource_share_grantsrow for the caller asuser, one of theirgroups, oreveryone(precedence user > group > everyone). - Legacy-scope shim — a compatibility bridge from token scopes/roles:
release:read→ Viewer,release:write→ Editor,release:publish→ Deployer, tenantadmin→ Owner.
GET .../access/effective returns each arm separately so the source of authority is legible:
{
"resourceType": "release",
"resourceId": "rel-...",
"role": "owner", // the effective Max of the three arms
"isImplicitOwner": true, // arm 1 — createdBy matches the caller
"explicitRole": "none", // arm 2 — active grant role (or "none")
"explicitVia": null, // "user" | "group" | "everyone"
"shimRole": "owner", // arm 3 — legacy-scope shim contribution
"canView": true, "canDeploy": true, "canEdit": true, "canManageGrants": true
}
Endpoints
Both releases/{id} and environments/{id} expose the same family. The group baseline is release:read + a tenant.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /releases/{id}/access | effective ≥ Viewer | List the access roster |
POST | /releases/{id}/access | effective ≥ Owner | Grant a role to a principal |
DELETE | /releases/{id}/access/{grantId} | effective ≥ Owner | Revoke a grant (idempotent) |
GET | /releases/{id}/access/effective | baseline | The caller’s own effective role (decomposed, as above) |
POST | /releases/{id}/access/transfer | effective ≥ Owner | Transfer ownership to another user |
GET | /access/effective?releaseId=&environmentId= | baseline | Combined effective for the deploy intersection |
environments/{id}/... mirrors every route above.
Grant a role — POST .../access
{ "principalType": "user" | "group" | "everyone", "principalId": "<subject-or-group-id>", "role": "viewer|deployer|editor|owner" }
principalId is omitted/ignored for everyone. Returns 201 with the created grant. Emits a release.access.granted audit event.
Transfer ownership — POST .../access/transfer
{ "principalType": "user", "principalId": "<new-owner-subject>" }
Owner-only. Ownership is held by an accountable individual, so the target must be a user (make a group/everyone an owner via the ordinary grant endpoint with role=owner). The operation:
- Grants the target an explicit Owner grant (roster-visible; emits
release.access.granted). - Reassigns the implicit-owner signal to the target — for a release this rewrites
release_json.createdBy(so the prior creator’sisImplicitOwnerbecomesfalse); for an environment there is no implicit owner, so this step is a no-op. - Revokes the prior owner’s explicit Owner grant (if any).
- Emits a
release.access.transferredaudit event recordingfrom → toandtransferredBy.
Because authority is the Max of the three arms, a tenant admin still resolves owner after transferring away — via the shim arm only (shimRole=owner, isImplicitOwner=false). A non-admin prior owner drops to None. Transfer is a 3-step non-atomic sequence; a mid-sequence failure fails toward an incomplete-but-safe state (no lockout, no privilege escalation). The true creation record is preserved in the immutable created release event.
Deploy intersection
Deploying a release to an environment requires the caller to hold ≥ Deployer on the release AND ≥ Deployer on the environment. GET /access/effective?releaseId=…&environmentId=… returns both effective roles plus a canDeploy boolean for the promotion wizard’s green light.
Audit actions
| Action | Emitted by |
|---|---|
release.access.granted | grant + step 1 of transfer |
release.access.transferred | transfer (records from, to, transferredBy) |
Grant/revoke history is also durably recorded on resource_share_grants (granted_by/granted_at, revoked_by/revoked_at).
