fix: enforce active organization boundary

This commit is contained in:
2026-07-10 18:42:12 +08:00
parent f07f280b8f
commit d730e51c3d
24 changed files with 1686 additions and 460 deletions
+32 -8
View File
@@ -1,5 +1,6 @@
import type { PermissionResourceType, PermissionRole, PrismaClient, PrincipalType } from "@prisma/client";
import type { OrganizationStatus, PermissionResourceType, PermissionRole, PrismaClient, PrincipalType } from "@prisma/client";
import { PrismaPrincipalResolver, type ActorInput, type PrincipalRef, type PrincipalResolution, type PrincipalResolver } from "./principals.js";
import { inactiveOrganizationReason } from "../org/status.js";
export type AuthorizationAction =
| "project.read"
@@ -61,8 +62,16 @@ export class PrismaPermissionAuthorizer implements PermissionAuthorizer {
if (req.action === "role.trigger" && (req.roleId === undefined || req.roleId === "")) {
throw new Error("role.trigger authorization requires roleId");
}
const organizationId = await this.organizationForResource(req.resource);
const resolution = await this.resolver.resolveActor(req.actor, { organizationId });
const organization = await this.organizationForResource(req.resource);
const inactiveReason = inactiveOrganizationReason(organization.id, organization.status);
if (inactiveReason !== undefined) {
return this.decision(req, inactivePrincipalResolution(req.actor, organization.id), {
allowed: false,
reason: inactiveReason,
requiredRole: defaultRequiredRole(req.action),
});
}
const resolution = await this.resolver.resolveActor(req.actor, { organizationId: organization.id });
const threshold = await this.requiredRole(req);
if (threshold === "DISABLED") {
return this.decision(req, resolution, {
@@ -221,27 +230,29 @@ export class PrismaPermissionAuthorizer implements PermissionAuthorizer {
};
}
private async organizationForResource(resource: AuthorizationResource): Promise<string> {
private async organizationForResource(
resource: AuthorizationResource,
): Promise<{ readonly id: string; readonly status: OrganizationStatus }> {
switch (resource.type) {
case "PROJECT": {
const project = await this.prisma.project.findUnique({
where: { id: resource.id },
select: { organizationId: true },
select: { organization: { select: { id: true, status: true } } },
});
if (project === null) {
throw new Error(`authorization resource missing: PROJECT ${resource.id}`);
}
return project.organizationId;
return project.organization;
}
case "PROJECT_GROUP": {
const binding = await this.prisma.projectGroupBinding.findFirst({
where: { chatId: resource.id, archivedAt: null },
select: { project: { select: { organizationId: true } } },
select: { project: { select: { organization: { select: { id: true, status: true } } } } },
});
if (binding === null) {
throw new Error(`authorization resource missing: PROJECT_GROUP ${resource.id}`);
}
return binding.project.organizationId;
return binding.project.organization;
}
case "ARTIFACT":
throw new Error("authorization for ARTIFACT resources requires an organization resolver");
@@ -293,3 +304,16 @@ function principalWhere(principals: readonly PrincipalRef[]): Array<{ principalT
principalId: principal.id,
}));
}
function inactivePrincipalResolution(actor: ActorInput, organizationId: string): PrincipalResolution {
const principals: PrincipalRef[] = [{ type: "USER", id: actor.feishuOpenId }];
if (actor.chatId !== undefined && actor.chatId !== "") {
principals.push({ type: "FEISHU_CHAT", id: actor.chatId });
}
return {
actor,
organizationId,
principals,
resolved: [],
};
}