309 lines
8.5 KiB
TypeScript
309 lines
8.5 KiB
TypeScript
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 {
|
|
private readonly entries: ReadonlyMap<string, Pick<AccessItem, "id" | "values">>;
|
|
|
|
constructor(
|
|
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);
|
|
}
|
|
|
|
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)
|
|
);
|
|
};
|
|
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))
|
|
);
|
|
};
|
|
|
|
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(
|
|
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(
|
|
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(
|
|
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;
|
|
}
|
|
|
|
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(
|
|
items: readonly Pick<AccessItem, "id" | "values">[],
|
|
people: readonly Pick<Person, "id" | "active">[],
|
|
today: string
|
|
) {
|
|
return new AccessProjection(items, people, today).project();
|
|
}
|