155 lines
4.8 KiB
TypeScript
155 lines
4.8 KiB
TypeScript
/**
|
|
* 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] });
|
|
},
|
|
});
|
|
}
|