Authentication & Authorization
Authentication Methods
OAuth 2.0 for Human Users
┌──────────────────────────────────────────────────────────────────────────────┐
│ OAUTH 2.0 AUTHORIZATION CODE FLOW │
│ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ Browser │ │ Authority │ │
│ └────┬─────┘ └──────┬───────┘ │
│ │ │ │
│ │ 1. Login request │ │
│ │ ────────────────────────────────────► │ │
│ │ │ │
│ │ 2. Redirect to IdP │ │
│ │ ◄──────────────────────────────────── │ │
│ │ │ │
│ │ 3. User authenticates at IdP │ │
│ │ ─────────────────────────────────► │ │
│ │ │ │
│ │ 4. IdP callback with code │ │
│ │ ◄──────────────────────────────────── │ │
│ │ │ │
│ │ 5. Exchange code for tokens │ │
│ │ ────────────────────────────────────► │ │
│ │ │ │
│ │ 6. Access token + refresh token │ │
│ │ ◄──────────────────────────────────── │ │
│ │ │ │
└──────────────────────────────────────────────────────────────────────────────┘
mTLS for Agents
Agents authenticate using mutual TLS with certificates issued by Stella’s internal CA.
Registration Flow:
- Admin generates one-time registration token
- Agent starts with registration token
- Agent submits CSR (Certificate Signing Request)
- Authority issues certificate signed by Stella CA
- Agent uses certificate for all subsequent requests
API Keys for Service-to-Service
External services can use API keys for programmatic access:
- Keys are tenant-scoped
- Keys can have restricted permissions
- Keys can have expiration dates
- Key usage is audited
JWT Token Structure
Access Token Claims
interface AccessTokenClaims {
// Standard claims
iss: string; // "https://authority.stella.local"
sub: string; // User ID
aud: string[]; // ["stella-api"]
exp: number; // Expiration timestamp
iat: number; // Issued at timestamp
jti: string; // Unique token ID
// Custom claims
tenant_id: string;
roles: string[];
permissions: Permission[];
email?: string;
name?: string;
}
Token Lifetimes
| Token Type | Lifetime | Refresh |
|---|---|---|
| Access Token | 15 minutes | Via refresh token |
| Refresh Token | 7 days | Rotated on use |
| Agent Token | 1 hour | Via mTLS connection |
| API Key | Configurable | Not refreshed |
Authorization Model
Resource Types
type ResourceType =
| "environment"
| "release"
| "promotion"
| "target"
| "agent"
| "workflow"
| "plugin"
| "integration"
| "evidence";
Action Types
type ActionType =
| "create"
| "read"
| "update"
| "delete"
| "execute"
| "approve"
| "deploy"
| "rollback";
Permission Structure
interface Permission {
resource: ResourceType;
action: ActionType;
scope?: PermissionScope;
conditions?: Condition[];
}
type PermissionScope =
| "*" // All resources
| { environmentId: UUID } // Specific environment
| { labels: Record<string, string> }; // Label-based
Built-in Roles
| Role | Description | Key Permissions |
|---|---|---|
admin | Full access | All permissions |
release_manager | Manage releases and promotions | Create releases, request promotions |
deployer | Execute deployments | Approve promotions (where allowed), view releases |
approver | Approve promotions | Approve promotions (SoD respected) |
viewer | Read-only access | Read all resources |
agent | Agent service account | Execute deployment tasks |
Role Definitions
const roles = {
admin: {
permissions: [
{ resource: "*", action: "*" }
]
},
release_manager: {
permissions: [
{ resource: "release", action: "create" },
{ resource: "release", action: "read" },
{ resource: "release", action: "update" },
{ resource: "promotion", action: "create" },
{ resource: "promotion", action: "read" },
{ resource: "environment", action: "read" },
{ resource: "workflow", action: "read" },
{ resource: "workflow", action: "execute" }
]
},
deployer: {
permissions: [
{ resource: "release", action: "read" },
{ resource: "promotion", action: "read" },
{ resource: "promotion", action: "approve" },
{ resource: "environment", action: "read" },
{ resource: "target", action: "read" },
{ resource: "agent", action: "read" }
]
},
approver: {
permissions: [
{ resource: "promotion", action: "read" },
{ resource: "promotion", action: "approve" },
{ resource: "release", action: "read" },
{ resource: "environment", action: "read" }
]
},
viewer: {
permissions: [
{ resource: "*", action: "read" }
]
}
};
Environment-Scoped Permissions
Permissions can be scoped to specific environments:
// User can approve promotions only to staging
{
resource: "promotion",
action: "approve",
scope: { environmentId: "staging-env-id" }
}
// User can deploy only to targets with specific labels
{
resource: "target",
action: "deploy",
scope: { labels: { "tier": "frontend" } }
}
Separation of Duties (SoD)
When SoD is enabled for an environment:
- The user who requested a promotion cannot approve it
- The user who created a release cannot be the sole approver
- Approval records include SoD verification status
interface ApprovalValidation {
promotionId: UUID;
approverId: UUID;
requesterId: UUID;
sodRequired: boolean;
sodSatisfied: boolean;
validationResult: "valid" | "self_approval_denied" | "sod_violation";
}
Permission Checking Algorithm
async function checkPermission(
userId: UUID,
resource: ResourceType,
action: ActionType,
resourceId?: UUID
): Promise<boolean> {
// 1. Get user's roles and direct permissions
const userRoles = await getUserRoles(userId);
const userPermissions = await getUserPermissions(userId);
// 2. Expand role permissions
const rolePermissions = userRoles.flatMap(r => roles[r].permissions);
const allPermissions = [...rolePermissions, ...userPermissions];
// 3. Check for matching permission
for (const perm of allPermissions) {
if (matchesResource(perm.resource, resource) &&
matchesAction(perm.action, action) &&
matchesScope(perm.scope, resourceId) &&
evaluateConditions(perm.conditions)) {
return true;
}
}
return false;
}
function matchesResource(pattern: string, resource: string): boolean {
return pattern === "*" || pattern === resource;
}
function matchesAction(pattern: string, action: string): boolean {
return pattern === "*" || pattern === action;
}
API Authorization Headers
All API requests require:
Authorization: Bearer <access_token>
For agent requests (over mTLS):
X-Agent-Id: <agent_id>
Authorization: Bearer <agent_token>
Permission Denied Response
{
"success": false,
"error": {
"code": "PERMISSION_DENIED",
"message": "User does not have permission to approve promotions to production",
"details": {
"resource": "promotion",
"action": "approve",
"scope": { "environmentId": "prod-env-id" },
"requiredRoles": ["admin", "approver"],
"userRoles": ["viewer"]
}
}
}
Deployment Share Grants (resource-scoped delegation)
Full model, endpoints, and tables: architecture — Deployment Share Grants. Decision record: ADR-027.
A deployment share grant lets an operator delegate a single deployment’s read or operate/deploy capability to a specific authenticated person, time-boxed, revocable, and audited. It is a ReleaseOrchestrator-local authorization mechanism, deliberately not a new global scope and not an Authority/identity-envelope change.
Key authorization properties (all server-enforced in DeploymentShareGrantService):
- Account-bound, never link-bound. The share link carries an opaque
grantId(dsg_<32 hex>), not a credential. Opening it is unprivileged; an unauthenticated accept/use returns 401. Only the authenticated subject who accepts (POST .../share/{grantId}/accept) is elevated. - Resource-scoped, not tenant-wide. An
Operate & Deploygrant confersorch:operatefor the one deployment (deploymentId) andrelease:publishfor the one release into the one target environment (releaseId+environmentId). It never authorizes another deployment, release, or environment. This is why the capability cannot be a coarse scope — resource scoping lives inrelease_orchestratorgrant rows, checked at the endpoint. - No amplification. The issuer must currently hold the capability being delegated (
View⇒release:read;Operate & Deploy⇒orch:operateANDrelease:publish) or be a global admin; else create returns 403. - Same-tenant only. Grant reads/writes are tenant-scoped (PK
(tenant_id, grant_id)); a recipient in another tenant gets 404. ASpecificSubjectsaudience binds an explicit subject list. - Production 4-eyes. An elevated grant whose target environment type is Production is created
PendingApprovaland is inert (View-only) — even after acceptance — until a second authorized person (global admin OR holder of the full delegated capability, ≠ the issuer) approves it. This composes with the existing promotion separation-of-duties; it does not replace it. - Native scope path is never weakened. The operate/initiate endpoints authorize on the native scope OR an active, accepted resource-scoped grant. A holder of the native scope passes exactly as before; the grant is purely additive. Native scopes by action: operate (pause/resume/cancel/retry) ⇒
orch:operateORrelease:write; rollback and initiate (deploy/promote) ⇒release:publish; global admin always. Operate acceptsorch:operateper that scope’s definition (“execute orchestrator control actions — cancel, retry, …”) and the Console’scanOperateOrchestrator()gate; rollback/initiate keep the higherrelease:publishbar because they change what is deployed. - Attributable. Every grant-backed privileged action emits a
release.share.usedaudit event with the real acting subject +grantId+ capability + route + resource ids (in addition to the lifecycle eventsrelease.share.{created,toggled,accepted,approved,revoked}). The native-scope path emits noshare.used.
