/** * 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 = { 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({ queryKey: ['policies', entityType], queryFn: () => apiGet(`/policies/${entityType}`), enabled: enabled && !!entityType, }); } // ─── Mutation hooks ────────────────────────────────────────── export function useCreatePolicy() { const qc = useQueryClient(); return useMutation({ mutationFn: (data) => apiPost('/policies', data), onSuccess: (policy) => { qc.invalidateQueries({ queryKey: ['policies', policy.entity_type] }); }, }); } export function useUpdatePolicy() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ id, data }) => apiPut(`/policies/${id}`, data), onSuccess: (policy) => { qc.invalidateQueries({ queryKey: ['policies', policy.entity_type] }); }, }); } export function useDeletePolicy() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ id }) => apiDelete(`/policies/${id}`), onSuccess: (_, vars) => { qc.invalidateQueries({ queryKey: ['policies', vars.entityType] }); }, }); }