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)

RoleCan
viewerSee the resource and its access roster
deployerDeploy (with an environment-side deployer too — see intersection)
editorUpdate versions / edit the resource
ownerEverything above + manage grants + transfer ownership

Effective role resolution

A caller’s effective role on a resource is the maximum of three independent arms:

  1. Implicit owner — the resource creator. For a release this is release_json.createdBy (the creating caller’s subject); an environment has no implicit owner.
  2. Explicit grant — an active resource_share_grants row for the caller as user, one of their groups, or everyone (precedence user > group > everyone).
  3. Legacy-scope shim — a compatibility bridge from token scopes/roles: release:read → Viewer, release:write → Editor, release:publish → Deployer, tenant admin → 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.

MethodPathAuthPurpose
GET/releases/{id}/accesseffective ≥ ViewerList the access roster
POST/releases/{id}/accesseffective ≥ OwnerGrant a role to a principal
DELETE/releases/{id}/access/{grantId}effective ≥ OwnerRevoke a grant (idempotent)
GET/releases/{id}/access/effectivebaselineThe caller’s own effective role (decomposed, as above)
POST/releases/{id}/access/transfereffective ≥ OwnerTransfer ownership to another user
GET/access/effective?releaseId=&environmentId=baselineCombined 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:

  1. Grants the target an explicit Owner grant (roster-visible; emits release.access.granted).
  2. Reassigns the implicit-owner signal to the target — for a release this rewrites release_json.createdBy (so the prior creator’s isImplicitOwner becomes false); for an environment there is no implicit owner, so this step is a no-op.
  3. Revokes the prior owner’s explicit Owner grant (if any).
  4. Emits a release.access.transferred audit event recording from → to and transferredBy.

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

ActionEmitted by
release.access.grantedgrant + step 1 of transfer
release.access.transferredtransfer (records from, to, transferredBy)

Grant/revoke history is also durably recorded on resource_share_grants (granted_by/granted_at, revoked_by/revoked_at).

References