metis-desk/modules/companies/src/api/domain/access/access-projection.ts

263 lines
8.4 KiB
TypeScript
Raw Normal View History

import type { Person } from "../person.entity";
import type { AccessDefinition } from "./access-definition";
import type { AccessItem } from "./access-item.entity";
type Reason =
| "incomplete"
| "expired"
| "inactive_person"
| "inactive_target"
| "unjustified"
| "uncovered"
| "pending_configuration"
| "pending_removal"
| "inactive_catalog";
type Coverage = { needId: string; covered: boolean; missingPermissionIds: string[] };
class AccessProjection {
2026-10-05 10:02:04 +00:00
private readonly entries: ReadonlyMap<string, Pick<AccessItem,"id"|"values">>;
constructor(
2026-10-05 10:02:04 +00:00
private readonly items: readonly Pick<AccessItem,"id"|"values">[],
private readonly people: readonly Pick<Person,"id"|"active">[],
private readonly today: string
) {
this.entries = new Map(items.map((item) => [item.id.toPrimitive(), item]));
}
data = (id: string) => this.entries.get(id)?.values.data;
isOpen = (id: string) => {
const value = this.data(id);
return value && "endDate" in value && value.endDate === null;
};
configured = (id: string) => {
const value = this.data(id);
return (
this.isOpen(id) && value && "configuration" in value && value.configuration === "configured"
);
};
isActive = (id: string) => {
const value = this.data(id);
return (
value &&
(!("active" in value) || value.active) &&
(value.kind !== "account" || value.expiresOn === null || value.expiresOn >= this.today)
);
};
personActive = (id: string) =>
this.people.some((person) => person.id.toPrimitive() === id && person.active);
private accountCovered(relation: Extract<AccessDefinition, { kind: "account_person" }>) {
return this.personActive(relation.personId) && this.isActive(relation.accountId);
}
private membershipAuthorized(id: string | null) {
if (id === null) {
return true;
}
const membership = this.data(id);
return (
membership?.kind === "membership" && this.isOpen(id) && this.isActive(membership.groupId)
);
}
private permissionNeeded(needId: string, permissionId: string) {
const need = this.data(needId);
return need?.kind === "need" && need.permissionIds.includes(permissionId);
}
2026-10-05 10:02:04 +00:00
authorizedLink = (item: Pick<AccessItem,"id"|"values">) => {
const value = item.values.data;
if (value.kind !== "coverage" || value.endDate !== null || !this.isOpen(value.needId)) {
return false;
}
const relation = this.data(value.accountPersonId);
const grant = this.data(value.grantId);
if (relation?.kind !== "account_person" || grant?.kind !== "grant") {
return false;
}
if (!(this.isOpen(value.accountPersonId) && this.isOpen(value.grantId))) {
return false;
}
return (
this.accountCovered(relation) &&
this.isActive(grant.resourceId) &&
this.isActive(grant.permissionId) &&
this.permissionNeeded(value.needId, grant.permissionId) &&
this.membershipAuthorized(value.membershipId)
);
};
2026-10-05 10:02:04 +00:00
validLink = (item: Pick<AccessItem,"id"|"values">) => {
const value = item.values.data;
if (value.kind !== "coverage" || !this.authorizedLink(item)) {
return false;
}
return (
this.configured(value.accountPersonId) &&
this.configured(value.grantId) &&
(value.membershipId === null || this.configured(value.membershipId))
);
};
2026-10-05 10:02:04 +00:00
needCoverage(item: Pick<AccessItem,"id"|"values">): Coverage[] {
const value = item.values.data;
if (value.kind !== "need") {
return [];
}
const links = this.items.filter(
(link) =>
link.values.data.kind === "coverage" &&
link.values.data.needId === item.id.toPrimitive() &&
this.validLink(link)
);
const coveredPermissions = links.flatMap((link) => {
if (link.values.data.kind !== "coverage") {
return [];
}
const grant = this.data(link.values.data.grantId);
return grant?.kind === "grant" ? [grant.permissionId] : [];
});
const missingPermissionIds = value.permissionIds.filter(
(id) => !coveredPermissions.includes(id)
);
return [
{
needId: item.id.toPrimitive(),
covered: value.endDate === null && missingPermissionIds.length === 0,
missingPermissionIds,
},
];
}
private accountReview(
2026-10-05 10:02:04 +00:00
item: Pick<AccessItem,"id"|"values">,
value: Extract<AccessDefinition, { kind: "account" }>
): Reason[] {
const reasons: Reason[] = [];
if (value.active && value.expiresOn !== null && value.expiresOn < this.today) {
reasons.push("expired");
}
if (
value.usage !== "technical" &&
!this.items.some(
(relation) =>
relation.values.data.kind === "account_person" &&
relation.values.data.accountId === item.id.toPrimitive() &&
relation.values.data.endDate === null
)
) {
reasons.push("incomplete");
}
return reasons;
}
private personRelationReview(
value: Extract<AccessDefinition, { kind: "account_person" }>
): Reason[] {
if (value.endDate !== null) {
return [];
}
const reasons: Reason[] = [];
if (!this.personActive(value.personId)) {
reasons.push("inactive_person");
}
if (!this.isActive(value.accountId)) {
reasons.push("inactive_target");
}
return reasons;
}
private grantReview(
2026-10-05 10:02:04 +00:00
item: Pick<AccessItem,"id"|"values">,
value: Extract<AccessDefinition, { kind: "grant" }>
): Reason[] {
if (value.endDate !== null) {
return [];
}
const reasons: Reason[] = [];
if (
!(
this.isActive(value.resourceId) &&
this.isActive(value.permissionId) &&
this.isActive(value.accountId ?? value.groupId ?? "")
)
) {
reasons.push("inactive_target");
}
if (
!this.items.some(
(link) =>
link.values.data.kind === "coverage" &&
link.values.data.grantId === item.id.toPrimitive() &&
this.authorizedLink(link)
)
) {
reasons.push("unjustified");
}
return reasons;
}
private needReview(
2026-10-05 10:02:04 +00:00
item: Pick<AccessItem,"id"|"values">,
value: Extract<AccessDefinition, { kind: "need" }>,
coverage: readonly Coverage[]
): Reason[] {
if (value.endDate !== null) {
return [];
}
const reasons: Reason[] = [];
if (!this.personActive(value.personId)) {
reasons.push("inactive_person");
}
if (!coverage.find((result) => result.needId === item.id.toPrimitive())?.covered) {
reasons.push("uncovered");
}
return reasons;
}
2026-10-05 10:02:04 +00:00
private specificReview(item: Pick<AccessItem,"id"|"values">, coverage: readonly Coverage[]): Reason[] {
const value = item.values.data;
switch (value.kind) {
case "account":
return this.accountReview(item, value);
case "account_person":
return this.personRelationReview(value);
case "grant":
return this.grantReview(item, value);
case "need":
return this.needReview(item, value, coverage);
case "membership":
return value.endDate === null &&
!(this.isActive(value.accountId) && this.isActive(value.groupId))
? ["inactive_target"]
: [];
default:
return [];
}
}
private configurationReview(value: AccessDefinition): Reason[] {
if (!("configuration" in value)) {
return [];
}
if (value.configuration === "pending" && value.endDate === null) {
return ["pending_configuration"];
}
return value.configuration === "removal_pending" ? ["pending_removal"] : [];
}
private catalogReview(value: AccessDefinition): Reason[] {
const inactive = Object.entries(value).some(
([key, id]) =>
["systemId", "environmentId", "resourceTypeId"].includes(key) &&
typeof id === "string" &&
!this.isActive(id)
);
return inactive ? ["inactive_catalog"] : [];
}
project() {
const coverage = this.items.flatMap((item) => this.needCoverage(item));
const reviews = this.items.flatMap((item) =>
[
...this.specificReview(item, coverage),
...this.configurationReview(item.values.data),
...this.catalogReview(item.values.data),
].map((reason) => ({ itemId: item.id.toPrimitive(), reason }))
);
return { coverage, reviews };
}
}
export function projectAccess(
2026-10-05 10:02:04 +00:00
items: readonly Pick<AccessItem,"id"|"values">[],
people: readonly Pick<Person,"id"|"active">[],
today: string
) {
return new AccessProjection(items, people, today).project();
}