@odavl/guardian 2.0.0 → 2.0.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/CHANGELOG.md +210 -210
- package/LICENSE +21 -21
- package/README.md +297 -184
- package/bin/guardian.js +2242 -2221
- package/config/README.md +59 -59
- package/config/guardian.config.json +54 -54
- package/config/guardian.policy.json +12 -12
- package/config/profiles/docs.yaml +18 -18
- package/config/profiles/ecommerce.yaml +17 -17
- package/config/profiles/landing-demo.yaml +16 -16
- package/config/profiles/marketing.yaml +18 -18
- package/config/profiles/saas.yaml +21 -21
- package/flows/example-login-flow.json +36 -36
- package/flows/example-signup-flow.json +44 -44
- package/package.json +124 -116
- package/policies/enterprise.json +12 -12
- package/policies/landing-demo.json +22 -22
- package/policies/saas.json +12 -12
- package/policies/startup.json +12 -12
- package/src/enterprise/audit-logger.js +166 -166
- package/src/enterprise/pdf-exporter.js +267 -267
- package/src/enterprise/rbac-gate.js +142 -142
- package/src/enterprise/rbac.js +239 -239
- package/src/enterprise/site-manager.js +180 -180
- package/src/founder/feedback-system.js +156 -156
- package/src/founder/founder-tracker.js +213 -213
- package/src/founder/usage-signals.js +141 -141
- package/src/guardian/action-hints.js +439 -439
- package/src/guardian/alert-ledger.js +121 -121
- package/src/guardian/artifact-sanitizer.js +56 -56
- package/src/guardian/attempt-engine.js +1069 -1029
- package/src/guardian/attempt-registry.js +267 -267
- package/src/guardian/attempt-relevance.js +106 -106
- package/src/guardian/attempt-reporter.js +513 -507
- package/src/guardian/attempt.js +274 -273
- package/src/guardian/attempts-filter.js +63 -63
- package/src/guardian/auto-attempt-builder.js +283 -283
- package/src/guardian/baseline-registry.js +177 -177
- package/src/guardian/baseline-reporter.js +143 -143
- package/src/guardian/baseline-storage.js +285 -285
- package/src/guardian/baseline.js +535 -534
- package/src/guardian/behavioral-signals.js +261 -261
- package/src/guardian/breakage-intelligence.js +224 -224
- package/src/guardian/browser-pool.js +131 -131
- package/src/guardian/browser.js +119 -119
- package/src/guardian/canonical-truth.js +308 -308
- package/src/guardian/ci-cli.js +121 -121
- package/src/guardian/ci-gate.js +96 -96
- package/src/guardian/ci-mode.js +15 -15
- package/src/guardian/ci-output.js +55 -38
- package/src/guardian/cli-summary.js +102 -102
- package/src/guardian/confidence-signals.js +251 -251
- package/src/guardian/config-loader.js +161 -161
- package/src/guardian/config-validator.js +285 -283
- package/src/guardian/coverage-model.js +239 -239
- package/src/guardian/coverage-packs.js +58 -58
- package/src/guardian/crawler.js +142 -142
- package/src/guardian/data-guardian-detector.js +189 -189
- package/src/guardian/decision-authority.js +746 -725
- package/src/guardian/detection-layers.js +271 -271
- package/src/guardian/determinism.js +146 -146
- package/src/guardian/discovery-engine.js +661 -661
- package/src/guardian/drift-detector.js +100 -100
- package/src/guardian/enhanced-html-reporter.js +522 -522
- package/src/guardian/env-guard.js +128 -127
- package/src/guardian/error-clarity.js +399 -399
- package/src/guardian/export-contract.js +196 -196
- package/src/guardian/fail-safe.js +212 -212
- package/src/guardian/failure-intelligence.js +173 -173
- package/src/guardian/failure-taxonomy.js +169 -169
- package/src/guardian/final-outcome.js +206 -206
- package/src/guardian/first-run-profile.js +89 -89
- package/src/guardian/first-run.js +65 -67
- package/src/guardian/flag-validator.js +111 -111
- package/src/guardian/flow-executor.js +641 -639
- package/src/guardian/flow-registry.js +67 -67
- package/src/guardian/honesty.js +394 -394
- package/src/guardian/html-reporter.js +416 -416
- package/src/guardian/human-intent-resolver.js +296 -296
- package/src/guardian/human-interaction-model.js +351 -351
- package/src/guardian/human-journey-context.js +184 -184
- package/src/guardian/human-navigator.js +544 -544
- package/src/guardian/human-reporter.js +435 -431
- package/src/guardian/index.js +226 -221
- package/src/guardian/init-command.js +143 -143
- package/src/guardian/intent-detector.js +148 -146
- package/src/guardian/journey-definitions.js +132 -132
- package/src/guardian/journey-scan-cli.js +142 -145
- package/src/guardian/journey-scanner.js +583 -583
- package/src/guardian/junit-reporter.js +281 -281
- package/src/guardian/language-detection.js +99 -99
- package/src/guardian/live-alert.js +56 -56
- package/src/guardian/live-baseline-compare.js +146 -146
- package/src/guardian/live-cli.js +95 -95
- package/src/guardian/live-guardian.js +210 -210
- package/src/guardian/live-scheduler-runner.js +137 -137
- package/src/guardian/live-scheduler-state.js +167 -168
- package/src/guardian/live-scheduler.js +146 -146
- package/src/guardian/live-state.js +110 -110
- package/src/guardian/market-criticality.js +335 -335
- package/src/guardian/market-reporter.js +577 -577
- package/src/guardian/network-trace.js +178 -178
- package/src/guardian/obs-logger.js +110 -110
- package/src/guardian/observed-capabilities.js +427 -427
- package/src/guardian/output-contract.js +154 -0
- package/src/guardian/output-readability.js +264 -264
- package/src/guardian/parallel-executor.js +116 -116
- package/src/guardian/path-safety.js +56 -56
- package/src/guardian/pattern-analyzer.js +348 -348
- package/src/guardian/policy.js +432 -434
- package/src/guardian/prelaunch-gate.js +193 -193
- package/src/guardian/prerequisite-checker.js +101 -101
- package/src/guardian/preset-loader.js +152 -157
- package/src/guardian/profile-loader.js +96 -96
- package/src/guardian/reality.js +3025 -2826
- package/src/guardian/realworld-scenarios.js +94 -94
- package/src/guardian/reporter.js +167 -167
- package/src/guardian/retry-policy.js +123 -123
- package/src/guardian/root-cause-analysis.js +171 -171
- package/src/guardian/rules-engine.js +558 -558
- package/src/guardian/run-artifacts.js +212 -212
- package/src/guardian/run-cleanup.js +207 -207
- package/src/guardian/run-export.js +522 -522
- package/src/guardian/run-latest.js +90 -90
- package/src/guardian/run-list.js +211 -211
- package/src/guardian/run-summary.js +20 -20
- package/src/guardian/runtime-root.js +246 -246
- package/src/guardian/safety.js +248 -248
- package/src/guardian/scan-presets.js +133 -149
- package/src/guardian/screenshot.js +152 -152
- package/src/guardian/secret-hygiene.js +44 -44
- package/src/guardian/selector-fallbacks.js +394 -394
- package/src/guardian/semantic-contact-detection.js +255 -255
- package/src/guardian/semantic-contact-finder.js +201 -201
- package/src/guardian/semantic-targets.js +234 -234
- package/src/guardian/site-intelligence.js +588 -588
- package/src/guardian/site-introspection.js +257 -257
- package/src/guardian/sitemap.js +225 -225
- package/src/guardian/smoke.js +283 -258
- package/src/guardian/snapshot-schema.js +177 -290
- package/src/guardian/snapshot.js +430 -397
- package/src/guardian/stability-scorer.js +169 -169
- package/src/guardian/success-evaluator.js +214 -214
- package/src/guardian/template-command.js +184 -184
- package/src/guardian/text-formatters.js +426 -426
- package/src/guardian/timeout-profiles.js +57 -57
- package/src/guardian/truth/attempt.contract.js +158 -0
- package/src/guardian/truth/decision.contract.js +275 -0
- package/src/guardian/truth/snapshot.contract.js +363 -0
- package/src/guardian/validators.js +323 -323
- package/src/guardian/verdict-card.js +474 -474
- package/src/guardian/verdict-clarity.js +298 -298
- package/src/guardian/verdict-policy.js +363 -363
- package/src/guardian/verdict.js +333 -333
- package/src/guardian/verdicts.js +79 -74
- package/src/guardian/visual-diff.js +247 -247
- package/src/guardian/wait-for-outcome.js +119 -119
- package/src/guardian/watch-runner.js +181 -181
- package/src/guardian/watchdog-diff.js +167 -167
- package/src/guardian/webhook.js +206 -206
- package/src/payments/stripe-checkout.js +169 -169
- package/src/plans/plan-definitions.js +148 -148
- package/src/plans/plan-manager.js +211 -211
- package/src/plans/usage-tracker.js +210 -210
- package/src/recipes/recipe-engine.js +188 -188
- package/src/recipes/recipe-failure-analysis.js +159 -159
- package/src/recipes/recipe-registry.js +134 -134
- package/src/recipes/recipe-runtime.js +507 -507
- package/src/recipes/recipe-store.js +410 -410
- package/SECURITY.md +0 -77
- package/VERSIONING.md +0 -100
- package/guardian-contract-v1.md +0 -502
package/src/enterprise/rbac.js
CHANGED
|
@@ -1,239 +1,239 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 11: Role-Based Access Control (RBAC)
|
|
3
|
-
* Enforce permission checks for enterprise features
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const os = require('os');
|
|
9
|
-
|
|
10
|
-
const RBAC_DIR = path.join(os.homedir(), '.odavl-guardian', 'rbac');
|
|
11
|
-
const USERS_FILE = path.join(RBAC_DIR, 'users.json');
|
|
12
|
-
|
|
13
|
-
// Role definitions
|
|
14
|
-
const ROLES = {
|
|
15
|
-
ADMIN: {
|
|
16
|
-
name: 'ADMIN',
|
|
17
|
-
permissions: [
|
|
18
|
-
'scan:run',
|
|
19
|
-
'scan:view',
|
|
20
|
-
'live:run',
|
|
21
|
-
'site:add',
|
|
22
|
-
'site:remove',
|
|
23
|
-
'site:view',
|
|
24
|
-
'plan:view',
|
|
25
|
-
'plan:upgrade',
|
|
26
|
-
'user:add',
|
|
27
|
-
'user:remove',
|
|
28
|
-
'user:view',
|
|
29
|
-
'audit:view',
|
|
30
|
-
'export:pdf',
|
|
31
|
-
'recipe:manage',
|
|
32
|
-
],
|
|
33
|
-
},
|
|
34
|
-
OPERATOR: {
|
|
35
|
-
name: 'OPERATOR',
|
|
36
|
-
permissions: [
|
|
37
|
-
'scan:run',
|
|
38
|
-
'scan:view',
|
|
39
|
-
'live:run',
|
|
40
|
-
'site:view',
|
|
41
|
-
'plan:view',
|
|
42
|
-
'export:pdf',
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
VIEWER: {
|
|
46
|
-
name: 'VIEWER',
|
|
47
|
-
permissions: [
|
|
48
|
-
'scan:view',
|
|
49
|
-
'site:view',
|
|
50
|
-
'plan:view',
|
|
51
|
-
'audit:view',
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Ensure RBAC directory exists
|
|
58
|
-
*/
|
|
59
|
-
function ensureRbacDir() {
|
|
60
|
-
if (!fs.existsSync(RBAC_DIR)) {
|
|
61
|
-
fs.mkdirSync(RBAC_DIR, { recursive: true });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Get all users
|
|
67
|
-
*/
|
|
68
|
-
function getUsers() {
|
|
69
|
-
ensureRbacDir();
|
|
70
|
-
|
|
71
|
-
if (!fs.existsSync(USERS_FILE)) {
|
|
72
|
-
// Default: current user is ADMIN
|
|
73
|
-
const defaultUser = {
|
|
74
|
-
username: os.userInfo().username || 'admin',
|
|
75
|
-
role: 'ADMIN',
|
|
76
|
-
addedAt: new Date().toISOString(),
|
|
77
|
-
};
|
|
78
|
-
return { users: [defaultUser] };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
return JSON.parse(fs.readFileSync(USERS_FILE, 'utf-8'));
|
|
83
|
-
} catch (
|
|
84
|
-
const defaultUser = {
|
|
85
|
-
username: os.userInfo().username || 'admin',
|
|
86
|
-
role: 'ADMIN',
|
|
87
|
-
addedAt: new Date().toISOString(),
|
|
88
|
-
};
|
|
89
|
-
return { users: [defaultUser] };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Save users
|
|
95
|
-
*/
|
|
96
|
-
function saveUsers(data) {
|
|
97
|
-
ensureRbacDir();
|
|
98
|
-
fs.writeFileSync(USERS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Add a user
|
|
103
|
-
*/
|
|
104
|
-
function addUser(username, role = 'VIEWER') {
|
|
105
|
-
if (!ROLES[role]) {
|
|
106
|
-
throw new Error(`Invalid role: ${role}. Must be ADMIN, OPERATOR, or VIEWER`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const data = getUsers();
|
|
110
|
-
|
|
111
|
-
// Check if user already exists
|
|
112
|
-
const existing = data.users.find(u => u.username === username);
|
|
113
|
-
if (existing) {
|
|
114
|
-
throw new Error(`User '${username}' already exists`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const user = {
|
|
118
|
-
username,
|
|
119
|
-
role,
|
|
120
|
-
addedAt: new Date().toISOString(),
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
data.users.push(user);
|
|
124
|
-
saveUsers(data);
|
|
125
|
-
|
|
126
|
-
return user;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Remove a user
|
|
131
|
-
*/
|
|
132
|
-
function removeUser(username) {
|
|
133
|
-
const data = getUsers();
|
|
134
|
-
|
|
135
|
-
const index = data.users.findIndex(u => u.username === username);
|
|
136
|
-
if (index === -1) {
|
|
137
|
-
throw new Error(`User '${username}' not found`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Prevent removing last ADMIN
|
|
141
|
-
const user = data.users[index];
|
|
142
|
-
if (user.role === 'ADMIN') {
|
|
143
|
-
const adminCount = data.users.filter(u => u.role === 'ADMIN').length;
|
|
144
|
-
if (adminCount === 1) {
|
|
145
|
-
throw new Error('Cannot remove last ADMIN user');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
data.users.splice(index, 1);
|
|
150
|
-
saveUsers(data);
|
|
151
|
-
|
|
152
|
-
return user;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Get current user
|
|
157
|
-
*/
|
|
158
|
-
function getCurrentUser() {
|
|
159
|
-
const currentUsername = os.userInfo().username || 'admin';
|
|
160
|
-
const data = getUsers();
|
|
161
|
-
|
|
162
|
-
let user = data.users.find(u => u.username === currentUsername);
|
|
163
|
-
|
|
164
|
-
// If user doesn't exist, create as ADMIN
|
|
165
|
-
if (!user) {
|
|
166
|
-
user = {
|
|
167
|
-
username: currentUsername,
|
|
168
|
-
role: 'ADMIN',
|
|
169
|
-
addedAt: new Date().toISOString(),
|
|
170
|
-
};
|
|
171
|
-
data.users.push(user);
|
|
172
|
-
saveUsers(data);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return user;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Check if user has permission
|
|
180
|
-
*/
|
|
181
|
-
function hasPermission(permission) {
|
|
182
|
-
const user = getCurrentUser();
|
|
183
|
-
const role = ROLES[user.role];
|
|
184
|
-
|
|
185
|
-
if (!role) {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return role.permissions.includes(permission);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Require permission (throws if denied)
|
|
194
|
-
*/
|
|
195
|
-
function requirePermission(permission, action = null) {
|
|
196
|
-
if (!hasPermission(permission)) {
|
|
197
|
-
const user = getCurrentUser();
|
|
198
|
-
const actionMsg = action ? ` to ${action}` : '';
|
|
199
|
-
throw new Error(
|
|
200
|
-
`Permission denied${actionMsg}. Required: ${permission}. Your role: ${user.role}`
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Get role details
|
|
207
|
-
*/
|
|
208
|
-
function getRole(roleName) {
|
|
209
|
-
return ROLES[roleName];
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* List all roles
|
|
214
|
-
*/
|
|
215
|
-
function listRoles() {
|
|
216
|
-
return Object.values(ROLES);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Reset users (for testing)
|
|
221
|
-
*/
|
|
222
|
-
function resetUsers() {
|
|
223
|
-
if (fs.existsSync(USERS_FILE)) {
|
|
224
|
-
fs.unlinkSync(USERS_FILE);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
module.exports = {
|
|
229
|
-
ROLES,
|
|
230
|
-
addUser,
|
|
231
|
-
removeUser,
|
|
232
|
-
getUsers,
|
|
233
|
-
getCurrentUser,
|
|
234
|
-
hasPermission,
|
|
235
|
-
requirePermission,
|
|
236
|
-
getRole,
|
|
237
|
-
listRoles,
|
|
238
|
-
resetUsers,
|
|
239
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Phase 11: Role-Based Access Control (RBAC)
|
|
3
|
+
* Enforce permission checks for enterprise features
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const RBAC_DIR = path.join(os.homedir(), '.odavl-guardian', 'rbac');
|
|
11
|
+
const USERS_FILE = path.join(RBAC_DIR, 'users.json');
|
|
12
|
+
|
|
13
|
+
// Role definitions
|
|
14
|
+
const ROLES = {
|
|
15
|
+
ADMIN: {
|
|
16
|
+
name: 'ADMIN',
|
|
17
|
+
permissions: [
|
|
18
|
+
'scan:run',
|
|
19
|
+
'scan:view',
|
|
20
|
+
'live:run',
|
|
21
|
+
'site:add',
|
|
22
|
+
'site:remove',
|
|
23
|
+
'site:view',
|
|
24
|
+
'plan:view',
|
|
25
|
+
'plan:upgrade',
|
|
26
|
+
'user:add',
|
|
27
|
+
'user:remove',
|
|
28
|
+
'user:view',
|
|
29
|
+
'audit:view',
|
|
30
|
+
'export:pdf',
|
|
31
|
+
'recipe:manage',
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
OPERATOR: {
|
|
35
|
+
name: 'OPERATOR',
|
|
36
|
+
permissions: [
|
|
37
|
+
'scan:run',
|
|
38
|
+
'scan:view',
|
|
39
|
+
'live:run',
|
|
40
|
+
'site:view',
|
|
41
|
+
'plan:view',
|
|
42
|
+
'export:pdf',
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
VIEWER: {
|
|
46
|
+
name: 'VIEWER',
|
|
47
|
+
permissions: [
|
|
48
|
+
'scan:view',
|
|
49
|
+
'site:view',
|
|
50
|
+
'plan:view',
|
|
51
|
+
'audit:view',
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Ensure RBAC directory exists
|
|
58
|
+
*/
|
|
59
|
+
function ensureRbacDir() {
|
|
60
|
+
if (!fs.existsSync(RBAC_DIR)) {
|
|
61
|
+
fs.mkdirSync(RBAC_DIR, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get all users
|
|
67
|
+
*/
|
|
68
|
+
function getUsers() {
|
|
69
|
+
ensureRbacDir();
|
|
70
|
+
|
|
71
|
+
if (!fs.existsSync(USERS_FILE)) {
|
|
72
|
+
// Default: current user is ADMIN
|
|
73
|
+
const defaultUser = {
|
|
74
|
+
username: os.userInfo().username || 'admin',
|
|
75
|
+
role: 'ADMIN',
|
|
76
|
+
addedAt: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
return { users: [defaultUser] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(fs.readFileSync(USERS_FILE, 'utf-8'));
|
|
83
|
+
} catch (_error) {
|
|
84
|
+
const defaultUser = {
|
|
85
|
+
username: os.userInfo().username || 'admin',
|
|
86
|
+
role: 'ADMIN',
|
|
87
|
+
addedAt: new Date().toISOString(),
|
|
88
|
+
};
|
|
89
|
+
return { users: [defaultUser] };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Save users
|
|
95
|
+
*/
|
|
96
|
+
function saveUsers(data) {
|
|
97
|
+
ensureRbacDir();
|
|
98
|
+
fs.writeFileSync(USERS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Add a user
|
|
103
|
+
*/
|
|
104
|
+
function addUser(username, role = 'VIEWER') {
|
|
105
|
+
if (!ROLES[role]) {
|
|
106
|
+
throw new Error(`Invalid role: ${role}. Must be ADMIN, OPERATOR, or VIEWER`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const data = getUsers();
|
|
110
|
+
|
|
111
|
+
// Check if user already exists
|
|
112
|
+
const existing = data.users.find(u => u.username === username);
|
|
113
|
+
if (existing) {
|
|
114
|
+
throw new Error(`User '${username}' already exists`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const user = {
|
|
118
|
+
username,
|
|
119
|
+
role,
|
|
120
|
+
addedAt: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
data.users.push(user);
|
|
124
|
+
saveUsers(data);
|
|
125
|
+
|
|
126
|
+
return user;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Remove a user
|
|
131
|
+
*/
|
|
132
|
+
function removeUser(username) {
|
|
133
|
+
const data = getUsers();
|
|
134
|
+
|
|
135
|
+
const index = data.users.findIndex(u => u.username === username);
|
|
136
|
+
if (index === -1) {
|
|
137
|
+
throw new Error(`User '${username}' not found`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Prevent removing last ADMIN
|
|
141
|
+
const user = data.users[index];
|
|
142
|
+
if (user.role === 'ADMIN') {
|
|
143
|
+
const adminCount = data.users.filter(u => u.role === 'ADMIN').length;
|
|
144
|
+
if (adminCount === 1) {
|
|
145
|
+
throw new Error('Cannot remove last ADMIN user');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
data.users.splice(index, 1);
|
|
150
|
+
saveUsers(data);
|
|
151
|
+
|
|
152
|
+
return user;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get current user
|
|
157
|
+
*/
|
|
158
|
+
function getCurrentUser() {
|
|
159
|
+
const currentUsername = os.userInfo().username || 'admin';
|
|
160
|
+
const data = getUsers();
|
|
161
|
+
|
|
162
|
+
let user = data.users.find(u => u.username === currentUsername);
|
|
163
|
+
|
|
164
|
+
// If user doesn't exist, create as ADMIN
|
|
165
|
+
if (!user) {
|
|
166
|
+
user = {
|
|
167
|
+
username: currentUsername,
|
|
168
|
+
role: 'ADMIN',
|
|
169
|
+
addedAt: new Date().toISOString(),
|
|
170
|
+
};
|
|
171
|
+
data.users.push(user);
|
|
172
|
+
saveUsers(data);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return user;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check if user has permission
|
|
180
|
+
*/
|
|
181
|
+
function hasPermission(permission) {
|
|
182
|
+
const user = getCurrentUser();
|
|
183
|
+
const role = ROLES[user.role];
|
|
184
|
+
|
|
185
|
+
if (!role) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return role.permissions.includes(permission);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Require permission (throws if denied)
|
|
194
|
+
*/
|
|
195
|
+
function requirePermission(permission, action = null) {
|
|
196
|
+
if (!hasPermission(permission)) {
|
|
197
|
+
const user = getCurrentUser();
|
|
198
|
+
const actionMsg = action ? ` to ${action}` : '';
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Permission denied${actionMsg}. Required: ${permission}. Your role: ${user.role}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get role details
|
|
207
|
+
*/
|
|
208
|
+
function getRole(roleName) {
|
|
209
|
+
return ROLES[roleName];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* List all roles
|
|
214
|
+
*/
|
|
215
|
+
function listRoles() {
|
|
216
|
+
return Object.values(ROLES);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Reset users (for testing)
|
|
221
|
+
*/
|
|
222
|
+
function resetUsers() {
|
|
223
|
+
if (fs.existsSync(USERS_FILE)) {
|
|
224
|
+
fs.unlinkSync(USERS_FILE);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
ROLES,
|
|
230
|
+
addUser,
|
|
231
|
+
removeUser,
|
|
232
|
+
getUsers,
|
|
233
|
+
getCurrentUser,
|
|
234
|
+
hasPermission,
|
|
235
|
+
requirePermission,
|
|
236
|
+
getRole,
|
|
237
|
+
listRoles,
|
|
238
|
+
resetUsers,
|
|
239
|
+
};
|