@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/src/schema.ts CHANGED
@@ -18,6 +18,15 @@
18
18
  * - mdm_rollbacks: Rollback operation history and status
19
19
  * - mdm_webhook_endpoints: Outbound webhook configuration
20
20
  * - mdm_webhook_deliveries: Webhook delivery history
21
+ * - mdm_tenants: Multi-tenant organization isolation
22
+ * - mdm_roles: RBAC role definitions
23
+ * - mdm_users: User accounts for authorization
24
+ * - mdm_user_roles: User-role mapping
25
+ * - mdm_audit_logs: Compliance and audit trail
26
+ * - mdm_scheduled_tasks: Scheduled task definitions
27
+ * - mdm_task_executions: Task execution history
28
+ * - mdm_message_queue: Persistent push message queue
29
+ * - mdm_plugin_storage: Plugin state persistence
21
30
  */
22
31
 
23
32
  // ============================================
@@ -456,6 +465,274 @@ export const mdmSchema: SchemaDefinition = {
456
465
  { columns: ['created_at'] },
457
466
  ],
458
467
  },
468
+
469
+ // ----------------------------------------
470
+ // Tenants Table (Multi-tenancy)
471
+ // ----------------------------------------
472
+ mdm_tenants: {
473
+ columns: {
474
+ id: { type: 'string', primaryKey: true },
475
+ name: { type: 'string' },
476
+ slug: { type: 'string', unique: true },
477
+ status: {
478
+ type: 'enum',
479
+ enumValues: ['active', 'suspended', 'pending'],
480
+ default: 'pending',
481
+ },
482
+ settings: { type: 'json', nullable: true },
483
+ metadata: { type: 'json', nullable: true },
484
+ created_at: { type: 'datetime', default: 'now' },
485
+ updated_at: { type: 'datetime', default: 'now' },
486
+ },
487
+ indexes: [
488
+ { columns: ['slug'], unique: true },
489
+ { columns: ['status'] },
490
+ ],
491
+ },
492
+
493
+ // ----------------------------------------
494
+ // Roles Table (RBAC)
495
+ // ----------------------------------------
496
+ mdm_roles: {
497
+ columns: {
498
+ id: { type: 'string', primaryKey: true },
499
+ tenant_id: {
500
+ type: 'string',
501
+ nullable: true,
502
+ references: { table: 'mdm_tenants', column: 'id', onDelete: 'cascade' },
503
+ },
504
+ name: { type: 'string' },
505
+ description: { type: 'text', nullable: true },
506
+ permissions: { type: 'json' },
507
+ is_system: { type: 'boolean', default: false },
508
+ created_at: { type: 'datetime', default: 'now' },
509
+ updated_at: { type: 'datetime', default: 'now' },
510
+ },
511
+ indexes: [
512
+ { columns: ['tenant_id'] },
513
+ { columns: ['name'] },
514
+ { columns: ['tenant_id', 'name'], unique: true },
515
+ ],
516
+ },
517
+
518
+ // ----------------------------------------
519
+ // Users Table (RBAC)
520
+ // ----------------------------------------
521
+ mdm_users: {
522
+ columns: {
523
+ id: { type: 'string', primaryKey: true },
524
+ tenant_id: {
525
+ type: 'string',
526
+ nullable: true,
527
+ references: { table: 'mdm_tenants', column: 'id', onDelete: 'cascade' },
528
+ },
529
+ email: { type: 'string' },
530
+ name: { type: 'string', nullable: true },
531
+ status: {
532
+ type: 'enum',
533
+ enumValues: ['active', 'inactive', 'pending'],
534
+ default: 'pending',
535
+ },
536
+ metadata: { type: 'json', nullable: true },
537
+ last_login_at: { type: 'datetime', nullable: true },
538
+ created_at: { type: 'datetime', default: 'now' },
539
+ updated_at: { type: 'datetime', default: 'now' },
540
+ },
541
+ indexes: [
542
+ { columns: ['tenant_id'] },
543
+ { columns: ['email'] },
544
+ { columns: ['tenant_id', 'email'], unique: true },
545
+ { columns: ['status'] },
546
+ ],
547
+ },
548
+
549
+ // ----------------------------------------
550
+ // User Roles (Many-to-Many)
551
+ // ----------------------------------------
552
+ mdm_user_roles: {
553
+ columns: {
554
+ user_id: {
555
+ type: 'string',
556
+ references: { table: 'mdm_users', column: 'id', onDelete: 'cascade' },
557
+ },
558
+ role_id: {
559
+ type: 'string',
560
+ references: { table: 'mdm_roles', column: 'id', onDelete: 'cascade' },
561
+ },
562
+ created_at: { type: 'datetime', default: 'now' },
563
+ },
564
+ indexes: [
565
+ { columns: ['user_id', 'role_id'], unique: true },
566
+ { columns: ['user_id'] },
567
+ { columns: ['role_id'] },
568
+ ],
569
+ },
570
+
571
+ // ----------------------------------------
572
+ // Audit Logs Table
573
+ // ----------------------------------------
574
+ mdm_audit_logs: {
575
+ columns: {
576
+ id: { type: 'string', primaryKey: true },
577
+ tenant_id: {
578
+ type: 'string',
579
+ nullable: true,
580
+ references: { table: 'mdm_tenants', column: 'id', onDelete: 'cascade' },
581
+ },
582
+ user_id: {
583
+ type: 'string',
584
+ nullable: true,
585
+ references: { table: 'mdm_users', column: 'id', onDelete: 'set null' },
586
+ },
587
+ action: { type: 'string' },
588
+ resource: { type: 'string' },
589
+ resource_id: { type: 'string', nullable: true },
590
+ details: { type: 'json', nullable: true },
591
+ ip_address: { type: 'string', nullable: true },
592
+ user_agent: { type: 'text', nullable: true },
593
+ created_at: { type: 'datetime', default: 'now' },
594
+ },
595
+ indexes: [
596
+ { columns: ['tenant_id'] },
597
+ { columns: ['user_id'] },
598
+ { columns: ['action'] },
599
+ { columns: ['resource'] },
600
+ { columns: ['resource', 'resource_id'] },
601
+ { columns: ['created_at'] },
602
+ ],
603
+ },
604
+
605
+ // ----------------------------------------
606
+ // Scheduled Tasks Table
607
+ // ----------------------------------------
608
+ mdm_scheduled_tasks: {
609
+ columns: {
610
+ id: { type: 'string', primaryKey: true },
611
+ tenant_id: {
612
+ type: 'string',
613
+ nullable: true,
614
+ references: { table: 'mdm_tenants', column: 'id', onDelete: 'cascade' },
615
+ },
616
+ name: { type: 'string' },
617
+ description: { type: 'text', nullable: true },
618
+ task_type: {
619
+ type: 'enum',
620
+ enumValues: ['command', 'policy_update', 'app_install', 'maintenance', 'custom'],
621
+ },
622
+ schedule: { type: 'json' },
623
+ target: { type: 'json', nullable: true },
624
+ payload: { type: 'json', nullable: true },
625
+ status: {
626
+ type: 'enum',
627
+ enumValues: ['active', 'paused', 'completed', 'failed'],
628
+ default: 'active',
629
+ },
630
+ next_run_at: { type: 'datetime', nullable: true },
631
+ last_run_at: { type: 'datetime', nullable: true },
632
+ max_retries: { type: 'integer', default: 3 },
633
+ retry_count: { type: 'integer', default: 0 },
634
+ created_at: { type: 'datetime', default: 'now' },
635
+ updated_at: { type: 'datetime', default: 'now' },
636
+ },
637
+ indexes: [
638
+ { columns: ['tenant_id'] },
639
+ { columns: ['task_type'] },
640
+ { columns: ['status'] },
641
+ { columns: ['next_run_at'] },
642
+ ],
643
+ },
644
+
645
+ // ----------------------------------------
646
+ // Task Executions Table
647
+ // ----------------------------------------
648
+ mdm_task_executions: {
649
+ columns: {
650
+ id: { type: 'string', primaryKey: true },
651
+ task_id: {
652
+ type: 'string',
653
+ references: { table: 'mdm_scheduled_tasks', column: 'id', onDelete: 'cascade' },
654
+ },
655
+ status: {
656
+ type: 'enum',
657
+ enumValues: ['running', 'completed', 'failed'],
658
+ default: 'running',
659
+ },
660
+ started_at: { type: 'datetime', default: 'now' },
661
+ completed_at: { type: 'datetime', nullable: true },
662
+ devices_processed: { type: 'integer', default: 0 },
663
+ devices_succeeded: { type: 'integer', default: 0 },
664
+ devices_failed: { type: 'integer', default: 0 },
665
+ error: { type: 'text', nullable: true },
666
+ details: { type: 'json', nullable: true },
667
+ },
668
+ indexes: [
669
+ { columns: ['task_id'] },
670
+ { columns: ['status'] },
671
+ { columns: ['started_at'] },
672
+ ],
673
+ },
674
+
675
+ // ----------------------------------------
676
+ // Message Queue Table
677
+ // ----------------------------------------
678
+ mdm_message_queue: {
679
+ columns: {
680
+ id: { type: 'string', primaryKey: true },
681
+ tenant_id: {
682
+ type: 'string',
683
+ nullable: true,
684
+ references: { table: 'mdm_tenants', column: 'id', onDelete: 'cascade' },
685
+ },
686
+ device_id: {
687
+ type: 'string',
688
+ references: { table: 'mdm_devices', column: 'id', onDelete: 'cascade' },
689
+ },
690
+ message_type: { type: 'string' },
691
+ payload: { type: 'json' },
692
+ priority: {
693
+ type: 'enum',
694
+ enumValues: ['high', 'normal', 'low'],
695
+ default: 'normal',
696
+ },
697
+ status: {
698
+ type: 'enum',
699
+ enumValues: ['pending', 'processing', 'delivered', 'failed', 'expired'],
700
+ default: 'pending',
701
+ },
702
+ attempts: { type: 'integer', default: 0 },
703
+ max_attempts: { type: 'integer', default: 3 },
704
+ last_attempt_at: { type: 'datetime', nullable: true },
705
+ last_error: { type: 'text', nullable: true },
706
+ expires_at: { type: 'datetime', nullable: true },
707
+ created_at: { type: 'datetime', default: 'now' },
708
+ updated_at: { type: 'datetime', default: 'now' },
709
+ },
710
+ indexes: [
711
+ { columns: ['tenant_id'] },
712
+ { columns: ['device_id'] },
713
+ { columns: ['status'] },
714
+ { columns: ['priority'] },
715
+ { columns: ['expires_at'] },
716
+ { columns: ['device_id', 'status', 'priority'] },
717
+ ],
718
+ },
719
+
720
+ // ----------------------------------------
721
+ // Plugin Storage Table
722
+ // ----------------------------------------
723
+ mdm_plugin_storage: {
724
+ columns: {
725
+ plugin_name: { type: 'string' },
726
+ key: { type: 'string' },
727
+ value: { type: 'json' },
728
+ created_at: { type: 'datetime', default: 'now' },
729
+ updated_at: { type: 'datetime', default: 'now' },
730
+ },
731
+ indexes: [
732
+ { columns: ['plugin_name', 'key'], unique: true },
733
+ { columns: ['plugin_name'] },
734
+ ],
735
+ },
459
736
  },
460
737
  };
461
738
 
package/src/tenant.ts ADDED
@@ -0,0 +1,237 @@
1
+ /**
2
+ * OpenMDM Tenant Manager
3
+ *
4
+ * Provides multi-tenancy support for the MDM system.
5
+ * Enables organization isolation, tenant management, and resource quotas.
6
+ */
7
+
8
+ import type {
9
+ Tenant,
10
+ TenantManager,
11
+ TenantFilter,
12
+ TenantListResult,
13
+ TenantStats,
14
+ CreateTenantInput,
15
+ UpdateTenantInput,
16
+ DatabaseAdapter,
17
+ } from './types';
18
+ import { TenantNotFoundError, ValidationError } from './types';
19
+
20
+ /**
21
+ * Generate a unique ID for entities
22
+ */
23
+ function generateId(): string {
24
+ return crypto.randomUUID();
25
+ }
26
+
27
+ /**
28
+ * Validate tenant slug format
29
+ */
30
+ function validateSlug(slug: string): boolean {
31
+ // Slug must be lowercase alphanumeric with hyphens, 3-50 chars
32
+ const slugRegex = /^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/;
33
+ return slugRegex.test(slug);
34
+ }
35
+
36
+ /**
37
+ * Create a TenantManager instance
38
+ */
39
+ export function createTenantManager(db: DatabaseAdapter): TenantManager {
40
+ return {
41
+ async get(id: string): Promise<Tenant | null> {
42
+ if (!db.findTenant) {
43
+ throw new Error('Database adapter does not support tenant operations');
44
+ }
45
+ return db.findTenant(id);
46
+ },
47
+
48
+ async getBySlug(slug: string): Promise<Tenant | null> {
49
+ if (!db.findTenantBySlug) {
50
+ throw new Error('Database adapter does not support tenant operations');
51
+ }
52
+ return db.findTenantBySlug(slug);
53
+ },
54
+
55
+ async list(filter?: TenantFilter): Promise<TenantListResult> {
56
+ if (!db.listTenants) {
57
+ throw new Error('Database adapter does not support tenant operations');
58
+ }
59
+ return db.listTenants(filter);
60
+ },
61
+
62
+ async create(data: CreateTenantInput): Promise<Tenant> {
63
+ if (!db.createTenant || !db.findTenantBySlug) {
64
+ throw new Error('Database adapter does not support tenant operations');
65
+ }
66
+
67
+ // Validate slug format
68
+ if (!validateSlug(data.slug)) {
69
+ throw new ValidationError(
70
+ 'Invalid slug format. Must be 3-50 lowercase alphanumeric characters with hyphens.',
71
+ { slug: data.slug }
72
+ );
73
+ }
74
+
75
+ // Check for duplicate slug
76
+ const existing = await db.findTenantBySlug(data.slug);
77
+ if (existing) {
78
+ throw new ValidationError(`Tenant with slug '${data.slug}' already exists`, {
79
+ slug: data.slug,
80
+ });
81
+ }
82
+
83
+ return db.createTenant({
84
+ ...data,
85
+ slug: data.slug.toLowerCase(),
86
+ });
87
+ },
88
+
89
+ async update(id: string, data: UpdateTenantInput): Promise<Tenant> {
90
+ if (!db.updateTenant || !db.findTenant || !db.findTenantBySlug) {
91
+ throw new Error('Database adapter does not support tenant operations');
92
+ }
93
+
94
+ const tenant = await db.findTenant(id);
95
+ if (!tenant) {
96
+ throw new TenantNotFoundError(id);
97
+ }
98
+
99
+ // Validate new slug if provided
100
+ if (data.slug) {
101
+ if (!validateSlug(data.slug)) {
102
+ throw new ValidationError(
103
+ 'Invalid slug format. Must be 3-50 lowercase alphanumeric characters with hyphens.',
104
+ { slug: data.slug }
105
+ );
106
+ }
107
+
108
+ // Check for duplicate slug
109
+ const existing = await db.findTenantBySlug(data.slug);
110
+ if (existing && existing.id !== id) {
111
+ throw new ValidationError(`Tenant with slug '${data.slug}' already exists`, {
112
+ slug: data.slug,
113
+ });
114
+ }
115
+
116
+ data.slug = data.slug.toLowerCase();
117
+ }
118
+
119
+ return db.updateTenant(id, data);
120
+ },
121
+
122
+ async delete(id: string, cascade: boolean = false): Promise<void> {
123
+ if (!db.deleteTenant || !db.findTenant) {
124
+ throw new Error('Database adapter does not support tenant operations');
125
+ }
126
+
127
+ const tenant = await db.findTenant(id);
128
+ if (!tenant) {
129
+ throw new TenantNotFoundError(id);
130
+ }
131
+
132
+ // If cascade is true, the database adapter should handle
133
+ // deletion of all related resources (devices, policies, etc.)
134
+ // This is typically done via ON DELETE CASCADE in the schema
135
+
136
+ await db.deleteTenant(id);
137
+ },
138
+
139
+ async getStats(tenantId: string): Promise<TenantStats> {
140
+ if (!db.getTenantStats || !db.findTenant) {
141
+ throw new Error('Database adapter does not support tenant operations');
142
+ }
143
+
144
+ const tenant = await db.findTenant(tenantId);
145
+ if (!tenant) {
146
+ throw new TenantNotFoundError(tenantId);
147
+ }
148
+
149
+ return db.getTenantStats(tenantId);
150
+ },
151
+
152
+ async activate(id: string): Promise<Tenant> {
153
+ if (!db.updateTenant || !db.findTenant) {
154
+ throw new Error('Database adapter does not support tenant operations');
155
+ }
156
+
157
+ const tenant = await db.findTenant(id);
158
+ if (!tenant) {
159
+ throw new TenantNotFoundError(id);
160
+ }
161
+
162
+ if (tenant.status === 'active') {
163
+ return tenant;
164
+ }
165
+
166
+ return db.updateTenant(id, { status: 'active' });
167
+ },
168
+
169
+ async deactivate(id: string): Promise<Tenant> {
170
+ if (!db.updateTenant || !db.findTenant) {
171
+ throw new Error('Database adapter does not support tenant operations');
172
+ }
173
+
174
+ const tenant = await db.findTenant(id);
175
+ if (!tenant) {
176
+ throw new TenantNotFoundError(id);
177
+ }
178
+
179
+ if (tenant.status === 'suspended') {
180
+ return tenant;
181
+ }
182
+
183
+ return db.updateTenant(id, { status: 'suspended' });
184
+ },
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Default system roles that can be used across tenants
190
+ */
191
+ export const DEFAULT_SYSTEM_ROLES = {
192
+ SUPER_ADMIN: {
193
+ name: 'Super Admin',
194
+ description: 'Full system access across all tenants',
195
+ permissions: [{ action: '*' as const, resource: '*' as const }],
196
+ isSystem: true,
197
+ },
198
+ TENANT_ADMIN: {
199
+ name: 'Tenant Admin',
200
+ description: 'Full access within the tenant',
201
+ permissions: [
202
+ { action: 'manage' as const, resource: 'devices' as const },
203
+ { action: 'manage' as const, resource: 'policies' as const },
204
+ { action: 'manage' as const, resource: 'applications' as const },
205
+ { action: 'manage' as const, resource: 'commands' as const },
206
+ { action: 'manage' as const, resource: 'groups' as const },
207
+ { action: 'manage' as const, resource: 'users' as const },
208
+ { action: 'read' as const, resource: 'audit' as const },
209
+ ],
210
+ isSystem: true,
211
+ },
212
+ DEVICE_MANAGER: {
213
+ name: 'Device Manager',
214
+ description: 'Manage devices and send commands',
215
+ permissions: [
216
+ { action: 'manage' as const, resource: 'devices' as const },
217
+ { action: 'manage' as const, resource: 'commands' as const },
218
+ { action: 'read' as const, resource: 'policies' as const },
219
+ { action: 'read' as const, resource: 'groups' as const },
220
+ ],
221
+ isSystem: true,
222
+ },
223
+ VIEWER: {
224
+ name: 'Viewer',
225
+ description: 'Read-only access to all resources',
226
+ permissions: [
227
+ { action: 'read' as const, resource: 'devices' as const },
228
+ { action: 'read' as const, resource: 'policies' as const },
229
+ { action: 'read' as const, resource: 'applications' as const },
230
+ { action: 'read' as const, resource: 'commands' as const },
231
+ { action: 'read' as const, resource: 'groups' as const },
232
+ ],
233
+ isSystem: true,
234
+ },
235
+ };
236
+
237
+ export type SystemRoleName = keyof typeof DEFAULT_SYSTEM_ROLES;