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

66 lines
2.0 KiB
TypeScript

import type { PrincipalResolution } from '@/lib/server/authz/principal';
/** Owner metadata stored beside a resource, never inside client-owned content. */
export interface ResourceOwnerBinding {
ownerPrincipalId?: string;
guestPrincipalId?: string;
}
export class ResourceOwnerBindingError extends Error {
constructor(message: string) {
super(message);
this.name = 'ResourceOwnerBindingError';
}
}
export function normalizePrincipalSubjectId(
value: string | undefined,
label: string,
): string | undefined {
if (value === undefined) return undefined;
const normalized = value.trim();
if (!normalized || normalized.length > 256 || /[\u0000-\u001f\u007f]/.test(normalized)) {
throw new ResourceOwnerBindingError(`Invalid ${label}`);
}
return normalized;
}
export function normalizeResourceOwnerBinding(
binding: ResourceOwnerBinding | undefined,
resourceLabel: string,
): ResourceOwnerBinding {
const ownerPrincipalId = normalizePrincipalSubjectId(
binding?.ownerPrincipalId,
'owner principal id',
);
const guestPrincipalId = normalizePrincipalSubjectId(
binding?.guestPrincipalId,
'guest principal id',
);
if (ownerPrincipalId && guestPrincipalId) {
throw new ResourceOwnerBindingError(
`${resourceLabel} cannot have both an account owner and a guest owner`,
);
}
return {
...(ownerPrincipalId ? { ownerPrincipalId } : {}),
...(guestPrincipalId ? { guestPrincipalId } : {}),
};
}
/** Permanent account ownership wins; an unbound device is a migratable guest. */
export function resourceOwnerBindingFromPrincipal(
principal: PrincipalResolution,
): ResourceOwnerBinding {
if (principal.state !== 'authenticated') return {};
if (principal.principal.kind === 'user') {
return { ownerPrincipalId: principal.principal.principalId };
}
if (principal.principal.kind === 'device') {
return principal.principal.userId
? { ownerPrincipalId: principal.principal.userId }
: { guestPrincipalId: principal.principal.principalId };
}
return {};
}