@grc-claw/rbac-multi-tenant 0.8.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,42 @@
1
+ import type { Role, RoleName, ScopeLevel, Resource, Action, Permission, UserRoleAssignment, TenantConfig, PermissionCheck, PermissionResult, AuditLogEntry, JWTPayload, RBACConfig } from "./types.js";
2
+ export declare class RBACEngine {
3
+ private roles;
4
+ private assignments;
5
+ private tenants;
6
+ private auditLog;
7
+ private config;
8
+ constructor(config: RBACConfig);
9
+ createTenant(name: string, parentTenantId?: string): TenantConfig;
10
+ getTenant(tenantId: string): TenantConfig | undefined;
11
+ assignRole(userId: string, role: RoleName, scope: ScopeLevel, tenantId: string, assignedBy: string, scopeId?: string): UserRoleAssignment;
12
+ removeRole(userId: string, tenantId: string, role?: RoleName): boolean;
13
+ getUserRoles(userId: string, tenantId: string): UserRoleAssignment[];
14
+ defineCustomRole(name: string, permissions: Permission[], description: string): Role;
15
+ checkPermission(check: PermissionCheck): PermissionResult;
16
+ hasPermission(userId: string, tenantId: string, resource: Resource, action: Action, scopeId?: string): boolean;
17
+ generateJWT(userId: string, tenantId: string): string;
18
+ verifyJWT(token: string): JWTPayload | null;
19
+ createMiddleware(): (req: {
20
+ headers: Record<string, string | undefined>;
21
+ userId?: string;
22
+ tenantId?: string;
23
+ }, res: {
24
+ status: (code: number) => {
25
+ json: (body: unknown) => void;
26
+ };
27
+ }, next: () => void) => void;
28
+ requirePermission(resource: Resource, action: Action): (req: {
29
+ userId?: string;
30
+ tenantId?: string;
31
+ }, res: {
32
+ status: (code: number) => {
33
+ json: (body: unknown) => void;
34
+ };
35
+ }, next: () => void) => void;
36
+ getAuditLog(tenantId?: string, limit?: number): AuditLogEntry[];
37
+ getRolePermissions(roleName: RoleName): Permission[];
38
+ getAllRoles(): Role[];
39
+ private validateScope;
40
+ private logAudit;
41
+ private base64url;
42
+ }
@@ -0,0 +1,349 @@
1
+ import { randomUUID, createHmac } from "node:crypto";
2
+ const BUILTIN_ROLES = {
3
+ admin: {
4
+ name: "admin",
5
+ description: "Full system administrator with unrestricted access",
6
+ permissions: [
7
+ { resource: "frameworks", actions: ["read", "write", "delete", "manage"] },
8
+ { resource: "evidence", actions: ["read", "write", "delete", "export"] },
9
+ { resource: "policies", actions: ["read", "write", "delete", "approve"] },
10
+ { resource: "controls", actions: ["read", "write", "delete"] },
11
+ { resource: "assessments", actions: ["read", "write", "delete", "approve"] },
12
+ { resource: "vendors", actions: ["read", "write", "delete"] },
13
+ { resource: "risks", actions: ["read", "write", "delete"] },
14
+ { resource: "incidents", actions: ["read", "write", "delete"] },
15
+ { resource: "connectors", actions: ["read", "write", "delete", "manage"] },
16
+ { resource: "users", actions: ["read", "write", "delete", "manage"] },
17
+ { resource: "roles", actions: ["read", "write", "delete", "manage"] },
18
+ { resource: "tenants", actions: ["read", "write", "delete", "manage"] },
19
+ { resource: "audit_logs", actions: ["read", "export"] },
20
+ { resource: "reports", actions: ["read", "write", "export"] },
21
+ { resource: "settings", actions: ["read", "write"] },
22
+ ],
23
+ },
24
+ compliance_officer: {
25
+ name: "compliance_officer",
26
+ description: "Manages compliance frameworks, policies, and evidence",
27
+ permissions: [
28
+ { resource: "frameworks", actions: ["read", "write"] },
29
+ { resource: "evidence", actions: ["read", "write", "export"] },
30
+ { resource: "policies", actions: ["read", "write", "approve"] },
31
+ { resource: "controls", actions: ["read", "write"] },
32
+ { resource: "assessments", actions: ["read", "write", "approve"] },
33
+ { resource: "vendors", actions: ["read", "write"] },
34
+ { resource: "risks", actions: ["read", "write"] },
35
+ { resource: "incidents", actions: ["read", "write"] },
36
+ { resource: "audit_logs", actions: ["read"] },
37
+ { resource: "reports", actions: ["read", "write", "export"] },
38
+ ],
39
+ },
40
+ auditor: {
41
+ name: "auditor",
42
+ description: "Read-only access for audit and review purposes",
43
+ permissions: [
44
+ { resource: "frameworks", actions: ["read"] },
45
+ { resource: "evidence", actions: ["read", "export"] },
46
+ { resource: "policies", actions: ["read"] },
47
+ { resource: "controls", actions: ["read"] },
48
+ { resource: "assessments", actions: ["read"] },
49
+ { resource: "vendors", actions: ["read"] },
50
+ { resource: "risks", actions: ["read"] },
51
+ { resource: "incidents", actions: ["read"] },
52
+ { resource: "audit_logs", actions: ["read", "export"] },
53
+ { resource: "reports", actions: ["read", "export"] },
54
+ ],
55
+ },
56
+ viewer: {
57
+ name: "viewer",
58
+ description: "Basic read-only access to view compliance data",
59
+ permissions: [
60
+ { resource: "frameworks", actions: ["read"] },
61
+ { resource: "evidence", actions: ["read"] },
62
+ { resource: "policies", actions: ["read"] },
63
+ { resource: "controls", actions: ["read"] },
64
+ { resource: "assessments", actions: ["read"] },
65
+ { resource: "reports", actions: ["read"] },
66
+ ],
67
+ },
68
+ custom: {
69
+ name: "custom",
70
+ description: "Custom role with user-defined permissions",
71
+ permissions: [],
72
+ },
73
+ };
74
+ export class RBACEngine {
75
+ roles = new Map();
76
+ assignments = new Map();
77
+ tenants = new Map();
78
+ auditLog = [];
79
+ config;
80
+ constructor(config) {
81
+ this.config = {
82
+ jwtExpiresIn: 3600,
83
+ auditLogLimit: 10000,
84
+ ...config,
85
+ };
86
+ for (const [name, role] of Object.entries(BUILTIN_ROLES)) {
87
+ this.roles.set(name, { ...role });
88
+ }
89
+ }
90
+ createTenant(name, parentTenantId) {
91
+ const tenant = {
92
+ id: `tenant-${randomUUID()}`,
93
+ name,
94
+ parentTenantId,
95
+ createdAt: new Date().toISOString(),
96
+ settings: {},
97
+ };
98
+ this.tenants.set(tenant.id, tenant);
99
+ return tenant;
100
+ }
101
+ getTenant(tenantId) {
102
+ return this.tenants.get(tenantId);
103
+ }
104
+ assignRole(userId, role, scope, tenantId, assignedBy, scopeId) {
105
+ const roleDef = this.roles.get(role);
106
+ if (!roleDef)
107
+ throw new Error(`Role not found: ${role}`);
108
+ const assignment = {
109
+ userId,
110
+ role,
111
+ scope,
112
+ scopeId,
113
+ tenantId,
114
+ assignedAt: new Date().toISOString(),
115
+ assignedBy,
116
+ };
117
+ const key = `${userId}:${tenantId}`;
118
+ const existing = this.assignments.get(key) || [];
119
+ existing.push(assignment);
120
+ this.assignments.set(key, existing);
121
+ this.logAudit({
122
+ userId: assignedBy,
123
+ tenantId,
124
+ resource: "roles",
125
+ action: "write",
126
+ allowed: true,
127
+ role: "admin",
128
+ scope: "global",
129
+ reason: `Assigned role ${role} to user ${userId}`,
130
+ });
131
+ return assignment;
132
+ }
133
+ removeRole(userId, tenantId, role) {
134
+ const key = `${userId}:${tenantId}`;
135
+ const existing = this.assignments.get(key);
136
+ if (!existing)
137
+ return false;
138
+ if (role) {
139
+ const filtered = existing.filter((a) => a.role !== role);
140
+ this.assignments.set(key, filtered);
141
+ return filtered.length < existing.length;
142
+ }
143
+ return this.assignments.delete(key);
144
+ }
145
+ getUserRoles(userId, tenantId) {
146
+ return this.assignments.get(`${userId}:${tenantId}`) || [];
147
+ }
148
+ defineCustomRole(name, permissions, description) {
149
+ const role = { name: "custom", permissions, description };
150
+ this.roles.set(name, role);
151
+ return role;
152
+ }
153
+ checkPermission(check) {
154
+ const assignments = this.getUserRoles(check.userId, check.tenantId);
155
+ if (assignments.length === 0) {
156
+ const result = {
157
+ allowed: false,
158
+ role: "viewer",
159
+ scope: "global",
160
+ reason: "No role assignments found",
161
+ };
162
+ this.logAudit({
163
+ ...check,
164
+ ...result,
165
+ });
166
+ return result;
167
+ }
168
+ for (const assignment of assignments) {
169
+ const roleDef = this.roles.get(assignment.role);
170
+ if (!roleDef)
171
+ continue;
172
+ for (const perm of roleDef.permissions) {
173
+ if (perm.resource === check.resource && perm.actions.includes(check.action)) {
174
+ const scopeValid = this.validateScope(assignment, check.scopeId);
175
+ const result = {
176
+ allowed: scopeValid,
177
+ role: assignment.role,
178
+ scope: assignment.scope,
179
+ reason: scopeValid
180
+ ? `Granted via role ${assignment.role}`
181
+ : `Insufficient scope: required ${assignment.scope}`,
182
+ };
183
+ this.logAudit({
184
+ ...check,
185
+ ...result,
186
+ });
187
+ return result;
188
+ }
189
+ }
190
+ }
191
+ const result = {
192
+ allowed: false,
193
+ role: assignments[0].role,
194
+ scope: assignments[0].scope,
195
+ reason: `No matching permission for ${check.resource}:${check.action}`,
196
+ };
197
+ this.logAudit({
198
+ ...check,
199
+ ...result,
200
+ });
201
+ return result;
202
+ }
203
+ hasPermission(userId, tenantId, resource, action, scopeId) {
204
+ return this.checkPermission({ userId, tenantId, resource, action, scopeId }).allowed;
205
+ }
206
+ generateJWT(userId, tenantId) {
207
+ const assignments = this.getUserRoles(userId, tenantId);
208
+ const primaryRole = assignments[0]?.role || "viewer";
209
+ const primaryScope = assignments[0]?.scope || "global";
210
+ const scopeId = assignments[0]?.scopeId;
211
+ const permissions = [];
212
+ for (const assignment of assignments) {
213
+ const roleDef = this.roles.get(assignment.role);
214
+ if (roleDef) {
215
+ for (const perm of roleDef.permissions) {
216
+ for (const action of perm.actions) {
217
+ const p = `${perm.resource}:${action}`;
218
+ if (!permissions.includes(p))
219
+ permissions.push(p);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ const now = Math.floor(Date.now() / 1000);
225
+ const payload = {
226
+ sub: userId,
227
+ tenant_id: tenantId,
228
+ role: primaryRole,
229
+ scope: primaryScope,
230
+ scope_id: scopeId,
231
+ permissions,
232
+ iat: now,
233
+ exp: now + (this.config.jwtExpiresIn || 3600),
234
+ };
235
+ const header = this.base64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
236
+ const body = this.base64url(JSON.stringify(payload));
237
+ const signature = createHmac("sha256", this.config.jwtSecret)
238
+ .update(`${header}.${body}`)
239
+ .digest("base64url");
240
+ return `${header}.${body}.${signature}`;
241
+ }
242
+ verifyJWT(token) {
243
+ try {
244
+ const parts = token.split(".");
245
+ if (parts.length !== 3)
246
+ return null;
247
+ const [header, body, signature] = parts;
248
+ const expectedSig = createHmac("sha256", this.config.jwtSecret)
249
+ .update(`${header}.${body}`)
250
+ .digest("base64url");
251
+ if (signature !== expectedSig)
252
+ return null;
253
+ const payload = JSON.parse(Buffer.from(body, "base64url").toString());
254
+ if (payload.exp < Math.floor(Date.now() / 1000))
255
+ return null;
256
+ return payload;
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ }
262
+ createMiddleware() {
263
+ return (req, res, next) => {
264
+ const authHeader = req.headers.authorization;
265
+ if (!authHeader?.startsWith("Bearer ")) {
266
+ res.status(401).json({ error: "Missing or invalid authorization header" });
267
+ return;
268
+ }
269
+ const token = authHeader.slice(7);
270
+ const payload = this.verifyJWT(token);
271
+ if (!payload) {
272
+ res.status(401).json({ error: "Invalid or expired token" });
273
+ return;
274
+ }
275
+ req.userId = payload.sub;
276
+ req.tenantId = payload.tenant_id;
277
+ next();
278
+ };
279
+ }
280
+ requirePermission(resource, action) {
281
+ return (req, res, next) => {
282
+ if (!req.userId || !req.tenantId) {
283
+ res.status(401).json({ error: "Not authenticated" });
284
+ return;
285
+ }
286
+ const result = this.checkPermission({
287
+ userId: req.userId,
288
+ tenantId: req.tenantId,
289
+ resource,
290
+ action,
291
+ });
292
+ if (!result.allowed) {
293
+ res.status(403).json({
294
+ error: "Insufficient permissions",
295
+ reason: result.reason,
296
+ required: `${resource}:${action}`,
297
+ });
298
+ return;
299
+ }
300
+ next();
301
+ };
302
+ }
303
+ getAuditLog(tenantId, limit) {
304
+ let logs = tenantId
305
+ ? this.auditLog.filter((l) => l.tenantId === tenantId)
306
+ : this.auditLog;
307
+ const max = limit || this.config.auditLogLimit || 1000;
308
+ return logs.slice(-max);
309
+ }
310
+ getRolePermissions(roleName) {
311
+ return this.roles.get(roleName)?.permissions || [];
312
+ }
313
+ getAllRoles() {
314
+ return Array.from(this.roles.values());
315
+ }
316
+ validateScope(assignment, requestedScopeId) {
317
+ if (assignment.scope === "global")
318
+ return true;
319
+ if (assignment.scope === "entity") {
320
+ if (!assignment.scopeId)
321
+ return true;
322
+ return !requestedScopeId || assignment.scopeId === requestedScopeId;
323
+ }
324
+ if (assignment.scope === "department") {
325
+ if (!assignment.scopeId)
326
+ return true;
327
+ return !requestedScopeId || assignment.scopeId === requestedScopeId;
328
+ }
329
+ return false;
330
+ }
331
+ logAudit(entry) {
332
+ const logEntry = {
333
+ id: `audit-${randomUUID()}`,
334
+ timestamp: new Date().toISOString(),
335
+ ...entry,
336
+ };
337
+ this.auditLog.push(logEntry);
338
+ if (this.auditLog.length > (this.config.auditLogLimit || 10000)) {
339
+ this.auditLog = this.auditLog.slice(-((this.config.auditLogLimit || 10000) / 2));
340
+ }
341
+ }
342
+ base64url(data) {
343
+ return Buffer.from(data)
344
+ .toString("base64")
345
+ .replace(/\+/g, "-")
346
+ .replace(/\//g, "_")
347
+ .replace(/=+$/, "");
348
+ }
349
+ }
@@ -0,0 +1,2 @@
1
+ export { RBACEngine } from "./RBACEngine.js";
2
+ export type { RoleName, ScopeLevel, Resource, Action, PermissionString, Permission, Role, UserRoleAssignment, TenantConfig, PermissionCheck, PermissionResult, AuditLogEntry, JWTPayload, RBACConfig, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { RBACEngine } from "./RBACEngine.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,289 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { RBACEngine } from "./RBACEngine.js";
4
+ function createEngine() {
5
+ return new RBACEngine({ jwtSecret: "test-secret-key-12345" });
6
+ }
7
+ describe("RBACEngine", () => {
8
+ describe("Tenant Management", () => {
9
+ it("creates a tenant", () => {
10
+ const engine = createEngine();
11
+ const tenant = engine.createTenant("Acme Corp");
12
+ assert.ok(tenant.id.startsWith("tenant-"));
13
+ assert.equal(tenant.name, "Acme Corp");
14
+ assert.ok(tenant.createdAt);
15
+ });
16
+ it("retrieves a tenant", () => {
17
+ const engine = createEngine();
18
+ const tenant = engine.createTenant("Acme Corp");
19
+ const retrieved = engine.getTenant(tenant.id);
20
+ assert.deepStrictEqual(retrieved, tenant);
21
+ });
22
+ it("returns undefined for unknown tenant", () => {
23
+ const engine = createEngine();
24
+ assert.equal(engine.getTenant("unknown"), undefined);
25
+ });
26
+ });
27
+ describe("Role Assignment", () => {
28
+ it("assigns a role to a user", () => {
29
+ const engine = createEngine();
30
+ const tenant = engine.createTenant("Acme Corp");
31
+ const assignment = engine.assignRole("user-1", "admin", "global", tenant.id, "system");
32
+ assert.equal(assignment.userId, "user-1");
33
+ assert.equal(assignment.role, "admin");
34
+ assert.equal(assignment.scope, "global");
35
+ assert.equal(assignment.tenantId, tenant.id);
36
+ });
37
+ it("retrieves user roles", () => {
38
+ const engine = createEngine();
39
+ const tenant = engine.createTenant("Acme Corp");
40
+ engine.assignRole("user-1", "admin", "global", tenant.id, "system");
41
+ engine.assignRole("user-1", "auditor", "entity", tenant.id, "system", "entity-1");
42
+ const roles = engine.getUserRoles("user-1", tenant.id);
43
+ assert.equal(roles.length, 2);
44
+ });
45
+ it("removes a specific role", () => {
46
+ const engine = createEngine();
47
+ const tenant = engine.createTenant("Acme Corp");
48
+ engine.assignRole("user-1", "admin", "global", tenant.id, "system");
49
+ engine.assignRole("user-1", "auditor", "global", tenant.id, "system");
50
+ const removed = engine.removeRole("user-1", tenant.id, "admin");
51
+ assert.equal(removed, true);
52
+ const roles = engine.getUserRoles("user-1", tenant.id);
53
+ assert.equal(roles.length, 1);
54
+ assert.equal(roles[0].role, "auditor");
55
+ });
56
+ it("removes all roles for a user", () => {
57
+ const engine = createEngine();
58
+ const tenant = engine.createTenant("Acme Corp");
59
+ engine.assignRole("user-1", "admin", "global", tenant.id, "system");
60
+ engine.assignRole("user-1", "auditor", "global", tenant.id, "system");
61
+ const removed = engine.removeRole("user-1", tenant.id);
62
+ assert.equal(removed, true);
63
+ assert.equal(engine.getUserRoles("user-1", tenant.id).length, 0);
64
+ });
65
+ it("throws for unknown role", () => {
66
+ const engine = createEngine();
67
+ const tenant = engine.createTenant("Acme Corp");
68
+ assert.throws(() => engine.assignRole("user-1", "nonexistent", "global", tenant.id, "system"), /Role not found/);
69
+ });
70
+ });
71
+ describe("Permission Checking", () => {
72
+ it("allows admin to write frameworks", () => {
73
+ const engine = createEngine();
74
+ const tenant = engine.createTenant("Acme Corp");
75
+ engine.assignRole("user-1", "admin", "global", tenant.id, "system");
76
+ const result = engine.checkPermission({
77
+ userId: "user-1",
78
+ tenantId: tenant.id,
79
+ resource: "frameworks",
80
+ action: "write",
81
+ });
82
+ assert.equal(result.allowed, true);
83
+ assert.equal(result.role, "admin");
84
+ });
85
+ it("denies viewer from writing frameworks", () => {
86
+ const engine = createEngine();
87
+ const tenant = engine.createTenant("Acme Corp");
88
+ engine.assignRole("user-1", "viewer", "global", tenant.id, "system");
89
+ const result = engine.checkPermission({
90
+ userId: "user-1",
91
+ tenantId: tenant.id,
92
+ resource: "frameworks",
93
+ action: "write",
94
+ });
95
+ assert.equal(result.allowed, false);
96
+ });
97
+ it("allows auditor to export evidence", () => {
98
+ const engine = createEngine();
99
+ const tenant = engine.createTenant("Acme Corp");
100
+ engine.assignRole("user-1", "auditor", "global", tenant.id, "system");
101
+ const result = engine.checkPermission({
102
+ userId: "user-1",
103
+ tenantId: tenant.id,
104
+ resource: "evidence",
105
+ action: "export",
106
+ });
107
+ assert.equal(result.allowed, true);
108
+ });
109
+ it("allows compliance_officer to approve policies", () => {
110
+ const engine = createEngine();
111
+ const tenant = engine.createTenant("Acme Corp");
112
+ engine.assignRole("user-1", "compliance_officer", "global", tenant.id, "system");
113
+ const result = engine.checkPermission({
114
+ userId: "user-1",
115
+ tenantId: tenant.id,
116
+ resource: "policies",
117
+ action: "approve",
118
+ });
119
+ assert.equal(result.allowed, true);
120
+ });
121
+ it("denies user with no assignments", () => {
122
+ const engine = createEngine();
123
+ const tenant = engine.createTenant("Acme Corp");
124
+ const result = engine.checkPermission({
125
+ userId: "unknown-user",
126
+ tenantId: tenant.id,
127
+ resource: "frameworks",
128
+ action: "read",
129
+ });
130
+ assert.equal(result.allowed, false);
131
+ assert.equal(result.reason, "No role assignments found");
132
+ });
133
+ it("hasPermission returns boolean", () => {
134
+ const engine = createEngine();
135
+ const tenant = engine.createTenant("Acme Corp");
136
+ engine.assignRole("user-1", "admin", "global", tenant.id, "system");
137
+ assert.equal(engine.hasPermission("user-1", tenant.id, "frameworks", "read"), true);
138
+ assert.equal(engine.hasPermission("user-1", tenant.id, "tenants", "delete"), true);
139
+ assert.equal(engine.hasPermission("user-2", tenant.id, "frameworks", "read"), false);
140
+ });
141
+ });
142
+ describe("Scope Validation", () => {
143
+ it("global scope allows access to all resources", () => {
144
+ const engine = createEngine();
145
+ const tenant = engine.createTenant("Acme Corp");
146
+ engine.assignRole("user-1", "admin", "global", tenant.id, "system");
147
+ const result = engine.checkPermission({
148
+ userId: "user-1",
149
+ tenantId: tenant.id,
150
+ resource: "frameworks",
151
+ action: "write",
152
+ });
153
+ assert.equal(result.allowed, true);
154
+ });
155
+ it("entity scope restricts to specific entity", () => {
156
+ const engine = createEngine();
157
+ const tenant = engine.createTenant("Acme Corp");
158
+ engine.assignRole("user-1", "compliance_officer", "entity", tenant.id, "system", "entity-1");
159
+ const result = engine.checkPermission({
160
+ userId: "user-1",
161
+ tenantId: tenant.id,
162
+ resource: "evidence",
163
+ action: "write",
164
+ scopeId: "entity-1",
165
+ });
166
+ assert.equal(result.allowed, true);
167
+ });
168
+ it("entity scope denies access to different entity", () => {
169
+ const engine = createEngine();
170
+ const tenant = engine.createTenant("Acme Corp");
171
+ engine.assignRole("user-1", "compliance_officer", "entity", tenant.id, "system", "entity-1");
172
+ const result = engine.checkPermission({
173
+ userId: "user-1",
174
+ tenantId: tenant.id,
175
+ resource: "evidence",
176
+ action: "write",
177
+ scopeId: "entity-2",
178
+ });
179
+ assert.equal(result.allowed, false);
180
+ });
181
+ });
182
+ describe("JWT Token", () => {
183
+ it("generates and verifies a valid JWT", () => {
184
+ const engine = createEngine();
185
+ const tenant = engine.createTenant("Acme Corp");
186
+ engine.assignRole("user-1", "admin", "global", tenant.id, "system");
187
+ const token = engine.generateJWT("user-1", tenant.id);
188
+ assert.ok(token);
189
+ assert.equal(token.split(".").length, 3);
190
+ const payload = engine.verifyJWT(token);
191
+ assert.ok(payload);
192
+ assert.equal(payload.sub, "user-1");
193
+ assert.equal(payload.tenant_id, tenant.id);
194
+ assert.equal(payload.role, "admin");
195
+ assert.ok(payload.permissions.length > 0);
196
+ });
197
+ it("rejects invalid token", () => {
198
+ const engine = createEngine();
199
+ assert.equal(engine.verifyJWT("invalid.token.here"), null);
200
+ });
201
+ it("rejects token with wrong secret", () => {
202
+ const engine1 = new RBACEngine({ jwtSecret: "secret-1" });
203
+ const engine2 = new RBACEngine({ jwtSecret: "secret-2" });
204
+ const tenant = engine1.createTenant("Acme Corp");
205
+ engine1.assignRole("user-1", "admin", "global", tenant.id, "system");
206
+ const token = engine1.generateJWT("user-1", tenant.id);
207
+ assert.equal(engine2.verifyJWT(token), null);
208
+ });
209
+ it("rejects expired token", () => {
210
+ const engine = new RBACEngine({ jwtSecret: "test", jwtExpiresIn: -1 });
211
+ const tenant = engine.createTenant("Acme Corp");
212
+ engine.assignRole("user-1", "admin", "global", tenant.id, "system");
213
+ const token = engine.generateJWT("user-1", tenant.id);
214
+ assert.equal(engine.verifyJWT(token), null);
215
+ });
216
+ });
217
+ describe("Audit Logging", () => {
218
+ it("logs permission checks", () => {
219
+ const engine = createEngine();
220
+ const tenant = engine.createTenant("Acme Corp");
221
+ engine.assignRole("user-1", "admin", "global", tenant.id, "system");
222
+ engine.checkPermission({
223
+ userId: "user-1",
224
+ tenantId: tenant.id,
225
+ resource: "frameworks",
226
+ action: "read",
227
+ });
228
+ const logs = engine.getAuditLog(tenant.id);
229
+ const permLog = logs.find((l) => l.resource === "frameworks");
230
+ assert.ok(permLog);
231
+ assert.equal(permLog.action, "read");
232
+ assert.equal(permLog.allowed, true);
233
+ });
234
+ it("logs role assignments", () => {
235
+ const engine = createEngine();
236
+ const tenant = engine.createTenant("Acme Corp");
237
+ engine.assignRole("user-1", "admin", "global", tenant.id, "system");
238
+ const logs = engine.getAuditLog(tenant.id);
239
+ assert.ok(logs.length > 0);
240
+ assert.equal(logs[0].resource, "roles");
241
+ assert.equal(logs[0].action, "write");
242
+ });
243
+ it("filters audit log by tenant", () => {
244
+ const engine = createEngine();
245
+ const tenant1 = engine.createTenant("Tenant 1");
246
+ const tenant2 = engine.createTenant("Tenant 2");
247
+ engine.assignRole("user-1", "admin", "global", tenant1.id, "system");
248
+ engine.assignRole("user-2", "admin", "global", tenant2.id, "system");
249
+ const logs1 = engine.getAuditLog(tenant1.id);
250
+ assert.ok(logs1.every((l) => l.tenantId === tenant1.id));
251
+ });
252
+ });
253
+ describe("Role Permissions", () => {
254
+ it("returns permissions for a role", () => {
255
+ const engine = createEngine();
256
+ const perms = engine.getRolePermissions("admin");
257
+ assert.ok(perms.length > 0);
258
+ assert.ok(perms.some((p) => p.resource === "frameworks"));
259
+ });
260
+ it("returns all built-in roles", () => {
261
+ const engine = createEngine();
262
+ const roles = engine.getAllRoles();
263
+ assert.ok(roles.length >= 5);
264
+ assert.ok(roles.some((r) => r.name === "admin"));
265
+ assert.ok(roles.some((r) => r.name === "viewer"));
266
+ });
267
+ });
268
+ describe("Multi-Tenant Isolation", () => {
269
+ it("isolates users across tenants", () => {
270
+ const engine = createEngine();
271
+ const tenant1 = engine.createTenant("Tenant 1");
272
+ const tenant2 = engine.createTenant("Tenant 2");
273
+ engine.assignRole("user-1", "admin", "global", tenant1.id, "system");
274
+ engine.assignRole("user-1", "viewer", "global", tenant2.id, "system");
275
+ const roles1 = engine.getUserRoles("user-1", tenant1.id);
276
+ const roles2 = engine.getUserRoles("user-1", tenant2.id);
277
+ assert.equal(roles1[0].role, "admin");
278
+ assert.equal(roles2[0].role, "viewer");
279
+ });
280
+ it("prevents cross-tenant permission leakage", () => {
281
+ const engine = createEngine();
282
+ const tenant1 = engine.createTenant("Tenant 1");
283
+ const tenant2 = engine.createTenant("Tenant 2");
284
+ engine.assignRole("user-1", "admin", "global", tenant1.id, "system");
285
+ assert.equal(engine.hasPermission("user-1", tenant1.id, "frameworks", "write"), true);
286
+ assert.equal(engine.hasPermission("user-1", tenant2.id, "frameworks", "write"), false);
287
+ });
288
+ });
289
+ });
@@ -0,0 +1,71 @@
1
+ export type RoleName = "admin" | "compliance_officer" | "auditor" | "viewer" | "custom";
2
+ export type ScopeLevel = "global" | "entity" | "department";
3
+ export type Resource = "frameworks" | "evidence" | "policies" | "controls" | "assessments" | "vendors" | "risks" | "incidents" | "connectors" | "users" | "roles" | "tenants" | "audit_logs" | "reports" | "settings";
4
+ export type Action = "read" | "write" | "delete" | "approve" | "export" | "manage";
5
+ export type PermissionString = `${Resource}:${Action}`;
6
+ export interface Permission {
7
+ resource: Resource;
8
+ actions: Action[];
9
+ }
10
+ export interface Role {
11
+ name: RoleName;
12
+ permissions: Permission[];
13
+ description: string;
14
+ }
15
+ export interface UserRoleAssignment {
16
+ userId: string;
17
+ role: RoleName;
18
+ scope: ScopeLevel;
19
+ scopeId?: string;
20
+ tenantId: string;
21
+ assignedAt: string;
22
+ assignedBy: string;
23
+ }
24
+ export interface TenantConfig {
25
+ id: string;
26
+ name: string;
27
+ parentTenantId?: string;
28
+ createdAt: string;
29
+ settings: Record<string, unknown>;
30
+ }
31
+ export interface PermissionCheck {
32
+ userId: string;
33
+ tenantId: string;
34
+ resource: Resource;
35
+ action: Action;
36
+ scopeId?: string;
37
+ }
38
+ export interface PermissionResult {
39
+ allowed: boolean;
40
+ role: RoleName;
41
+ scope: ScopeLevel;
42
+ reason: string;
43
+ }
44
+ export interface AuditLogEntry {
45
+ id: string;
46
+ timestamp: string;
47
+ userId: string;
48
+ tenantId: string;
49
+ resource: Resource;
50
+ action: Action;
51
+ allowed: boolean;
52
+ role: RoleName;
53
+ scope: ScopeLevel;
54
+ reason: string;
55
+ metadata?: Record<string, unknown>;
56
+ }
57
+ export interface JWTPayload {
58
+ sub: string;
59
+ tenant_id: string;
60
+ role: RoleName;
61
+ scope: ScopeLevel;
62
+ scope_id?: string;
63
+ permissions: PermissionString[];
64
+ iat: number;
65
+ exp: number;
66
+ }
67
+ export interface RBACConfig {
68
+ jwtSecret: string;
69
+ jwtExpiresIn?: number;
70
+ auditLogLimit?: number;
71
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@grc-claw/rbac-multi-tenant",
3
+ "version": "0.8.0",
4
+ "description": "RBAC engine with multi-tenant isolation for A2Z SOC platform",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "test": "node --import tsx --test src/**/*.test.ts"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "devDependencies": {
23
+ "typescript": "^5.7.0",
24
+ "tsx": "^4.19.0"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/AAH20/GRC_Claw"
32
+ }
33
+ }