Files
govoplan-idm/webui/src/api/idm.ts
T
zemion 21e8f0bc39
Module Package Release / publish-packages (push) Successful in 11s
feat(idm): govern delegation chains and timed escalation
2026-08-22 03:12:30 +02:00

476 lines
15 KiB
TypeScript

import { apiFetch, apiPatchJson, apiPostJson, type ApiSettings } from "@govoplan/core-webui";
export type OrganizationUnitItem = {
id: string;
tenant_id: string;
unit_type_id?: string | null;
parent_id?: string | null;
slug: string;
name: string;
description?: string | null;
is_active: boolean;
settings: Record<string, unknown>;
created_at: string;
updated_at: string;
};
export type OrganizationFunctionItem = {
id: string;
tenant_id: string;
function_type_id?: string | null;
organization_unit_id: string;
slug: string;
name: string;
description?: string | null;
delegable: boolean;
act_in_place_allowed: boolean;
is_active: boolean;
settings: Record<string, unknown>;
created_at: string;
updated_at: string;
};
export type OrganizationModel = {
units: OrganizationUnitItem[];
functions: OrganizationFunctionItem[];
};
export type IdentityOption = {
id: string;
display_name?: string | null;
external_subject?: string | null;
source: string;
primary_account_id?: string | null;
account_ids: string[];
status: string;
};
export type IdentityListResponse = {
identities: IdentityOption[];
};
export type TypedGroupItem = {
id: string;
tenant_id: string;
key: string;
name: string;
group_type: string;
description?: string | null;
status: "active" | "inactive";
source_provider: string;
source_resource_type?: string | null;
source_resource_id?: string | null;
source_revision?: string | null;
properties: Record<string, unknown>;
provenance: Record<string, unknown>;
revision: number;
created_at?: string | null;
updated_at?: string | null;
};
export type TypedGroupList = {
groups: TypedGroupItem[];
total: number;
};
export type TypedGroupPayload = {
key: string;
name: string;
group_type: string;
description?: string | null;
source_provider?: string;
source_resource_type?: string | null;
source_resource_id?: string | null;
source_revision?: string | null;
properties?: Record<string, unknown>;
provenance?: Record<string, unknown>;
};
export type TypedGroupUpdatePayload = Partial<TypedGroupPayload> & {
base_revision: number;
status?: "active" | "inactive";
};
export type IdentityRelationshipItem = {
id: string;
tenant_id: string;
relationship_kind: string;
subject_identity_id: string;
target_group_id?: string | null;
related_identity_id?: string | null;
role?: string | null;
valid_from?: string | null;
valid_until?: string | null;
status: "active" | "revoked";
revoked_at?: string | null;
revoked_by?: string | null;
revocation_reason?: string | null;
expired_event_at?: string | null;
source_provider: string;
source_resource_type?: string | null;
source_resource_id?: string | null;
source_revision?: string | null;
properties: Record<string, unknown>;
provenance: Record<string, unknown>;
revision: number;
created_at?: string | null;
updated_at?: string | null;
};
export type IdentityRelationshipList = {
relationships: IdentityRelationshipItem[];
total: number;
};
export type IdentityRelationshipPayload = {
relationship_kind: string;
subject_identity_id: string;
target_group_id?: string | null;
related_identity_id?: string | null;
role?: string | null;
valid_from?: string | null;
valid_until?: string | null;
source_provider?: string;
source_resource_type?: string | null;
source_resource_id?: string | null;
source_revision?: string | null;
properties?: Record<string, unknown>;
provenance?: Record<string, unknown>;
};
export type IdentityRelationshipUpdatePayload = Omit<
Partial<IdentityRelationshipPayload>,
"subject_identity_id"
> & {
base_revision: number;
};
export type IdentityRelationshipDecisionItem = {
relationship: IdentityRelationshipItem;
included: boolean;
code: string;
explanation: string;
identity_status?: string | null;
};
export type TypedGroupMembershipResolution = {
group: TypedGroupItem;
effective_at: string;
decisions: IdentityRelationshipDecisionItem[];
identity_ids: string[];
};
export type OrganizationFunctionAssignmentItem = {
id: string;
tenant_id: string;
identity_id: string;
account_id?: string | null;
function_id: string;
organization_unit_id: string;
applies_to_subunits: boolean;
source: string;
delegated_from_assignment_id?: string | null;
acting_for_account_id?: string | null;
valid_from?: string | null;
valid_until?: string | null;
is_active: boolean;
settings: Record<string, unknown>;
created_at: string;
updated_at: string;
};
export type OrganizationFunctionAssignmentList = {
assignments: OrganizationFunctionAssignmentItem[];
total?: number;
page?: number;
page_size?: number;
pages?: number;
};
export type IdmSettings = {
tenant_id: string;
require_assignment_change_requests: boolean;
audit_detail_level: "summary" | "standard" | "full";
change_retention_days?: number | null;
settings: Record<string, unknown>;
created_at?: string | null;
updated_at?: string | null;
};
export type OrganizationFunctionAssignmentPayload = {
identity_id: string;
function_id: string;
account_id?: string | null;
applies_to_subunits?: boolean;
source?: string;
delegated_from_assignment_id?: string | null;
acting_for_account_id?: string | null;
valid_from?: string | null;
valid_until?: string | null;
is_active?: boolean;
settings?: Record<string, unknown>;
change_request_id?: string | null;
governance_override_reason?: string | null;
governance_override_evidence?: string[];
};
export type IdmSettingsPayload = Partial<Pick<IdmSettings, "require_assignment_change_requests" | "audit_detail_level" | "change_retention_days" | "settings">>;
export type FunctionAssignmentChangeKind = "request" | "grant";
export type FunctionAssignmentChangeAction = "approve" | "reject" | "accept" | "request_changes" | "respond" | "withdraw" | "recover";
export type FunctionAssignmentChangeEvent = {
id: string;
sequence: number;
action: string;
from_state?: string | null;
to_state: string;
actor_account_id?: string | null;
actor_identity_id?: string | null;
comment?: string | null;
evidence: string[];
policy_decision: Record<string, unknown>;
workflow_step_id?: string | null;
details: Record<string, unknown>;
created_at: string;
};
export type FunctionAssignmentChange = {
id: string;
tenant_id: string;
kind: FunctionAssignmentChangeKind;
state: string;
profile: string;
function_id: string;
organization_unit_id: string;
candidate_identity_id: string;
candidate_account_id?: string | null;
initiator_account_id: string;
initiator_identity_id?: string | null;
justification: string;
evidence: string[];
requested_valid_from?: string | null;
requested_valid_until?: string | null;
required_steps: string[];
completed_steps: string[];
policy_decision: Record<string, unknown>;
workflow_definition_revision?: number | null;
workflow_definition_hash?: string | null;
workflow_instance_id?: string | null;
resulting_assignment_id?: string | null;
expires_at?: string | null;
review_deadline_at?: string | null;
escalated_at?: string | null;
escalation_from_state?: string | null;
escalation_target_function_id?: string | null;
outcome_reason?: string | null;
resource_revision: number;
etag: string;
metadata: Record<string, unknown>;
events: FunctionAssignmentChangeEvent[];
available_actions: FunctionAssignmentChangeAction[];
availability_reason?: string | null;
created_at: string;
updated_at: string;
};
export type FunctionAssignmentChangeList = {
changes: FunctionAssignmentChange[];
total: number;
page: number;
page_size: number;
pages: number;
};
export type FunctionAssignmentChangePayload = {
kind: FunctionAssignmentChangeKind;
function_id: string;
candidate_identity_id: string;
candidate_account_id?: string | null;
justification: string;
evidence?: string[];
requested_valid_from?: string | null;
requested_valid_until?: string | null;
applies_to_subunits?: boolean;
assignment_source?: "governance" | "delegated";
represented_assignment_id?: string | null;
idempotency_key: string;
metadata?: Record<string, unknown>;
};
export function getOrganizationModel(settings: ApiSettings): Promise<OrganizationModel> {
return apiFetch<OrganizationModel>(settings, "/api/v1/organizations/model");
}
export async function getOrganizationFunctionAssignments(settings: ApiSettings): Promise<OrganizationFunctionAssignmentList> {
const pageSize = 500;
const assignments: OrganizationFunctionAssignmentItem[] = [];
let total = 0;
for (let page = 1; ; page += 1) {
const response = await apiFetch<OrganizationFunctionAssignmentList>(
settings,
`/api/v1/idm/organization-function-assignments?page=${page}&page_size=${pageSize}`
);
assignments.push(...response.assignments);
total = response.total ?? assignments.length;
if (page >= (response.pages ?? 1)) {
return {
assignments,
total,
page: 1,
page_size: assignments.length,
pages: 1
};
}
}
}
export function getIdmSettings(settings: ApiSettings): Promise<IdmSettings> {
return apiFetch<IdmSettings>(settings, "/api/v1/idm/settings");
}
export function patchIdmSettings(settings: ApiSettings, payload: IdmSettingsPayload): Promise<IdmSettings> {
return apiPatchJson(settings, "/api/v1/idm/settings", payload);
}
export function searchOrganizationIdentityOptions(
settings: ApiSettings,
query = "",
limit = 50,
signal?: AbortSignal
): Promise<IdentityListResponse> {
const params = new URLSearchParams();
const trimmed = query.trim();
if (trimmed) params.set("query", trimmed);
params.set("limit", String(limit));
return apiFetch<IdentityListResponse>(settings, `/api/v1/idm/organization-identities?${params.toString()}`, { signal });
}
export function getTypedGroups(
settings: ApiSettings,
options: { query?: string; includeInactive?: boolean; limit?: number; signal?: AbortSignal } = {}
): Promise<TypedGroupList> {
const params = new URLSearchParams();
if (options.query?.trim()) params.set("query", options.query.trim());
if (options.includeInactive) params.set("include_inactive", "true");
params.set("limit", String(options.limit ?? 1000));
return apiFetch<TypedGroupList>(settings, `/api/v1/idm/typed-groups?${params.toString()}`, { signal: options.signal });
}
export function createTypedGroup(settings: ApiSettings, payload: TypedGroupPayload): Promise<TypedGroupItem> {
return apiPostJson(settings, "/api/v1/idm/typed-groups", payload);
}
export function patchTypedGroup(
settings: ApiSettings,
groupId: string,
payload: TypedGroupUpdatePayload
): Promise<TypedGroupItem> {
return apiPatchJson(settings, `/api/v1/idm/typed-groups/${encodeURIComponent(groupId)}`, payload);
}
export function getIdentityRelationships(
settings: ApiSettings,
options: { includeRevoked?: boolean; identityId?: string; groupId?: string; relationshipKind?: string; limit?: number } = {}
): Promise<IdentityRelationshipList> {
const params = new URLSearchParams();
if (options.includeRevoked) params.set("include_revoked", "true");
if (options.identityId) params.set("identity_id", options.identityId);
if (options.groupId) params.set("group_id", options.groupId);
if (options.relationshipKind) params.set("relationship_kind", options.relationshipKind);
params.set("limit", String(options.limit ?? 1000));
return apiFetch<IdentityRelationshipList>(settings, `/api/v1/idm/relationships?${params.toString()}`);
}
export function createIdentityRelationship(
settings: ApiSettings,
payload: IdentityRelationshipPayload
): Promise<IdentityRelationshipItem> {
return apiPostJson(settings, "/api/v1/idm/relationships", payload);
}
export function patchIdentityRelationship(
settings: ApiSettings,
relationshipId: string,
payload: IdentityRelationshipUpdatePayload
): Promise<IdentityRelationshipItem> {
return apiPatchJson(settings, `/api/v1/idm/relationships/${encodeURIComponent(relationshipId)}`, payload);
}
export function revokeIdentityRelationship(
settings: ApiSettings,
relationship: Pick<IdentityRelationshipItem, "id" | "revision">,
reason: string
): Promise<IdentityRelationshipItem> {
return apiPostJson(settings, `/api/v1/idm/relationships/${encodeURIComponent(relationship.id)}/revoke`, {
base_revision: relationship.revision,
reason
});
}
export function resolveTypedGroupMemberships(
settings: ApiSettings,
groupId: string,
options: { effectiveAt?: string; relationshipKinds?: string[] } = {}
): Promise<TypedGroupMembershipResolution> {
const params = new URLSearchParams();
if (options.effectiveAt) params.set("effective_at", options.effectiveAt);
for (const kind of options.relationshipKinds ?? ["member"]) params.append("relationship_kind", kind);
return apiFetch<TypedGroupMembershipResolution>(
settings,
`/api/v1/idm/typed-groups/${encodeURIComponent(groupId)}/memberships?${params.toString()}`
);
}
export function createOrganizationFunctionAssignment(
settings: ApiSettings,
payload: OrganizationFunctionAssignmentPayload
): Promise<OrganizationFunctionAssignmentItem> {
return apiPostJson(settings, "/api/v1/idm/organization-function-assignments", payload);
}
export function patchOrganizationFunctionAssignment(
settings: ApiSettings,
id: string,
payload: Partial<OrganizationFunctionAssignmentPayload>
): Promise<OrganizationFunctionAssignmentItem> {
return apiPatchJson(settings, `/api/v1/idm/organization-function-assignments/${encodeURIComponent(id)}`, payload);
}
export function getFunctionAssignmentChanges(settings: ApiSettings): Promise<FunctionAssignmentChangeList> {
return apiFetch<FunctionAssignmentChangeList>(settings, "/api/v1/idm/function-assignment-changes?page_size=200");
}
export function getFunctionAssignmentChange(settings: ApiSettings, id: string): Promise<FunctionAssignmentChange> {
return apiFetch<FunctionAssignmentChange>(settings, `/api/v1/idm/function-assignment-changes/${encodeURIComponent(id)}`);
}
export function createFunctionAssignmentChange(
settings: ApiSettings,
payload: FunctionAssignmentChangePayload
): Promise<FunctionAssignmentChange> {
return apiPostJson(settings, "/api/v1/idm/function-assignment-changes", payload);
}
export function actOnFunctionAssignmentChange(
settings: ApiSettings,
change: FunctionAssignmentChange,
action: FunctionAssignmentChangeAction,
comment?: string
): Promise<FunctionAssignmentChange> {
return apiFetch<FunctionAssignmentChange>(
settings,
`/api/v1/idm/function-assignment-changes/${encodeURIComponent(change.id)}/actions`,
{
method: "POST",
headers: { "If-Match": change.etag },
body: JSON.stringify({
action,
base_revision: change.resource_revision,
comment: comment?.trim() || null,
evidence: []
})
}
);
}