Files
openmaic/OpenMAIC/lib/server/authz/resource-ownership.ts
2026-08-16 14:58:47 +08:00

97 lines
3.2 KiB
TypeScript

import type { PrincipalResolution } from '@/lib/server/authz/principal';
export type ResourceAuthorizationAction = 'read' | 'write' | 'cancel' | 'resume' | 'delete';
export type ResourceOwnership =
| { readonly state: 'owned'; readonly ownerPrincipalId: string }
| { readonly state: 'guest-owned'; readonly guestPrincipalId: string }
| { readonly state: 'public-read' }
| { readonly state: 'legacy-unowned' }
| { readonly state: 'unknown' };
export type ResourceAuthorizationDecision =
| {
readonly effect: 'allow';
readonly reason: 'owner_match' | 'guest_match' | 'public_read' | 'explicit_capability';
}
| {
readonly effect: 'deny';
readonly reason: 'unauthenticated' | 'owner_mismatch' | 'action_not_allowed';
}
| {
readonly effect: 'indeterminate';
readonly reason: 'resolver_unavailable' | 'owner_missing' | 'owner_unknown';
};
export interface ResourceAuthorizationInput {
readonly principal: PrincipalResolution;
readonly ownership: ResourceOwnership;
readonly action: ResourceAuthorizationAction;
/**
* An exact capability which may override an owner mismatch. Principal kind
* alone never grants elevated access.
*/
readonly overrideCapability?: string;
}
/** Pure ownership policy. It performs no authentication and no I/O. */
export function decideResourceAuthorization(
input: ResourceAuthorizationInput,
): ResourceAuthorizationDecision {
const { ownership, principal, action } = input;
if (ownership.state === 'legacy-unowned') {
return { effect: 'indeterminate', reason: 'owner_missing' };
}
if (
ownership.state === 'unknown' ||
(ownership.state === 'owned' && ownership.ownerPrincipalId.trim().length === 0) ||
(ownership.state === 'guest-owned' && ownership.guestPrincipalId.trim().length === 0)
) {
return { effect: 'indeterminate', reason: 'owner_unknown' };
}
if (ownership.state === 'public-read') {
return action === 'read'
? { effect: 'allow', reason: 'public_read' }
: { effect: 'deny', reason: 'action_not_allowed' };
}
if (principal.state === 'unavailable') {
return { effect: 'indeterminate', reason: 'resolver_unavailable' };
}
if (principal.state === 'anonymous') {
return { effect: 'deny', reason: 'unauthenticated' };
}
if (
ownership.state === 'owned' &&
(principal.principal.principalId === ownership.ownerPrincipalId ||
principal.principal.userId === ownership.ownerPrincipalId)
) {
return { effect: 'allow', reason: 'owner_match' };
}
if (
ownership.state === 'guest-owned' &&
principal.principal.kind === 'device' &&
principal.principal.principalId === ownership.guestPrincipalId
) {
return { effect: 'allow', reason: 'guest_match' };
}
if (
input.overrideCapability &&
principal.principal.capabilities.includes(input.overrideCapability)
) {
return { effect: 'allow', reason: 'explicit_capability' };
}
return { effect: 'deny', reason: 'owner_mismatch' };
}
/** Future enforcement must fail closed for deny and indeterminate decisions. */
export function isResourceAuthorizationAllowed(decision: ResourceAuthorizationDecision): boolean {
return decision.effect === 'allow';
}