feat(policies): UI fuer ABAC-Richtlinien mit Conditions-Builder — Modul 10/16 des UI-Backlogs
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Policies API client — ABAC entity policies (attribute-based access
|
||||
* control) management (UI-Backlog module 10/16).
|
||||
*
|
||||
* Backend: /api/v1/policies
|
||||
* - GET /{entity_type} → list policies for one entity type
|
||||
* - POST / → create policy (201)
|
||||
* - PUT /{policy_id} → update policy
|
||||
* - DELETE /{policy_id} → delete policy (204)
|
||||
* Permissions: policies:read (list) / policies:write (create, update, delete).
|
||||
*
|
||||
* Conditions format (JSONB):
|
||||
* { operator: 'AND' | 'OR', rules: [{ field, op, value }] }
|
||||
* Supported ops: eq, neq, in, not_in, gt, gte, lt, lte, contains,
|
||||
* starts_with, is_null, is_not_null.
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from '@/api/client';
|
||||
|
||||
export type PrincipalType = 'user' | 'group' | 'role';
|
||||
export type PolicyEffect = 'allow' | 'deny';
|
||||
export type ConditionOperator = 'AND' | 'OR';
|
||||
|
||||
export interface PolicyRule {
|
||||
field: string;
|
||||
op: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export interface PolicyConditions {
|
||||
operator: ConditionOperator;
|
||||
rules: PolicyRule[];
|
||||
}
|
||||
|
||||
export interface Policy {
|
||||
id: string;
|
||||
name: string;
|
||||
entity_type: string;
|
||||
principal_type: PrincipalType;
|
||||
principal_id: string;
|
||||
effect: PolicyEffect;
|
||||
conditions: PolicyConditions | null;
|
||||
priority: number;
|
||||
tenant_id: string;
|
||||
enabled: boolean;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface PolicyListResponse {
|
||||
items: Policy[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface PolicyCreatePayload {
|
||||
name: string;
|
||||
entity_type: string;
|
||||
principal_type: PrincipalType;
|
||||
principal_id: string;
|
||||
effect?: PolicyEffect;
|
||||
conditions?: PolicyConditions | null;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
export interface PolicyUpdatePayload {
|
||||
name?: string;
|
||||
entity_type?: string;
|
||||
principal_type?: PrincipalType;
|
||||
principal_id?: string;
|
||||
effect?: PolicyEffect;
|
||||
conditions?: PolicyConditions | null;
|
||||
priority?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/** Entity types with ABAC field whitelists (backend ABAC_ALLOWED_FIELDS). */
|
||||
export const POLICY_ENTITY_TYPES = [
|
||||
'contact',
|
||||
'file',
|
||||
'task',
|
||||
'calendar_event',
|
||||
'mailbox',
|
||||
'address',
|
||||
'contact_folder',
|
||||
'workflow',
|
||||
] as const;
|
||||
|
||||
export const POLICY_OPS = [
|
||||
'eq', 'neq', 'in', 'not_in', 'gt', 'gte', 'lt', 'lte',
|
||||
'contains', 'starts_with', 'is_null', 'is_not_null',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Mirrored from backend policy_service.ABAC_ALLOWED_FIELDS — the fields
|
||||
* each entity type may reference in policy conditions. The backend skips
|
||||
* non-whitelisted fields; showing them here prevents invalid input.
|
||||
* Keep in sync with app/services/policy_service.py.
|
||||
*/
|
||||
export const ABAC_ALLOWED_FIELDS: Record<string, string[]> = {
|
||||
contact: ['status', 'type', 'country', 'tags', 'created_at', 'updated_at', 'owner_id', 'name', 'displayname', 'firstname', 'surname', 'email_1', 'email_2', 'code'],
|
||||
file: ['status', 'size', 'mime_type', 'created_at'],
|
||||
task: ['status', 'priority', 'due_date', 'created_at'],
|
||||
calendar_event: ['status', 'start_time', 'end_time', 'created_at'],
|
||||
mailbox: ['status', 'created_at'],
|
||||
address: ['country', 'city', 'created_at', 'updated_at'],
|
||||
contact_folder: ['name', 'created_at'],
|
||||
workflow: ['status', 'created_at'],
|
||||
};
|
||||
|
||||
/** Ops where the rule value is ignored (null checks). */
|
||||
export const NULL_OPS = ['is_null', 'is_not_null'] as const;
|
||||
|
||||
// ─── Query hooks ─────────────────────────────────────────────
|
||||
|
||||
export function usePolicies(entityType: string, enabled = true) {
|
||||
return useQuery<PolicyListResponse>({
|
||||
queryKey: ['policies', entityType],
|
||||
queryFn: () => apiGet<PolicyListResponse>(`/policies/${entityType}`),
|
||||
enabled: enabled && !!entityType,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Mutation hooks ──────────────────────────────────────────
|
||||
|
||||
export function useCreatePolicy() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation<Policy, Error, PolicyCreatePayload>({
|
||||
mutationFn: (data) => apiPost<Policy>('/policies', data),
|
||||
onSuccess: (policy) => {
|
||||
qc.invalidateQueries({ queryKey: ['policies', policy.entity_type] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdatePolicy() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation<Policy, Error, { id: string; data: PolicyUpdatePayload }>({
|
||||
mutationFn: ({ id, data }) => apiPut<Policy>(`/policies/${id}`, data),
|
||||
onSuccess: (policy) => {
|
||||
qc.invalidateQueries({ queryKey: ['policies', policy.entity_type] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeletePolicy() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation<void, Error, { id: string; entityType: string }>({
|
||||
mutationFn: ({ id }) => apiDelete(`/policies/${id}`),
|
||||
onSuccess: (_, vars) => {
|
||||
qc.invalidateQueries({ queryKey: ['policies', vars.entityType] });
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user