export interface IToken { token: string; expiresAt: number; assignedRoles: string[]; } export type TServiceDeploymentCapability = | 'deployment:preflight' | 'deployment:reserve' | 'deployment:registry-push' | 'deployment:release-read' | 'deployment:promote-image' | 'deployment:adopt-existing' | 'deployment:status' | 'deployment:create-greenfield' | 'deployment:promote-route' | 'deployment:rollback-image' | 'deployment:rollback-route' | 'deployment:retry' | 'deployment:cleanup'; /** * Server-persisted authority for one logical service. These grants are never * accepted from the identity payload supplied by a caller; Cloudly reloads * the owning user from the verified JWT before evaluating them. */ interface IServiceDeploymentGrantBase { organizationId: string; serviceId: string; capabilities: TServiceDeploymentCapability[]; } /** Authority over one service that already exists and has the same owner. */ export interface IExistingServiceDeploymentGrant extends IServiceDeploymentGrantBase { scope: 'service'; } /** Authority to create one exact, not-yet-existing service ID in an organization. */ export interface IOrganizationServiceSlotDeploymentGrant extends IServiceDeploymentGrantBase { scope: 'organization-service-slot'; } export type IServiceDeploymentGrant = | IExistingServiceDeploymentGrant | IOrganizationServiceSlotDeploymentGrant; const serviceDeploymentCapabilities = new Set([ 'deployment:preflight', 'deployment:reserve', 'deployment:registry-push', 'deployment:release-read', 'deployment:promote-image', 'deployment:adopt-existing', 'deployment:status', 'deployment:create-greenfield', 'deployment:promote-route', 'deployment:rollback-image', 'deployment:rollback-route', 'deployment:retry', 'deployment:cleanup', ]); const deploymentBoundaryIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/; export const validateServiceDeploymentGrant = ( grantArg: unknown, ): string[] => { const errors: string[] = []; if (!grantArg || typeof grantArg !== 'object' || Array.isArray(grantArg)) { return ['grant must be an object']; } const grant = grantArg as Record; if (grant.scope !== 'service' && grant.scope !== 'organization-service-slot') { errors.push('scope must identify an existing service or an exact future service slot'); } if (typeof grant.organizationId !== 'string' || !deploymentBoundaryIdentifierRegex.test(grant.organizationId)) { errors.push('organizationId must be a bounded canonical identifier'); } if (typeof grant.serviceId !== 'string' || !deploymentBoundaryIdentifierRegex.test(grant.serviceId)) { errors.push('serviceId must be a bounded canonical identifier'); } const capabilities = Array.isArray(grant.capabilities) ? grant.capabilities : undefined; if (!capabilities || capabilities.length < 1 || capabilities.length > serviceDeploymentCapabilities.size || new Set(capabilities).size !== capabilities.length || capabilities.some((capabilityArg) => ( typeof capabilityArg !== 'string' || !serviceDeploymentCapabilities.has(capabilityArg as TServiceDeploymentCapability) ))) { errors.push('capabilities must be a non-empty unique set of supported deployment capabilities'); } if (capabilities && grant.scope === 'service' && capabilities.includes('deployment:create-greenfield')) { errors.push('an existing-service grant cannot authorize greenfield creation'); } if (capabilities && grant.scope === 'organization-service-slot' && !capabilities.includes('deployment:create-greenfield')) { errors.push('an organization-service-slot grant must authorize greenfield creation'); } if (capabilities && grant.scope !== 'service' && capabilities.includes('deployment:adopt-existing')) { errors.push('deployment:adopt-existing requires an existing-service grant'); } return errors; }; export interface IIdentityCredential { jwt: string; } /** * an identity is assumed by authentication as a user * an identity is ephemeral and has to be renewed regularly */ export interface IIdentity extends IIdentityCredential { name: string; userId: string; type: 'machine' | 'human'; role: 'admin' | 'user' | 'api' | 'cluster'; expiresAt: number; } export interface IUser { id: string; data: { type: 'machine' | 'human'; role: 'admin' | 'user' | 'api' | 'cluster'; username?: string; password?: string; tokens?: IToken[]; serviceDeploymentGrants?: IServiceDeploymentGrant[]; } }