@openmdm/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,418 @@
1
+ /**
2
+ * OpenMDM Authorization Manager
3
+ *
4
+ * Provides Role-Based Access Control (RBAC) for the MDM system.
5
+ * Enables fine-grained permission management for users and resources.
6
+ */
7
+
8
+ import type {
9
+ Role,
10
+ User,
11
+ UserWithRoles,
12
+ Permission,
13
+ PermissionAction,
14
+ PermissionResource,
15
+ AuthorizationManager,
16
+ CreateRoleInput,
17
+ UpdateRoleInput,
18
+ CreateUserInput,
19
+ UpdateUserInput,
20
+ UserFilter,
21
+ UserListResult,
22
+ DatabaseAdapter,
23
+ } from './types';
24
+ import {
25
+ UserNotFoundError,
26
+ RoleNotFoundError,
27
+ AuthorizationError,
28
+ ValidationError,
29
+ } from './types';
30
+
31
+ /**
32
+ * Check if an action matches the required action
33
+ */
34
+ function actionMatches(required: PermissionAction, granted: PermissionAction): boolean {
35
+ if (granted === '*') return true;
36
+ if (granted === 'manage') {
37
+ // 'manage' implies all CRUD operations
38
+ return ['create', 'read', 'update', 'delete', 'manage'].includes(required);
39
+ }
40
+ return required === granted;
41
+ }
42
+
43
+ /**
44
+ * Check if a resource matches the required resource
45
+ */
46
+ function resourceMatches(required: PermissionResource, granted: PermissionResource): boolean {
47
+ if (granted === '*') return true;
48
+ return required === granted;
49
+ }
50
+
51
+ /**
52
+ * Check if a permission matches the required permission
53
+ */
54
+ function permissionMatches(
55
+ required: { action: PermissionAction; resource: PermissionResource },
56
+ granted: Permission
57
+ ): boolean {
58
+ return (
59
+ actionMatches(required.action, granted.action) &&
60
+ resourceMatches(required.resource, granted.resource)
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Check if any permission in the list grants the required access
66
+ */
67
+ function hasPermission(
68
+ permissions: Permission[],
69
+ action: PermissionAction,
70
+ resource: PermissionResource
71
+ ): boolean {
72
+ return permissions.some((p) => permissionMatches({ action, resource }, p));
73
+ }
74
+
75
+ /**
76
+ * Check if user has admin permissions (full access)
77
+ */
78
+ function isAdminPermission(permissions: Permission[]): boolean {
79
+ return permissions.some(
80
+ (p) => p.action === '*' && p.resource === '*'
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Validate email format
86
+ */
87
+ function validateEmail(email: string): boolean {
88
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
89
+ return emailRegex.test(email);
90
+ }
91
+
92
+ /**
93
+ * Create an AuthorizationManager instance
94
+ */
95
+ export function createAuthorizationManager(db: DatabaseAdapter): AuthorizationManager {
96
+ /**
97
+ * Get all permissions for a user from all their roles
98
+ */
99
+ async function getAllUserPermissions(userId: string): Promise<Permission[]> {
100
+ if (!db.getUserRoles) {
101
+ throw new Error('Database adapter does not support RBAC operations');
102
+ }
103
+
104
+ const roles = await db.getUserRoles(userId);
105
+ const permissions: Permission[] = [];
106
+
107
+ for (const role of roles) {
108
+ permissions.push(...role.permissions);
109
+ }
110
+
111
+ return permissions;
112
+ }
113
+
114
+ return {
115
+ // ========================================
116
+ // Role Management
117
+ // ========================================
118
+
119
+ async createRole(data: CreateRoleInput): Promise<Role> {
120
+ if (!db.createRole) {
121
+ throw new Error('Database adapter does not support RBAC operations');
122
+ }
123
+
124
+ // Validate permissions array
125
+ if (!data.permissions || !Array.isArray(data.permissions)) {
126
+ throw new ValidationError('Permissions must be an array');
127
+ }
128
+
129
+ for (const permission of data.permissions) {
130
+ if (!permission.action || !permission.resource) {
131
+ throw new ValidationError('Each permission must have action and resource');
132
+ }
133
+ }
134
+
135
+ return db.createRole(data);
136
+ },
137
+
138
+ async getRole(id: string): Promise<Role | null> {
139
+ if (!db.findRole) {
140
+ throw new Error('Database adapter does not support RBAC operations');
141
+ }
142
+ return db.findRole(id);
143
+ },
144
+
145
+ async listRoles(tenantId?: string): Promise<Role[]> {
146
+ if (!db.listRoles) {
147
+ throw new Error('Database adapter does not support RBAC operations');
148
+ }
149
+ return db.listRoles(tenantId);
150
+ },
151
+
152
+ async updateRole(id: string, data: UpdateRoleInput): Promise<Role> {
153
+ if (!db.updateRole || !db.findRole) {
154
+ throw new Error('Database adapter does not support RBAC operations');
155
+ }
156
+
157
+ const role = await db.findRole(id);
158
+ if (!role) {
159
+ throw new RoleNotFoundError(id);
160
+ }
161
+
162
+ // Cannot update system roles
163
+ if (role.isSystem) {
164
+ throw new AuthorizationError('Cannot modify system roles');
165
+ }
166
+
167
+ // Validate permissions if provided
168
+ if (data.permissions) {
169
+ if (!Array.isArray(data.permissions)) {
170
+ throw new ValidationError('Permissions must be an array');
171
+ }
172
+
173
+ for (const permission of data.permissions) {
174
+ if (!permission.action || !permission.resource) {
175
+ throw new ValidationError('Each permission must have action and resource');
176
+ }
177
+ }
178
+ }
179
+
180
+ return db.updateRole(id, data);
181
+ },
182
+
183
+ async deleteRole(id: string): Promise<void> {
184
+ if (!db.deleteRole || !db.findRole) {
185
+ throw new Error('Database adapter does not support RBAC operations');
186
+ }
187
+
188
+ const role = await db.findRole(id);
189
+ if (!role) {
190
+ throw new RoleNotFoundError(id);
191
+ }
192
+
193
+ // Cannot delete system roles
194
+ if (role.isSystem) {
195
+ throw new AuthorizationError('Cannot delete system roles');
196
+ }
197
+
198
+ await db.deleteRole(id);
199
+ },
200
+
201
+ // ========================================
202
+ // User Management
203
+ // ========================================
204
+
205
+ async createUser(data: CreateUserInput): Promise<User> {
206
+ if (!db.createUser || !db.findUserByEmail) {
207
+ throw new Error('Database adapter does not support RBAC operations');
208
+ }
209
+
210
+ // Validate email
211
+ if (!validateEmail(data.email)) {
212
+ throw new ValidationError('Invalid email format', { email: data.email });
213
+ }
214
+
215
+ // Check for duplicate email within tenant
216
+ const existing = await db.findUserByEmail(data.email, data.tenantId);
217
+ if (existing) {
218
+ throw new ValidationError(`User with email '${data.email}' already exists`, {
219
+ email: data.email,
220
+ });
221
+ }
222
+
223
+ return db.createUser({
224
+ ...data,
225
+ email: data.email.toLowerCase(),
226
+ });
227
+ },
228
+
229
+ async getUser(id: string): Promise<UserWithRoles | null> {
230
+ if (!db.findUser || !db.getUserRoles) {
231
+ throw new Error('Database adapter does not support RBAC operations');
232
+ }
233
+
234
+ const user = await db.findUser(id);
235
+ if (!user) return null;
236
+
237
+ const roles = await db.getUserRoles(id);
238
+ return { ...user, roles };
239
+ },
240
+
241
+ async getUserByEmail(email: string, tenantId?: string): Promise<UserWithRoles | null> {
242
+ if (!db.findUserByEmail || !db.getUserRoles) {
243
+ throw new Error('Database adapter does not support RBAC operations');
244
+ }
245
+
246
+ const user = await db.findUserByEmail(email.toLowerCase(), tenantId);
247
+ if (!user) return null;
248
+
249
+ const roles = await db.getUserRoles(user.id);
250
+ return { ...user, roles };
251
+ },
252
+
253
+ async listUsers(filter?: UserFilter): Promise<UserListResult> {
254
+ if (!db.listUsers) {
255
+ throw new Error('Database adapter does not support RBAC operations');
256
+ }
257
+ return db.listUsers(filter);
258
+ },
259
+
260
+ async updateUser(id: string, data: UpdateUserInput): Promise<User> {
261
+ if (!db.updateUser || !db.findUser) {
262
+ throw new Error('Database adapter does not support RBAC operations');
263
+ }
264
+
265
+ const user = await db.findUser(id);
266
+ if (!user) {
267
+ throw new UserNotFoundError(id);
268
+ }
269
+
270
+ // Validate email if provided
271
+ if (data.email) {
272
+ if (!validateEmail(data.email)) {
273
+ throw new ValidationError('Invalid email format', { email: data.email });
274
+ }
275
+ data.email = data.email.toLowerCase();
276
+ }
277
+
278
+ return db.updateUser(id, data);
279
+ },
280
+
281
+ async deleteUser(id: string): Promise<void> {
282
+ if (!db.deleteUser || !db.findUser) {
283
+ throw new Error('Database adapter does not support RBAC operations');
284
+ }
285
+
286
+ const user = await db.findUser(id);
287
+ if (!user) {
288
+ throw new UserNotFoundError(id);
289
+ }
290
+
291
+ await db.deleteUser(id);
292
+ },
293
+
294
+ // ========================================
295
+ // Role Assignment
296
+ // ========================================
297
+
298
+ async assignRole(userId: string, roleId: string): Promise<void> {
299
+ if (!db.assignRoleToUser || !db.findUser || !db.findRole) {
300
+ throw new Error('Database adapter does not support RBAC operations');
301
+ }
302
+
303
+ const user = await db.findUser(userId);
304
+ if (!user) {
305
+ throw new UserNotFoundError(userId);
306
+ }
307
+
308
+ const role = await db.findRole(roleId);
309
+ if (!role) {
310
+ throw new RoleNotFoundError(roleId);
311
+ }
312
+
313
+ // Verify tenant compatibility
314
+ if (role.tenantId && user.tenantId && role.tenantId !== user.tenantId) {
315
+ throw new AuthorizationError('Role belongs to a different tenant');
316
+ }
317
+
318
+ await db.assignRoleToUser(userId, roleId);
319
+ },
320
+
321
+ async removeRole(userId: string, roleId: string): Promise<void> {
322
+ if (!db.removeRoleFromUser || !db.findUser) {
323
+ throw new Error('Database adapter does not support RBAC operations');
324
+ }
325
+
326
+ const user = await db.findUser(userId);
327
+ if (!user) {
328
+ throw new UserNotFoundError(userId);
329
+ }
330
+
331
+ await db.removeRoleFromUser(userId, roleId);
332
+ },
333
+
334
+ async getUserRoles(userId: string): Promise<Role[]> {
335
+ if (!db.getUserRoles || !db.findUser) {
336
+ throw new Error('Database adapter does not support RBAC operations');
337
+ }
338
+
339
+ const user = await db.findUser(userId);
340
+ if (!user) {
341
+ throw new UserNotFoundError(userId);
342
+ }
343
+
344
+ return db.getUserRoles(userId);
345
+ },
346
+
347
+ // ========================================
348
+ // Permission Checking
349
+ // ========================================
350
+
351
+ async can(
352
+ userId: string,
353
+ action: PermissionAction,
354
+ resource: PermissionResource,
355
+ _resourceId?: string
356
+ ): Promise<boolean> {
357
+ if (!db.findUser) {
358
+ throw new Error('Database adapter does not support RBAC operations');
359
+ }
360
+
361
+ const user = await db.findUser(userId);
362
+ if (!user) return false;
363
+
364
+ // Inactive users have no permissions
365
+ if (user.status !== 'active') return false;
366
+
367
+ const permissions = await getAllUserPermissions(userId);
368
+ return hasPermission(permissions, action, resource);
369
+ },
370
+
371
+ async requirePermission(
372
+ userId: string,
373
+ action: PermissionAction,
374
+ resource: PermissionResource,
375
+ resourceId?: string
376
+ ): Promise<void> {
377
+ const allowed = await this.can(userId, action, resource, resourceId);
378
+ if (!allowed) {
379
+ throw new AuthorizationError(
380
+ `Permission denied: ${action} on ${resource}${resourceId ? ` (${resourceId})` : ''}`
381
+ );
382
+ }
383
+ },
384
+
385
+ async canAny(
386
+ userId: string,
387
+ permissions: Array<{ action: PermissionAction; resource: PermissionResource }>
388
+ ): Promise<boolean> {
389
+ if (!db.findUser) {
390
+ throw new Error('Database adapter does not support RBAC operations');
391
+ }
392
+
393
+ const user = await db.findUser(userId);
394
+ if (!user) return false;
395
+
396
+ // Inactive users have no permissions
397
+ if (user.status !== 'active') return false;
398
+
399
+ const userPermissions = await getAllUserPermissions(userId);
400
+
401
+ return permissions.some((required) =>
402
+ hasPermission(userPermissions, required.action, required.resource)
403
+ );
404
+ },
405
+
406
+ async isAdmin(userId: string): Promise<boolean> {
407
+ if (!db.findUser) {
408
+ throw new Error('Database adapter does not support RBAC operations');
409
+ }
410
+
411
+ const user = await db.findUser(userId);
412
+ if (!user || user.status !== 'active') return false;
413
+
414
+ const permissions = await getAllUserPermissions(userId);
415
+ return isAdminPermission(permissions);
416
+ },
417
+ };
418
+ }