-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathpermission.zod.ts
More file actions
207 lines (188 loc) · 8.68 KB
/
Copy pathpermission.zod.ts
File metadata and controls
207 lines (188 loc) · 8.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod';
import { RowLevelSecurityPolicySchema } from './rls.zod';
/**
* Entity (Object) Level Permissions
* Defines CRUD + VAMA (View All / Modify All) + Lifecycle access.
*
* Refined with enterprise data lifecycle controls:
* - Transfer (Ownership change)
* - Restore (Soft delete recovery)
* - Purge (Hard delete / Compliance)
*/
import { lazySchema } from '../shared/lazy-schema';
/**
* [ADR-0057 D1] Object access DEPTH — the Dataverse "access level" axis,
* layered on top of OWD. Widens the owner-match for owner-scoped objects.
*/
export const ObjectAccessScopeSchema = z.enum(['own', 'own_and_reports', 'unit', 'unit_and_below', 'org']);
export type ObjectAccessScope = z.infer<typeof ObjectAccessScopeSchema>;
export const ObjectPermissionSchema = lazySchema(() => z.object({
/** C: Create */
allowCreate: z.boolean().default(false).describe('Create permission'),
/** R: Read (Owned records or Shared records) */
allowRead: z.boolean().default(false).describe('Read permission'),
/** U: Edit (Owned records or Shared records) */
allowEdit: z.boolean().default(false).describe('Edit permission'),
/** D: Delete (Owned records or Shared records) */
allowDelete: z.boolean().default(false).describe('Delete permission'),
/**
* Lifecycle Operations.
*
* EXPERIMENTAL — not enforced (ADR-0049). The `transfer`/`restore`/`purge`
* operations these bits gate do not yet exist as ObjectQL operations, and no
* runtime consumer reads these bits. Authoring them is currently a no-op.
* The runtime fails CLOSED if such an operation is ever introduced without a
* matching permission mapping (see `permission-evaluator.ts`
* DESTRUCTIVE_OPERATIONS). Tracked by #1883.
*/
allowTransfer: z.boolean().default(false).describe('[EXPERIMENTAL — not enforced] Change record ownership'),
allowRestore: z.boolean().default(false).describe('[EXPERIMENTAL — not enforced] Restore from trash (Undelete)'),
allowPurge: z.boolean().default(false).describe('[EXPERIMENTAL — not enforced] Permanently delete (Hard Delete/GDPR)'),
/**
* View All Records: Super-user read access.
* Bypasses Sharing Rules and Ownership checks.
* Equivalent to Microsoft Dataverse "Organization" level read access.
*/
viewAllRecords: z.boolean().default(false).describe('View All Data (Bypass Sharing)'),
/**
* Modify All Records: Super-user write access.
* Bypasses Sharing Rules and Ownership checks.
* Equivalent to Microsoft Dataverse "Organization" level write access.
*/
modifyAllRecords: z.boolean().default(false).describe('Modify All Data (Bypass Sharing)'),
/**
* [ADR-0057 D1] Read access DEPTH (Dataverse-style access level), layered on
* top of OWD. For owner-scoped (`private`) objects it widens the owner-match:
* `own` (owner only) | `own_and_reports` (me + my sys_user.manager_id
* report chain) | `unit` (my business unit) | `unit_and_below` (my BU +
* descendants) | `org` (whole tenant). Unset = `own` baseline. Resolved into
* an `owner_id IN (…)` set at request time; sharing rules still widen on top.
*/
readScope: ObjectAccessScopeSchema.optional().describe('[ADR-0057 D1] Read depth: own|unit|unit_and_below|org'),
/** [ADR-0057 D1] Write (edit/delete) access DEPTH — same enum as readScope. */
writeScope: ObjectAccessScopeSchema.optional().describe('[ADR-0057 D1] Write depth: own|unit|unit_and_below|org'),
}));
/**
* Field Level Security (FLS)
*/
export const FieldPermissionSchema = lazySchema(() => z.object({
/** Can see this field */
readable: z.boolean().default(true).describe('Field read access'),
/** Can edit this field */
editable: z.boolean().default(false).describe('Field edit access'),
}));
/**
* Permission Set Schema
* Defines a collection of permissions that can be assigned to users.
*
* DIFFERENTIATION:
* - Profile: The ONE primary functional definition of a user (e.g. Standard User).
* - Permission Set: Add-on capabilities assigned to users (e.g. Export Reports).
* - Role: (Defined in src/system/role.zod.ts) Defines data visibility hierarchy.
*
* **NAMING CONVENTION:**
* Permission set names MUST be lowercase snake_case to prevent security issues.
*
* @example Good permission set names
* - 'read_only'
* - 'system_admin'
* - 'standard_user'
* - 'api_access'
*
* @example Bad permission set names (will be rejected)
* - 'ReadOnly' (camelCase)
* - 'SystemAdmin' (mixed case)
* - 'Read Only' (spaces)
*/
export const PermissionSetSchema = lazySchema(() => z.object({
/** Unique permission set name */
name: SnakeCaseIdentifierSchema.describe('Permission set unique name (lowercase snake_case)'),
/** Display label */
label: z.string().optional().describe('Display label'),
/** Is this a Profile? (Base set for a user) */
isProfile: z.boolean().default(false).describe('Whether this is a user profile'),
isDefault: z.boolean().default(false).describe('[ADR-0056 D7] When true, this profile is the FALLBACK assigned to authenticated users who have no explicit grants — an app declares its default access posture here instead of relying on the built-in member_default. Foundation for SSO/JIT provisioning.'),
/** Object Permissions Map: <entity_name> -> permissions */
objects: z.record(z.string(), ObjectPermissionSchema).describe('Entity permissions'),
/** Field Permissions Map: <entity_name>.<field_name> -> permissions */
fields: z.record(z.string(), FieldPermissionSchema).optional().describe('Field level security'),
/** System permissions (e.g., "manage_users") */
systemPermissions: z.array(z.string()).optional().describe('System level capabilities'),
/**
* Tab/App Visibility Permissions (Salesforce Pattern)
* Controls which app tabs are visible, hidden, or set as default for this permission set.
*
* @example
* ```typescript
* tabPermissions: {
* 'app_crm': 'visible',
* 'app_admin': 'hidden',
* 'app_sales': 'default_on'
* }
* ```
*/
tabPermissions: z.record(z.string(), z.enum(['visible', 'hidden', 'default_on', 'default_off'])).optional()
.describe('App/tab visibility: visible, hidden, default_on (shown by default), default_off (available but hidden initially)'),
/**
* Row-Level Security Rules
*
* Row-level security policies that filter records based on user context.
* These rules are applied in addition to object-level permissions.
*
* Uses the canonical RLS protocol from rls.zod.ts for comprehensive
* row-level security features including PostgreSQL-style USING and CHECK clauses.
*
* @see {@link RowLevelSecurityPolicySchema} for full RLS specification
* @see {@link file://./rls.zod.ts} for comprehensive RLS documentation
*
* @example Multi-tenant isolation
* ```typescript
* rls: [{
* name: 'tenant_filter',
* object: 'account',
* operation: 'select',
* using: 'organization_id == current_user.organization_id'
* }]
* ```
*/
rowLevelSecurity: z.array(RowLevelSecurityPolicySchema).optional()
.describe('Row-level security policies (see rls.zod.ts for full spec)'),
/**
* Context-Based Access Control Variables
*
* Custom context variables that can be referenced in RLS rules.
* These variables are evaluated at runtime based on the user's session.
*
* Common context variables:
* - `current_user.id` - Current user ID
* - `current_user.organization_id` - Active organization id
* - `current_user.department` - User's department
* - `current_user.role` - User's role
* - `current_user.region` - User's geographic region
*
* @example Custom context
* ```typescript
* contextVariables: {
* allowed_regions: ['US', 'EU'],
* access_level: 2,
* custom_attribute: 'value'
* }
* ```
*/
contextVariables: z.record(z.string(), z.unknown()).optional().describe('Context variables for RLS evaluation'),
}));
export type PermissionSet = z.infer<typeof PermissionSetSchema>;
/** Authoring input for {@link PermissionSet} — defaulted fields are optional. */
export type PermissionSetInput = z.input<typeof PermissionSetSchema>;
export type ObjectPermission = z.infer<typeof ObjectPermissionSchema>;
export type FieldPermission = z.infer<typeof FieldPermissionSchema>;
/**
* Type-safe factory for a permission set / profile. Validates at authoring time via
* `.parse()` and accepts input-shape config (optional defaults, CEL
* shorthand) — preferred over a bare `: PermissionSet` literal.
*/
export function definePermissionSet(config: z.input<typeof PermissionSetSchema>): PermissionSet {
return PermissionSetSchema.parse(config);
}