@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.
- package/dist/index.d.ts +105 -3
- package/dist/index.js +1553 -41
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +9 -0
- package/dist/schema.js +259 -0
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +591 -1
- package/dist/types.js +21 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/audit.ts +317 -0
- package/src/authorization.ts +418 -0
- package/src/dashboard.ts +327 -0
- package/src/index.ts +222 -0
- package/src/plugin-storage.ts +128 -0
- package/src/queue.ts +161 -0
- package/src/schedule.ts +325 -0
- package/src/schema.ts +277 -0
- package/src/tenant.ts +237 -0
- package/src/types.ts +708 -0
|
@@ -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
|
+
}
|