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

74 lines
2.4 KiB
TypeScript

import {
resolveRequestPrincipal,
type PrincipalResolution,
type RequestPrincipalResolver,
} from '@/lib/server/authz/principal';
import {
decideResourceAuthorization,
type ResourceAuthorizationAction,
type ResourceAuthorizationDecision,
type ResourceOwnership,
} from '@/lib/server/authz/resource-ownership';
export interface ResourceAuthorizationObservation {
readonly resourceType: string;
readonly resourceId: string;
readonly action: ResourceAuthorizationAction;
readonly principalState: PrincipalResolution['state'];
readonly ownershipState: ResourceOwnership['state'];
readonly decision: ResourceAuthorizationDecision;
}
export type ResourceAuthorizationObserver = (
observation: ResourceAuthorizationObservation,
) => void | Promise<void>;
export interface ObserveResourceAuthorizationInput {
readonly request: Request;
readonly resourceType: string;
readonly resourceId: string;
readonly action: ResourceAuthorizationAction;
readonly ownership: ResourceOwnership;
readonly overrideCapability?: string;
}
export interface ResourceAuthorizationObservationDeps {
readonly resolvePrincipal?: RequestPrincipalResolver;
readonly observer?: ResourceAuthorizationObserver;
}
const ignoreObservation: ResourceAuthorizationObserver = () => undefined;
/**
* Observe a future authorization decision without enforcing it.
*
* The function intentionally returns no decision, and every resolver/observer
* failure is contained. A caller's existing response must never depend on this
* migration seam.
*/
export async function observeResourceAuthorization(
input: ObserveResourceAuthorizationInput,
deps: ResourceAuthorizationObservationDeps = {},
): Promise<void> {
try {
const principal = await (deps.resolvePrincipal ?? resolveRequestPrincipal)(input.request);
const decision = decideResourceAuthorization({
principal,
ownership: input.ownership,
action: input.action,
...(input.overrideCapability ? { overrideCapability: input.overrideCapability } : {}),
});
await (deps.observer ?? ignoreObservation)({
resourceType: input.resourceType,
resourceId: input.resourceId,
action: input.action,
principalState: principal.state,
ownershipState: input.ownership.state,
decision,
});
} catch {
// Observe-only means failures must never change route status/body or throw.
}
}