Files
curriculum-project-hub/hub/src/permission.ts
T

106 lines
3.7 KiB
TypeScript

/**
* Permission compatibility exports.
*
* ADR-0019 moves production authorization behind PrincipalResolver +
* PermissionAuthorizer. The wrappers below preserve the old helper names for
* tests and incremental callers while routing project trigger checks through
* the new authorizer.
*/
import type { PrismaClient } from "@prisma/client";
import { createPermissionAuthorizer } from "./permissions/authorizer.js";
import { inactiveOrganizationReason } from "./org/status.js";
export { createPermissionAuthorizer, PrismaPermissionAuthorizer, roleGrantsEdit } from "./permissions/authorizer.js";
export { PrismaPrincipalResolver, principalKey } from "./permissions/principals.js";
export { syncExternalPrincipalMemberships } from "./permissions/externalSync.js";
export { grantTeamProjectAccess, listProjectTeamAccess, revokeTeamProjectAccess } from "./permissions/projectTeamAccess.js";
export type {
AuthorizationAction,
AuthorizationDecision,
AuthorizationRequest,
AuthorizationResource,
PermissionAuthorizer,
} from "./permissions/authorizer.js";
export type {
ActorInput,
PrincipalRef,
PrincipalResolution,
PrincipalResolver,
ResolvedPrincipal,
} from "./permissions/principals.js";
export type {
ExternalPrincipalMembershipInput,
ExternalPrincipalType,
SyncExternalPrincipalMembershipsInput,
SyncExternalPrincipalMembershipsResult,
} from "./permissions/externalSync.js";
export type {
GrantTeamProjectAccessInput,
ProjectTeamAccessEntry,
RevokeTeamProjectAccessInput,
} from "./permissions/projectTeamAccess.js";
export interface PermissionResult {
readonly allowed: boolean;
readonly reason: string;
}
export async function canTriggerAgent(
prisma: PrismaClient,
projectId: string,
principal: string,
): Promise<PermissionResult> {
try {
const decision = await createPermissionAuthorizer(prisma).can({
actor: { feishuOpenId: principal },
action: "agent.trigger",
resource: { type: "PROJECT", id: projectId },
});
return { allowed: decision.allowed, reason: decision.reason };
} catch (e) {
return { allowed: false, reason: `permission check failed: ${e instanceof Error ? e.message : String(e)}` };
}
}
/**
* Role-only legacy helper. New trigger code uses `role.trigger` on
* PermissionAuthorizer so project edit+ and role grant composition happen in
* one decision.
*/
export async function canTriggerRole(
prisma: PrismaClient,
projectId: string,
roleId: string,
principal: string,
): Promise<PermissionResult> {
try {
const project = await prisma.project.findUnique({
where: { id: projectId },
select: { organization: { select: { id: true, status: true } } },
});
if (project === null) return { allowed: false, reason: `project not found: ${projectId}` };
const inactiveReason = inactiveOrganizationReason(project.organization.id, project.organization.status);
if (inactiveReason !== undefined) return { allowed: false, reason: inactiveReason };
const anyGrant = await prisma.roleTriggerGrant.findFirst({
where: { projectId, roleId },
select: { id: true },
});
if (anyGrant === null) {
return { allowed: true, reason: `role ${roleId} unconfigured on project (open)` };
}
const grant = await prisma.roleTriggerGrant.findFirst({
where: { projectId, roleId, principalType: "USER", principalId: principal, revokedAt: null },
select: { id: true },
});
if (grant !== null) {
return { allowed: true, reason: `granted role ${roleId}` };
}
return {
allowed: false,
reason: `no active ${roleId} grant for ${principal} on project ${projectId}`,
};
} catch (e) {
return { allowed: false, reason: `role permission check failed: ${e instanceof Error ? e.message : String(e)}` };
}
}