forked from bai/curriculum-project-hub
195 lines
6.4 KiB
TypeScript
195 lines
6.4 KiB
TypeScript
import type { PrismaClient, PrincipalType } from "@prisma/client";
|
|
import { scopedFeishuPrincipalId } from "../feishu/identityNamespace.js";
|
|
|
|
export interface PrincipalRef {
|
|
readonly type: PrincipalType;
|
|
readonly id: string;
|
|
}
|
|
|
|
export interface ActorInput {
|
|
readonly feishuOpenId: string;
|
|
readonly chatId?: string | undefined;
|
|
}
|
|
|
|
export interface PrincipalResolutionScope {
|
|
readonly organizationId: string;
|
|
}
|
|
|
|
export type PrincipalSource =
|
|
| "actor-user"
|
|
| "context-chat"
|
|
| "team-membership"
|
|
| "external-membership"
|
|
| "team-external-binding";
|
|
|
|
export interface ResolvedPrincipal {
|
|
readonly principal: PrincipalRef;
|
|
readonly source: PrincipalSource;
|
|
readonly via?: PrincipalRef | undefined;
|
|
}
|
|
|
|
export interface PrincipalResolution {
|
|
readonly actor: ActorInput;
|
|
readonly organizationId: string;
|
|
readonly userId?: string | undefined;
|
|
readonly principals: readonly PrincipalRef[];
|
|
readonly resolved: readonly ResolvedPrincipal[];
|
|
}
|
|
|
|
export interface PrincipalResolver {
|
|
resolveActor(actor: ActorInput, scope: PrincipalResolutionScope): Promise<PrincipalResolution>;
|
|
}
|
|
|
|
export class PrismaPrincipalResolver implements PrincipalResolver {
|
|
constructor(
|
|
private readonly prisma: PrismaClient,
|
|
private readonly allowLegacyIdentity = process.env["NODE_ENV"] !== "production",
|
|
) {}
|
|
|
|
async resolveActor(actor: ActorInput, scope: PrincipalResolutionScope): Promise<PrincipalResolution> {
|
|
const resolved = new PrincipalCollector();
|
|
const scopedIdentity = await this.prisma.feishuUserIdentity.findFirst({
|
|
where: {
|
|
openId: actor.feishuOpenId,
|
|
connection: {
|
|
organizationId: scope.organizationId,
|
|
status: "ACTIVE",
|
|
activeSecretVersion: { retiredAt: null },
|
|
},
|
|
},
|
|
select: { userId: true, connectionId: true },
|
|
});
|
|
// Explicit migration adapter for pre-ADR-0024 fixtures/data. Alpha Silo
|
|
// ingress always has an ACTIVE connection-scoped identity.
|
|
const user = scopedIdentity === null && this.allowLegacyIdentity
|
|
? await this.prisma.user.findUnique({
|
|
where: { feishuOpenId: actor.feishuOpenId },
|
|
select: { id: true },
|
|
})
|
|
: scopedIdentity === null ? null : { id: scopedIdentity.userId };
|
|
const connection = await this.prisma.organizationFeishuApplicationConnection.findUnique({
|
|
where: { organizationId: scope.organizationId },
|
|
select: {
|
|
id: true,
|
|
status: true,
|
|
activeSecretVersion: { select: { connectionId: true, retiredAt: true } },
|
|
},
|
|
});
|
|
const activeConnectionId = scopedIdentity?.connectionId ?? (
|
|
connection?.status === "ACTIVE" && connection.activeSecretVersion !== null &&
|
|
connection.activeSecretVersion.connectionId === connection.id &&
|
|
connection.activeSecretVersion.retiredAt === null
|
|
? connection.id
|
|
: undefined
|
|
);
|
|
if (activeConnectionId === undefined && !this.allowLegacyIdentity) {
|
|
throw new Error(`active Feishu Application Connection not found for organization ${scope.organizationId}`);
|
|
}
|
|
const userPrincipalId = activeConnectionId === undefined
|
|
? actor.feishuOpenId
|
|
: scopedFeishuPrincipalId("USER", activeConnectionId, actor.feishuOpenId);
|
|
resolved.add({ type: "USER", id: userPrincipalId }, "actor-user");
|
|
if (actor.chatId !== undefined && actor.chatId !== "") {
|
|
const chatPrincipalId = activeConnectionId === undefined
|
|
? actor.chatId
|
|
: scopedFeishuPrincipalId("CHAT", activeConnectionId, actor.chatId);
|
|
resolved.add({ type: "FEISHU_CHAT", id: chatPrincipalId }, "context-chat");
|
|
}
|
|
|
|
if (user !== null) {
|
|
const memberships = await this.prisma.teamMembership.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
revokedAt: null,
|
|
team: { organizationId: scope.organizationId, archivedAt: null },
|
|
},
|
|
select: { teamId: true },
|
|
});
|
|
for (const membership of memberships) {
|
|
resolved.add({ type: "TEAM", id: membership.teamId }, "team-membership");
|
|
}
|
|
|
|
const externalMemberships = await this.prisma.externalPrincipalMembership.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
revokedAt: null,
|
|
connection: {
|
|
organizationId: scope.organizationId,
|
|
revokedAt: null,
|
|
},
|
|
},
|
|
select: { principalType: true, principalId: true },
|
|
});
|
|
for (const membership of externalMemberships) {
|
|
resolved.add(
|
|
{ type: membership.principalType, id: membership.principalId },
|
|
"external-membership",
|
|
);
|
|
}
|
|
}
|
|
|
|
const externalPrincipals = resolved.principals().filter((principal) => isExternalPrincipal(principal.type));
|
|
if (externalPrincipals.length > 0) {
|
|
const bindings = await this.prisma.teamExternalBinding.findMany({
|
|
where: {
|
|
revokedAt: null,
|
|
team: { organizationId: scope.organizationId, archivedAt: null },
|
|
OR: externalPrincipals.map((principal) => ({
|
|
principalType: principal.type,
|
|
principalId: principal.id,
|
|
})),
|
|
},
|
|
select: { teamId: true, principalType: true, principalId: true },
|
|
});
|
|
for (const binding of bindings) {
|
|
resolved.add(
|
|
{ type: "TEAM", id: binding.teamId },
|
|
"team-external-binding",
|
|
{ type: binding.principalType, id: binding.principalId },
|
|
);
|
|
}
|
|
}
|
|
|
|
return {
|
|
actor,
|
|
organizationId: scope.organizationId,
|
|
...(user !== null ? { userId: user.id } : {}),
|
|
principals: resolved.principals(),
|
|
resolved: resolved.entries(),
|
|
};
|
|
}
|
|
}
|
|
|
|
export function principalKey(principal: PrincipalRef): string {
|
|
return `${principal.type}:${principal.id}`;
|
|
}
|
|
|
|
function isExternalPrincipal(type: PrincipalType): boolean {
|
|
return type === "FEISHU_CHAT" || type === "FEISHU_DEPARTMENT" || type === "FEISHU_USER_GROUP";
|
|
}
|
|
|
|
class PrincipalCollector {
|
|
private readonly refs = new Map<string, PrincipalRef>();
|
|
private readonly provenance: ResolvedPrincipal[] = [];
|
|
|
|
add(principal: PrincipalRef, source: PrincipalSource, via?: PrincipalRef): void {
|
|
const key = principalKey(principal);
|
|
if (!this.refs.has(key)) {
|
|
this.refs.set(key, principal);
|
|
}
|
|
this.provenance.push({
|
|
principal,
|
|
source,
|
|
...(via !== undefined ? { via } : {}),
|
|
});
|
|
}
|
|
|
|
principals(): readonly PrincipalRef[] {
|
|
return [...this.refs.values()];
|
|
}
|
|
|
|
entries(): readonly ResolvedPrincipal[] {
|
|
return this.provenance;
|
|
}
|
|
}
|