forked from bai/curriculum-project-hub
106 lines
3.7 KiB
TypeScript
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)}` };
|
|
}
|
|
}
|