tlc-claude-code 1.3.0 → 1.4.1
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/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
- package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- package/server/lib/zero-retention.test.js +258 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { ZeroRetentionPane } from './ZeroRetentionPane.js';
|
|
5
|
+
describe('ZeroRetentionPane', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
describe('enabled state', () => {
|
|
13
|
+
it('renders enabled state correctly', () => {
|
|
14
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true }));
|
|
15
|
+
const output = lastFrame();
|
|
16
|
+
expect(output).toContain('Zero-Retention');
|
|
17
|
+
expect(output).toMatch(/enabled|active|on/i);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('disabled state', () => {
|
|
21
|
+
it('renders disabled state correctly', () => {
|
|
22
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: false }));
|
|
23
|
+
const output = lastFrame();
|
|
24
|
+
expect(output).toContain('Zero-Retention');
|
|
25
|
+
expect(output).toMatch(/disabled|inactive|off/i);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('toggle callback', () => {
|
|
29
|
+
it('toggle calls onToggle callback', () => {
|
|
30
|
+
const onToggle = vi.fn();
|
|
31
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: false, isActive: true, onToggle: onToggle }));
|
|
32
|
+
// Verify toggle control is shown when active and onToggle is provided
|
|
33
|
+
const output = lastFrame();
|
|
34
|
+
expect(output).toMatch(/\[t\]|toggle/i);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('retention policy summary', () => {
|
|
38
|
+
it('shows retention policy summary', () => {
|
|
39
|
+
const policy = {
|
|
40
|
+
retention: 'immediate',
|
|
41
|
+
persist: false,
|
|
42
|
+
sensitivityLevels: {
|
|
43
|
+
critical: { retention: 'immediate', persist: false },
|
|
44
|
+
high: { retention: 'immediate', persist: false },
|
|
45
|
+
medium: { retention: 'session', persist: false },
|
|
46
|
+
low: { retention: '24h', persist: true },
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, policy: policy }));
|
|
50
|
+
const output = lastFrame();
|
|
51
|
+
expect(output).toContain('Policy');
|
|
52
|
+
expect(output).toMatch(/immediate|session|purge/i);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('purge activity list', () => {
|
|
56
|
+
it('shows purge activity list', () => {
|
|
57
|
+
const purgeHistory = [
|
|
58
|
+
{
|
|
59
|
+
id: '1',
|
|
60
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
61
|
+
itemCount: 5,
|
|
62
|
+
dataTypes: ['secrets', 'pii'],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: '2',
|
|
66
|
+
timestamp: '2024-01-15T10:35:00.000Z',
|
|
67
|
+
itemCount: 3,
|
|
68
|
+
dataTypes: ['general'],
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, purgeHistory: purgeHistory }));
|
|
72
|
+
const output = lastFrame();
|
|
73
|
+
expect(output).toContain('Purge');
|
|
74
|
+
expect(output).toContain('5');
|
|
75
|
+
expect(output).toContain('3');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('sensitive data warning', () => {
|
|
79
|
+
it('shows warning for sensitive data', () => {
|
|
80
|
+
const sensitiveDataDetected = {
|
|
81
|
+
detected: true,
|
|
82
|
+
count: 3,
|
|
83
|
+
types: ['api_key', 'password', 'pii'],
|
|
84
|
+
};
|
|
85
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, sensitiveDataDetected: sensitiveDataDetected }));
|
|
86
|
+
const output = lastFrame();
|
|
87
|
+
expect(output).toMatch(/warning|sensitive|detected/i);
|
|
88
|
+
expect(output).toContain('3');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('empty purge history', () => {
|
|
92
|
+
it('handles empty purge history', () => {
|
|
93
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, purgeHistory: [] }));
|
|
94
|
+
const output = lastFrame();
|
|
95
|
+
expect(output).toContain('Zero-Retention');
|
|
96
|
+
expect(output).toMatch(/no.*purge|empty|none/i);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('policy formatting', () => {
|
|
100
|
+
it('formats policy for display', () => {
|
|
101
|
+
const policy = {
|
|
102
|
+
retention: 'immediate',
|
|
103
|
+
persist: false,
|
|
104
|
+
dataTypes: {
|
|
105
|
+
secrets: { retention: 'immediate', persist: false },
|
|
106
|
+
pii: { retention: 'session', persist: false },
|
|
107
|
+
general: { retention: '7d', persist: true },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, policy: policy }));
|
|
111
|
+
const output = lastFrame();
|
|
112
|
+
// Should display policy rules in a readable format
|
|
113
|
+
expect(output).toContain('Policy');
|
|
114
|
+
expect(output).toBeDefined();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('subsystem status', () => {
|
|
118
|
+
it('shows subsystem status when provided', () => {
|
|
119
|
+
const subsystems = {
|
|
120
|
+
ephemeralStorage: true,
|
|
121
|
+
sessionPurge: true,
|
|
122
|
+
memoryExclusion: true,
|
|
123
|
+
};
|
|
124
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, subsystems: subsystems }));
|
|
125
|
+
const output = lastFrame();
|
|
126
|
+
expect(output).toMatch(/ephemeral|storage/i);
|
|
127
|
+
expect(output).toMatch(/session|purge/i);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('configuration validation', () => {
|
|
131
|
+
it('shows validation warnings when present', () => {
|
|
132
|
+
const validation = {
|
|
133
|
+
valid: false,
|
|
134
|
+
conflicts: ['Ephemeral storage has basePath set'],
|
|
135
|
+
warnings: ['Audit logging conflicts with zero-retention'],
|
|
136
|
+
};
|
|
137
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, validation: validation }));
|
|
138
|
+
const output = lastFrame();
|
|
139
|
+
expect(output).toMatch(/conflict|warning/i);
|
|
140
|
+
});
|
|
141
|
+
it('shows valid status when no conflicts', () => {
|
|
142
|
+
const validation = {
|
|
143
|
+
valid: true,
|
|
144
|
+
conflicts: [],
|
|
145
|
+
warnings: [],
|
|
146
|
+
};
|
|
147
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: true, validation: validation }));
|
|
148
|
+
const output = lastFrame();
|
|
149
|
+
expect(output).toMatch(/valid|ok|configured/i);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('keyboard controls', () => {
|
|
153
|
+
it('shows controls when active', () => {
|
|
154
|
+
const { lastFrame } = render(_jsx(ZeroRetentionPane, { enabled: false, isActive: true }));
|
|
155
|
+
const output = lastFrame();
|
|
156
|
+
// Should show control hints
|
|
157
|
+
expect(output).toMatch(/\[t\]|toggle|enable|disable/i);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access Control Documenter
|
|
3
|
+
* Documents who has access to what for compliance and auditing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// TLC roles and permissions (from auth-system.js)
|
|
7
|
+
const USER_ROLES = {
|
|
8
|
+
ADMIN: 'admin',
|
|
9
|
+
ENGINEER: 'engineer',
|
|
10
|
+
QA: 'qa',
|
|
11
|
+
PO: 'po',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const ROLE_PERMISSIONS = {
|
|
15
|
+
[USER_ROLES.ADMIN]: ['*'],
|
|
16
|
+
[USER_ROLES.ENGINEER]: ['read', 'write', 'deploy', 'claim', 'release'],
|
|
17
|
+
[USER_ROLES.QA]: ['read', 'verify', 'bug', 'test'],
|
|
18
|
+
[USER_ROLES.PO]: ['read', 'plan', 'verify', 'approve'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const ROLE_DESCRIPTIONS = {
|
|
22
|
+
[USER_ROLES.ADMIN]: 'Full system administrator with all permissions',
|
|
23
|
+
[USER_ROLES.ENGINEER]: 'Developer with read/write, deploy, and task management',
|
|
24
|
+
[USER_ROLES.QA]: 'Quality assurance with testing and verification access',
|
|
25
|
+
[USER_ROLES.PO]: 'Product owner with planning and approval permissions',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// All possible permissions (for expansion and orphan detection)
|
|
29
|
+
const ALL_PERMISSIONS = [
|
|
30
|
+
'read',
|
|
31
|
+
'write',
|
|
32
|
+
'deploy',
|
|
33
|
+
'claim',
|
|
34
|
+
'release',
|
|
35
|
+
'verify',
|
|
36
|
+
'bug',
|
|
37
|
+
'test',
|
|
38
|
+
'plan',
|
|
39
|
+
'approve',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Role priority for sorting (admin first, then alphabetical)
|
|
43
|
+
const ROLE_PRIORITY = {
|
|
44
|
+
[USER_ROLES.ADMIN]: 0,
|
|
45
|
+
[USER_ROLES.ENGINEER]: 1,
|
|
46
|
+
[USER_ROLES.PO]: 2,
|
|
47
|
+
[USER_ROLES.QA]: 3,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* List all users with their roles
|
|
52
|
+
* @param {Object[]} users - Array of user objects
|
|
53
|
+
* @returns {Object[]} Sanitized users with roles, sorted by role then name
|
|
54
|
+
*/
|
|
55
|
+
function listUsers(users) {
|
|
56
|
+
if (!users || !Array.isArray(users) || users.length === 0) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Sanitize and extract relevant fields
|
|
61
|
+
const sanitized = users.map((user) => ({
|
|
62
|
+
id: user.id,
|
|
63
|
+
email: user.email,
|
|
64
|
+
name: user.name,
|
|
65
|
+
role: user.role,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
// Sort by role priority, then by name
|
|
69
|
+
sanitized.sort((a, b) => {
|
|
70
|
+
const rolePriorityA = ROLE_PRIORITY[a.role] ?? 999;
|
|
71
|
+
const rolePriorityB = ROLE_PRIORITY[b.role] ?? 999;
|
|
72
|
+
|
|
73
|
+
if (rolePriorityA !== rolePriorityB) {
|
|
74
|
+
return rolePriorityA - rolePriorityB;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (a.name || '').localeCompare(b.name || '');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return sanitized;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List all roles with their permissions
|
|
85
|
+
* @returns {Object} Roles mapped to their permissions and descriptions
|
|
86
|
+
*/
|
|
87
|
+
function listRoles() {
|
|
88
|
+
const result = {};
|
|
89
|
+
|
|
90
|
+
for (const [role, permissions] of Object.entries(ROLE_PERMISSIONS)) {
|
|
91
|
+
result[role] = {
|
|
92
|
+
permissions: [...permissions],
|
|
93
|
+
description: ROLE_DESCRIPTIONS[role] || '',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get permissions for a specific role
|
|
102
|
+
* @param {string} role - Role name
|
|
103
|
+
* @param {Object} options - Options
|
|
104
|
+
* @param {boolean} options.expand - Expand wildcard to all permissions
|
|
105
|
+
* @returns {string[]} Array of permissions
|
|
106
|
+
*/
|
|
107
|
+
function getRolePermissions(role, options = {}) {
|
|
108
|
+
if (!role) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const permissions = ROLE_PERMISSIONS[role];
|
|
113
|
+
|
|
114
|
+
if (!permissions) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Expand wildcard if requested
|
|
119
|
+
if (options.expand && permissions.includes('*')) {
|
|
120
|
+
return [...ALL_PERMISSIONS];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return [...permissions];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get SSO role mappings from config
|
|
128
|
+
* @param {Object} config - TLC config object
|
|
129
|
+
* @returns {Object} SSO mappings and default role
|
|
130
|
+
*/
|
|
131
|
+
function getSSOMapping(config) {
|
|
132
|
+
if (!config || !config.sso || !config.sso.roleMappings) {
|
|
133
|
+
return {
|
|
134
|
+
mappings: [],
|
|
135
|
+
defaultRole: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Sort mappings by priority
|
|
140
|
+
const sortedMappings = [...config.sso.roleMappings].sort(
|
|
141
|
+
(a, b) => a.priority - b.priority
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
mappings: sortedMappings,
|
|
146
|
+
defaultRole: config.sso.defaultRole || null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if user has a specific permission
|
|
152
|
+
* @param {Object} user - User object
|
|
153
|
+
* @param {string} permission - Permission to check
|
|
154
|
+
* @returns {boolean} Whether user has the permission
|
|
155
|
+
*/
|
|
156
|
+
function hasPermission(user, permission) {
|
|
157
|
+
if (!user || !user.role) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const permissions = ROLE_PERMISSIONS[user.role] || [];
|
|
162
|
+
|
|
163
|
+
return permissions.includes('*') || permissions.includes(permission);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Generate access matrix showing user/permission relationships
|
|
168
|
+
* @param {Object[]} users - Array of user objects
|
|
169
|
+
* @returns {Object} Access matrix with users, permissions, and matrix
|
|
170
|
+
*/
|
|
171
|
+
function getAccessMatrix(users) {
|
|
172
|
+
if (!users || !Array.isArray(users)) {
|
|
173
|
+
users = [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const userEmails = users.map((u) => u.email);
|
|
177
|
+
const matrix = {};
|
|
178
|
+
|
|
179
|
+
for (const user of users) {
|
|
180
|
+
matrix[user.email] = {};
|
|
181
|
+
|
|
182
|
+
for (const permission of ALL_PERMISSIONS) {
|
|
183
|
+
matrix[user.email][permission] = hasPermission(user, permission);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
users: userEmails,
|
|
189
|
+
permissions: ALL_PERMISSIONS,
|
|
190
|
+
matrix,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Sequence counter for stable ordering within same timestamp
|
|
195
|
+
let sequenceCounter = 0;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Track a permission change
|
|
199
|
+
* @param {Object} store - Permission store
|
|
200
|
+
* @param {Object} change - Change details
|
|
201
|
+
* @returns {Object} Recorded change with ID and timestamp
|
|
202
|
+
*/
|
|
203
|
+
function trackPermissionChange(store, change) {
|
|
204
|
+
sequenceCounter++;
|
|
205
|
+
|
|
206
|
+
const record = {
|
|
207
|
+
id: generateId(),
|
|
208
|
+
timestamp: new Date().toISOString(),
|
|
209
|
+
sequence: sequenceCounter,
|
|
210
|
+
userId: change.userId,
|
|
211
|
+
oldRole: change.oldRole,
|
|
212
|
+
newRole: change.newRole,
|
|
213
|
+
changedBy: change.changedBy,
|
|
214
|
+
reason: change.reason || null,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
store.add(record);
|
|
218
|
+
|
|
219
|
+
return record;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get permission change history
|
|
224
|
+
* @param {Object} store - Permission store
|
|
225
|
+
* @param {Object} filters - Filter options
|
|
226
|
+
* @returns {Object[]} Array of permission changes
|
|
227
|
+
*/
|
|
228
|
+
function getPermissionHistory(store, filters = {}) {
|
|
229
|
+
let history = store.getHistory();
|
|
230
|
+
|
|
231
|
+
// Filter by userId
|
|
232
|
+
if (filters.userId) {
|
|
233
|
+
history = history.filter((h) => h.userId === filters.userId);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Filter by date range
|
|
237
|
+
if (filters.from) {
|
|
238
|
+
const fromDate = new Date(filters.from);
|
|
239
|
+
history = history.filter((h) => new Date(h.timestamp) >= fromDate);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (filters.to) {
|
|
243
|
+
const toDate = new Date(filters.to);
|
|
244
|
+
history = history.filter((h) => new Date(h.timestamp) <= toDate);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Sort by timestamp descending (most recent first), then by sequence for stable ordering
|
|
248
|
+
history.sort((a, b) => {
|
|
249
|
+
const timeDiff = new Date(b.timestamp) - new Date(a.timestamp);
|
|
250
|
+
if (timeDiff !== 0) return timeDiff;
|
|
251
|
+
return (b.sequence || 0) - (a.sequence || 0);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return history;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Export access control data as compliance evidence
|
|
259
|
+
* @param {Object[]} users - Array of user objects
|
|
260
|
+
* @param {Object} config - TLC config
|
|
261
|
+
* @param {Object} options - Export options
|
|
262
|
+
* @returns {Object|string} Evidence in requested format
|
|
263
|
+
*/
|
|
264
|
+
function exportAsEvidence(users, config, options = {}) {
|
|
265
|
+
const { format = 'json', permissionStore, exportedBy = 'system' } = options;
|
|
266
|
+
|
|
267
|
+
const evidence = {
|
|
268
|
+
version: '1.0',
|
|
269
|
+
exportDate: new Date().toISOString(),
|
|
270
|
+
exportedBy,
|
|
271
|
+
users: listUsers(users || []),
|
|
272
|
+
roles: listRoles(),
|
|
273
|
+
ssoMappings: getSSOMapping(config || {}),
|
|
274
|
+
accessMatrix: getAccessMatrix(users || []),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Include permission history if store provided
|
|
278
|
+
if (permissionStore) {
|
|
279
|
+
evidence.permissionHistory = getPermissionHistory(permissionStore);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (format === 'csv') {
|
|
283
|
+
return formatAsCSV(evidence);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return evidence;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Format evidence as CSV
|
|
291
|
+
* @param {Object} evidence - Evidence object
|
|
292
|
+
* @returns {string} CSV formatted string
|
|
293
|
+
*/
|
|
294
|
+
function formatAsCSV(evidence) {
|
|
295
|
+
const lines = [];
|
|
296
|
+
|
|
297
|
+
// Header
|
|
298
|
+
lines.push('email,name,role,' + ALL_PERMISSIONS.join(','));
|
|
299
|
+
|
|
300
|
+
// User rows
|
|
301
|
+
for (const user of evidence.users) {
|
|
302
|
+
const matrix = evidence.accessMatrix.matrix[user.email] || {};
|
|
303
|
+
const permissions = ALL_PERMISSIONS.map((p) => (matrix[p] ? 'Y' : 'N'));
|
|
304
|
+
lines.push(`${user.email},${user.name},${user.role},${permissions.join(',')}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return lines.join('\n');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Format access report for human reading
|
|
312
|
+
* @param {Object[]} users - Array of user objects
|
|
313
|
+
* @param {Object} options - Formatting options
|
|
314
|
+
* @returns {string} Formatted report
|
|
315
|
+
*/
|
|
316
|
+
function formatAccessReport(users, options = {}) {
|
|
317
|
+
const { includeMatrix = false, config = null } = options;
|
|
318
|
+
|
|
319
|
+
const lines = [];
|
|
320
|
+
const userList = listUsers(users || []);
|
|
321
|
+
|
|
322
|
+
// Header
|
|
323
|
+
lines.push('# Access Control Report');
|
|
324
|
+
lines.push(`Generated: ${new Date().toISOString()}`);
|
|
325
|
+
lines.push('');
|
|
326
|
+
|
|
327
|
+
// User list
|
|
328
|
+
lines.push('## Users');
|
|
329
|
+
lines.push('');
|
|
330
|
+
|
|
331
|
+
if (userList.length === 0) {
|
|
332
|
+
lines.push('No users configured.');
|
|
333
|
+
} else {
|
|
334
|
+
lines.push('| Email | Name | Role |');
|
|
335
|
+
lines.push('|-------|------|------|');
|
|
336
|
+
|
|
337
|
+
for (const user of userList) {
|
|
338
|
+
lines.push(`| ${user.email} | ${user.name} | ${user.role} |`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
lines.push('');
|
|
342
|
+
|
|
343
|
+
// Role summary
|
|
344
|
+
lines.push('## Role Summary');
|
|
345
|
+
lines.push('');
|
|
346
|
+
|
|
347
|
+
const roleCounts = {};
|
|
348
|
+
for (const user of userList) {
|
|
349
|
+
roleCounts[user.role] = (roleCounts[user.role] || 0) + 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
for (const [role, count] of Object.entries(roleCounts)) {
|
|
353
|
+
lines.push(`- ${role}: ${count}`);
|
|
354
|
+
}
|
|
355
|
+
lines.push('');
|
|
356
|
+
|
|
357
|
+
// Roles and permissions
|
|
358
|
+
lines.push('## Role Permissions');
|
|
359
|
+
lines.push('');
|
|
360
|
+
|
|
361
|
+
const roles = listRoles();
|
|
362
|
+
for (const [role, info] of Object.entries(roles)) {
|
|
363
|
+
lines.push(`### ${role}`);
|
|
364
|
+
lines.push(`${info.description}`);
|
|
365
|
+
lines.push(`Permissions: ${info.permissions.join(', ')}`);
|
|
366
|
+
lines.push('');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Permission matrix
|
|
370
|
+
if (includeMatrix && userList.length > 0) {
|
|
371
|
+
lines.push('## Permission Matrix');
|
|
372
|
+
lines.push('');
|
|
373
|
+
|
|
374
|
+
const matrix = getAccessMatrix(userList);
|
|
375
|
+
const header = ['Email', ...ALL_PERMISSIONS].join(' | ');
|
|
376
|
+
lines.push(`| ${header} |`);
|
|
377
|
+
lines.push('|' + '------|'.repeat(ALL_PERMISSIONS.length + 1));
|
|
378
|
+
|
|
379
|
+
for (const email of matrix.users) {
|
|
380
|
+
const perms = ALL_PERMISSIONS.map((p) =>
|
|
381
|
+
matrix.matrix[email][p] ? 'Y' : '-'
|
|
382
|
+
);
|
|
383
|
+
lines.push(`| ${email} | ${perms.join(' | ')} |`);
|
|
384
|
+
}
|
|
385
|
+
lines.push('');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// SSO mappings
|
|
389
|
+
if (config && config.sso && config.sso.roleMappings) {
|
|
390
|
+
lines.push('## SSO Role Mappings');
|
|
391
|
+
lines.push('');
|
|
392
|
+
|
|
393
|
+
const ssoMapping = getSSOMapping(config);
|
|
394
|
+
|
|
395
|
+
if (ssoMapping.mappings.length === 0) {
|
|
396
|
+
lines.push('No SSO role mappings configured.');
|
|
397
|
+
} else {
|
|
398
|
+
lines.push('| Pattern | Role | Priority |');
|
|
399
|
+
lines.push('|---------|------|----------|');
|
|
400
|
+
|
|
401
|
+
for (const mapping of ssoMapping.mappings) {
|
|
402
|
+
lines.push(
|
|
403
|
+
`| \`${mapping.pattern}\` | ${mapping.role} | ${mapping.priority} |`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (ssoMapping.defaultRole) {
|
|
408
|
+
lines.push('');
|
|
409
|
+
lines.push(`Default role: ${ssoMapping.defaultRole}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
lines.push('');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return lines.join('\n');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Detect permissions not assigned to any user
|
|
420
|
+
* @param {Object[]} users - Array of user objects
|
|
421
|
+
* @returns {string[]} Array of orphaned permissions
|
|
422
|
+
*/
|
|
423
|
+
function detectOrphanedPermissions(users) {
|
|
424
|
+
if (!users || !Array.isArray(users) || users.length === 0) {
|
|
425
|
+
return [...ALL_PERMISSIONS];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const usedPermissions = new Set();
|
|
429
|
+
|
|
430
|
+
for (const user of users) {
|
|
431
|
+
const permissions = ROLE_PERMISSIONS[user.role] || [];
|
|
432
|
+
|
|
433
|
+
// Wildcard means all permissions are used
|
|
434
|
+
if (permissions.includes('*')) {
|
|
435
|
+
return [];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const perm of permissions) {
|
|
439
|
+
usedPermissions.add(perm);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return ALL_PERMISSIONS.filter((p) => !usedPermissions.has(p));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Generate a unique ID
|
|
448
|
+
* @returns {string} Unique identifier
|
|
449
|
+
*/
|
|
450
|
+
function generateId() {
|
|
451
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Create an access control documenter instance
|
|
456
|
+
* @param {Object} options - Configuration options
|
|
457
|
+
* @returns {Object} Documenter instance
|
|
458
|
+
*/
|
|
459
|
+
function createAccessControlDoc(options = {}) {
|
|
460
|
+
const { config = {} } = options;
|
|
461
|
+
const permissionStore = createInternalPermissionStore();
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
listUsers(users) {
|
|
465
|
+
return listUsers(users);
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
listRoles() {
|
|
469
|
+
return listRoles();
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
getRolePermissions(role, opts) {
|
|
473
|
+
return getRolePermissions(role, opts);
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
getSSOMapping() {
|
|
477
|
+
return getSSOMapping(config);
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
getAccessMatrix(users) {
|
|
481
|
+
return getAccessMatrix(users);
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
trackPermissionChange(change) {
|
|
485
|
+
return trackPermissionChange(permissionStore, change);
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
getPermissionHistory(filters) {
|
|
489
|
+
return getPermissionHistory(permissionStore, filters);
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
exportAsEvidence(users, opts = {}) {
|
|
493
|
+
return exportAsEvidence(users, config, {
|
|
494
|
+
...opts,
|
|
495
|
+
permissionStore,
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
|
|
499
|
+
formatAccessReport(users, opts) {
|
|
500
|
+
return formatAccessReport(users, { ...opts, config });
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
detectOrphanedPermissions(users) {
|
|
504
|
+
return detectOrphanedPermissions(users);
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Create internal permission store for tracking changes
|
|
511
|
+
* @returns {Object} Permission store
|
|
512
|
+
*/
|
|
513
|
+
function createInternalPermissionStore() {
|
|
514
|
+
const changes = [];
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
add(change) {
|
|
518
|
+
changes.push(change);
|
|
519
|
+
},
|
|
520
|
+
getHistory() {
|
|
521
|
+
return [...changes];
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export {
|
|
527
|
+
createAccessControlDoc,
|
|
528
|
+
listUsers,
|
|
529
|
+
listRoles,
|
|
530
|
+
getRolePermissions,
|
|
531
|
+
getSSOMapping,
|
|
532
|
+
getAccessMatrix,
|
|
533
|
+
trackPermissionChange,
|
|
534
|
+
getPermissionHistory,
|
|
535
|
+
exportAsEvidence,
|
|
536
|
+
formatAccessReport,
|
|
537
|
+
detectOrphanedPermissions,
|
|
538
|
+
USER_ROLES,
|
|
539
|
+
ROLE_PERMISSIONS,
|
|
540
|
+
ALL_PERMISSIONS,
|
|
541
|
+
};
|