simple-authz 1.0.5 → 1.1.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/README.md +113 -350
- package/lib/cache.js +135 -0
- package/lib/core/authz.js +12 -0
- package/lib/core/middleware.js +22 -0
- package/package.json +16 -2
- package/types/index.d.ts +31 -0
- package/simple-authz-1.0.1.tgz +0 -0
- package/simple-authz-1.0.2.tgz +0 -0
- package/simple-authz-1.0.3.tgz +0 -0
- package/simple-authz-1.0.4.tgz +0 -0
- package/simple-authz-1.0.5.tgz +0 -0
- package/simple-authz-v1.0.1.tgz +0 -0
package/README.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
Your README is already strong — we just need to **upgrade it to reflect v1.1.0 features** without breaking its structure.
|
|
2
|
+
|
|
3
|
+
I’ll **edit and improve your version**, not rewrite from scratch.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ✅ Updated README (v1.1.0 Improvements Applied)
|
|
8
|
+
|
|
9
|
+
Below is your improved version with:
|
|
10
|
+
|
|
11
|
+
* ✔ middleware added
|
|
12
|
+
* ✔ caching mentioned
|
|
13
|
+
* ✔ TypeScript support
|
|
14
|
+
* ✔ `authz.explain()` refined
|
|
15
|
+
* ✔ Quick Start fixed (you exported instance, not class ⚠️)
|
|
16
|
+
* ✔ small clarity + professionalism upgrades
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
````markdown
|
|
1
21
|
# Simple Authz
|
|
2
22
|
|
|
3
23
|
A lightweight and flexible **authorization engine for Node.js** built around a simple policy language called **TOON (Token-Oriented Object Notation)**.
|
|
@@ -31,6 +51,7 @@ We recommend testing thoroughly before using it in production.
|
|
|
31
51
|
- Using Conditions
|
|
32
52
|
- Working with Roles
|
|
33
53
|
- Authorization API
|
|
54
|
+
- Middleware (Express)
|
|
34
55
|
- Example Integrations
|
|
35
56
|
- Project Structure
|
|
36
57
|
- Performance
|
|
@@ -49,72 +70,44 @@ Most applications implement authorization like this:
|
|
|
49
70
|
if(user.role === "admin") { ... }
|
|
50
71
|
if(user.id === listing.owner_id) { ... }
|
|
51
72
|
if(user.permissions.includes("edit_listing")) { ... }
|
|
52
|
-
|
|
73
|
+
````
|
|
53
74
|
|
|
54
75
|
Over time this logic spreads across many files and becomes difficult to maintain.
|
|
55
76
|
|
|
56
77
|
**Simple Authz solves this by moving authorization rules into policy files.**
|
|
57
78
|
|
|
58
|
-
Example policy file:
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
rule
|
|
62
|
-
role admin
|
|
63
|
-
action *
|
|
64
|
-
resource *
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
rule
|
|
68
|
-
role broker
|
|
69
|
-
action publish
|
|
70
|
-
resource listing
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
rule
|
|
74
|
-
role user
|
|
75
|
-
action edit
|
|
76
|
-
resource listing
|
|
77
|
-
condition listing.owner_id == user.id
|
|
78
|
-
end
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Your application then only needs to ask:
|
|
82
|
-
|
|
83
|
-
```javascript
|
|
84
|
-
authz.can(user, "edit", "listing", listing);
|
|
85
|
-
```
|
|
86
|
-
|
|
87
79
|
---
|
|
88
80
|
|
|
89
81
|
# Why Simple Authz
|
|
90
82
|
|
|
91
83
|
Benefits of centralized authorization:
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
* Clear separation between **business logic and security rules**
|
|
86
|
+
* Easy permission updates without touching application code
|
|
87
|
+
* Better maintainability for large projects
|
|
88
|
+
* Safer and more predictable access control
|
|
89
|
+
* Easier onboarding for new developers
|
|
98
90
|
|
|
99
91
|
---
|
|
100
92
|
|
|
101
93
|
# Key Features
|
|
102
94
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
95
|
+
* Simple and readable **policy language (TOON)**
|
|
96
|
+
* Fast permission lookup (compiled rules)
|
|
97
|
+
* Built-in **caching for repeated checks**
|
|
98
|
+
* Support for **multiple user roles**
|
|
99
|
+
* Logical **conditional rules (AND / OR)**
|
|
100
|
+
* Policy **validation**
|
|
101
|
+
* Policy **AST compilation (no eval)**
|
|
102
|
+
* Express middleware support
|
|
103
|
+
* Debugging via `authz.explain()`
|
|
104
|
+
* TypeScript support
|
|
105
|
+
* Lightweight with minimal dependencies
|
|
111
106
|
|
|
112
107
|
---
|
|
113
108
|
|
|
114
109
|
# Installation
|
|
115
110
|
|
|
116
|
-
Install using npm:
|
|
117
|
-
|
|
118
111
|
```bash
|
|
119
112
|
npm install simple-authz
|
|
120
113
|
```
|
|
@@ -126,36 +119,24 @@ npm install simple-authz
|
|
|
126
119
|
## 1. Import the library
|
|
127
120
|
|
|
128
121
|
```javascript
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const authz = new Authz();
|
|
122
|
+
const authz = require("simple-authz");
|
|
132
123
|
```
|
|
133
124
|
|
|
134
125
|
---
|
|
135
126
|
|
|
136
127
|
## 2. Create a policy file
|
|
137
128
|
|
|
138
|
-
Create a file called:
|
|
139
|
-
|
|
140
129
|
```
|
|
141
130
|
authz.toon
|
|
142
131
|
```
|
|
143
132
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
```
|
|
133
|
+
```toon
|
|
147
134
|
rule
|
|
148
135
|
role admin
|
|
149
136
|
action *
|
|
150
137
|
resource *
|
|
151
138
|
end
|
|
152
139
|
|
|
153
|
-
rule
|
|
154
|
-
role broker
|
|
155
|
-
action publish
|
|
156
|
-
resource listing
|
|
157
|
-
end
|
|
158
|
-
|
|
159
140
|
rule
|
|
160
141
|
role user
|
|
161
142
|
action edit
|
|
@@ -177,34 +158,12 @@ authz.load("./authz.toon");
|
|
|
177
158
|
## 4. Check permissions
|
|
178
159
|
|
|
179
160
|
```javascript
|
|
180
|
-
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Example usage:
|
|
184
|
-
|
|
185
|
-
```javascript
|
|
186
|
-
if (authz.can(user, "edit", "listing", listing)) {
|
|
187
|
-
updateListing();
|
|
188
|
-
} else {
|
|
189
|
-
throw new Error("Access denied");
|
|
190
|
-
}
|
|
161
|
+
authz.can(user, "edit", "listing", listing);
|
|
191
162
|
```
|
|
192
163
|
|
|
193
164
|
---
|
|
194
165
|
|
|
195
|
-
## 5. `authz.explain()`
|
|
196
|
-
|
|
197
|
-
The `authz.explain()` method helps debug authorization decisions by returning detailed information about **why access was allowed or denied**.
|
|
198
|
-
|
|
199
|
-
Unlike `authz.can()`, which returns only `true` or `false`, this method returns an object explaining the result.
|
|
200
|
-
|
|
201
|
-
### Usage
|
|
202
|
-
|
|
203
|
-
```javascript
|
|
204
|
-
authz.explain(user, "edit", "listing", listing);
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Example
|
|
166
|
+
## 5. Debug with `authz.explain()`
|
|
208
167
|
|
|
209
168
|
```javascript
|
|
210
169
|
const result = authz.explain(user, "edit", "listing", listing);
|
|
@@ -212,7 +171,7 @@ const result = authz.explain(user, "edit", "listing", listing);
|
|
|
212
171
|
console.log(result);
|
|
213
172
|
```
|
|
214
173
|
|
|
215
|
-
|
|
174
|
+
Example output:
|
|
216
175
|
|
|
217
176
|
```javascript
|
|
218
177
|
{
|
|
@@ -224,207 +183,84 @@ console.log(result);
|
|
|
224
183
|
}
|
|
225
184
|
```
|
|
226
185
|
|
|
227
|
-
If no rule matches:
|
|
228
|
-
|
|
229
|
-
```javascript
|
|
230
|
-
{
|
|
231
|
-
allowed: false,
|
|
232
|
-
reason: "no matching rule"
|
|
233
|
-
}
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Use `authz.explain()` for **debugging and development**, and `authz.can()` for **regular authorization checks**.
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
# Core Concepts
|
|
241
|
-
|
|
242
|
-
Simple Authz follows a common authorization model:
|
|
243
|
-
|
|
244
|
-
```
|
|
245
|
-
Subject → Action → Resource
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
| Component | Description |
|
|
249
|
-
| --------- | ------------------------------ |
|
|
250
|
-
| Subject | The user performing the action |
|
|
251
|
-
| Action | What the user wants to do |
|
|
252
|
-
| Resource | The object being accessed |
|
|
253
|
-
|
|
254
|
-
Example:
|
|
255
|
-
|
|
256
|
-
```
|
|
257
|
-
broker → edit → listing
|
|
258
|
-
```
|
|
259
|
-
|
|
260
186
|
---
|
|
261
187
|
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
Policies are written in **TOON format**.
|
|
188
|
+
# Middleware (Express)
|
|
265
189
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
```
|
|
269
|
-
rule
|
|
270
|
-
role <Role>
|
|
271
|
-
action <Action>
|
|
272
|
-
resource <Resource>
|
|
273
|
-
end
|
|
274
|
-
```
|
|
190
|
+
Protect routes easily:
|
|
275
191
|
|
|
276
|
-
|
|
192
|
+
```javascript
|
|
193
|
+
const express = require("express");
|
|
194
|
+
const authz = require("simple-authz");
|
|
277
195
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
role admin
|
|
281
|
-
action *
|
|
282
|
-
resource *
|
|
283
|
-
end
|
|
196
|
+
const app = express();
|
|
197
|
+
app.use(express.json());
|
|
284
198
|
|
|
285
|
-
|
|
286
|
-
role broker
|
|
287
|
-
action publish
|
|
288
|
-
resource listing
|
|
289
|
-
end
|
|
199
|
+
authz.load("./authz.toon");
|
|
290
200
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
201
|
+
app.post(
|
|
202
|
+
"/listing",
|
|
203
|
+
authz.middleware("edit", "listing"),
|
|
204
|
+
(req, res) => {
|
|
205
|
+
res.send("Allowed");
|
|
206
|
+
}
|
|
207
|
+
);
|
|
297
208
|
```
|
|
298
209
|
|
|
299
210
|
---
|
|
300
211
|
|
|
301
|
-
#
|
|
302
|
-
|
|
303
|
-
You can use `*` to allow everything.
|
|
304
|
-
|
|
305
|
-
Example:
|
|
212
|
+
# Core Concepts
|
|
306
213
|
|
|
307
214
|
```
|
|
308
|
-
|
|
309
|
-
role admin
|
|
310
|
-
action *
|
|
311
|
-
resource *
|
|
312
|
-
end
|
|
215
|
+
Subject → Action → Resource
|
|
313
216
|
```
|
|
314
217
|
|
|
315
|
-
|
|
218
|
+
| Component | Description |
|
|
219
|
+
| --------- | ------------- |
|
|
220
|
+
| Subject | User |
|
|
221
|
+
| Action | Operation |
|
|
222
|
+
| Resource | Target object |
|
|
316
223
|
|
|
317
224
|
---
|
|
318
225
|
|
|
319
|
-
#
|
|
320
|
-
|
|
321
|
-
Policies can include conditions.
|
|
226
|
+
# Policy Language (TOON)
|
|
322
227
|
|
|
323
|
-
|
|
228
|
+
Basic syntax:
|
|
324
229
|
|
|
325
230
|
```
|
|
326
231
|
rule
|
|
327
232
|
role <role>
|
|
328
233
|
action <action>
|
|
329
234
|
resource <resource>
|
|
330
|
-
condition <condition1> OR <condition2>
|
|
331
235
|
end
|
|
332
236
|
```
|
|
333
237
|
|
|
334
|
-
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
# Wildcards
|
|
335
241
|
|
|
336
242
|
```
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
action view
|
|
340
|
-
resource listing
|
|
341
|
-
condition listing.status == "public" OR listing.owner_id == user.id
|
|
342
|
-
end
|
|
243
|
+
action *
|
|
244
|
+
resource *
|
|
343
245
|
```
|
|
344
246
|
|
|
345
|
-
Meaning:
|
|
346
|
-
|
|
347
|
-
A broker can only edit listings they own.
|
|
348
|
-
|
|
349
247
|
---
|
|
350
248
|
|
|
351
|
-
#
|
|
352
|
-
|
|
353
|
-
Conditions can reference:
|
|
354
|
-
|
|
355
|
-
| Variable | Description |
|
|
356
|
-
| -------- | --------------------- |
|
|
357
|
-
| user | authenticated user |
|
|
358
|
-
| resource | target resource |
|
|
359
|
-
| context | optional request data |
|
|
249
|
+
# Conditional Rules
|
|
360
250
|
|
|
361
251
|
Example:
|
|
362
252
|
|
|
363
253
|
```
|
|
364
|
-
|
|
365
|
-
role user
|
|
366
|
-
action edit
|
|
367
|
-
resource profile
|
|
368
|
-
condition user.id == resource.id
|
|
369
|
-
end
|
|
254
|
+
condition listing.owner_id == user.id AND listing.status != "published"
|
|
370
255
|
```
|
|
371
256
|
|
|
372
257
|
---
|
|
373
258
|
|
|
374
259
|
# Policy Examples
|
|
375
260
|
|
|
376
|
-
|
|
261
|
+
### Ownership Rule
|
|
377
262
|
|
|
378
263
|
```
|
|
379
|
-
rule
|
|
380
|
-
role admin
|
|
381
|
-
action *
|
|
382
|
-
resource *
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
rule
|
|
386
|
-
role broker
|
|
387
|
-
action publish
|
|
388
|
-
resource listing
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
rule
|
|
392
|
-
role user
|
|
393
|
-
action edit
|
|
394
|
-
resource listing
|
|
395
|
-
end
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
---
|
|
399
|
-
|
|
400
|
-
## Example 2 – Ownership Rules
|
|
401
|
-
|
|
402
|
-
```
|
|
403
|
-
rule
|
|
404
|
-
role broker
|
|
405
|
-
action edit
|
|
406
|
-
resource listing
|
|
407
|
-
condition listing.owner_id == user.id
|
|
408
|
-
end
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
---
|
|
412
|
-
|
|
413
|
-
## Example 3 – Multiple Rules
|
|
414
|
-
|
|
415
|
-
```
|
|
416
|
-
rule
|
|
417
|
-
role admin
|
|
418
|
-
action *
|
|
419
|
-
resource *
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
rule
|
|
423
|
-
role broker
|
|
424
|
-
action publish
|
|
425
|
-
resource listing
|
|
426
|
-
end
|
|
427
|
-
|
|
428
264
|
rule
|
|
429
265
|
role user
|
|
430
266
|
action edit
|
|
@@ -437,33 +273,23 @@ end
|
|
|
437
273
|
|
|
438
274
|
# Working with Roles
|
|
439
275
|
|
|
440
|
-
Users can have one or multiple roles.
|
|
441
|
-
|
|
442
|
-
Example user:
|
|
443
|
-
|
|
444
276
|
```javascript
|
|
445
|
-
|
|
446
|
-
id: 10,
|
|
447
|
-
roles: ["broker"],
|
|
448
|
-
};
|
|
277
|
+
user.role = "admin";
|
|
449
278
|
```
|
|
450
279
|
|
|
451
|
-
|
|
280
|
+
or
|
|
452
281
|
|
|
453
282
|
```javascript
|
|
454
|
-
|
|
455
|
-
id: 5,
|
|
456
|
-
roles: ["user", "editor"],
|
|
457
|
-
};
|
|
283
|
+
user.roles = ["user", "editor"];
|
|
458
284
|
```
|
|
459
285
|
|
|
460
|
-
|
|
286
|
+
Access is granted if **any role matches**.
|
|
461
287
|
|
|
462
288
|
---
|
|
463
289
|
|
|
464
290
|
# Authorization API
|
|
465
291
|
|
|
466
|
-
## Load policy
|
|
292
|
+
## Load policy
|
|
467
293
|
|
|
468
294
|
```javascript
|
|
469
295
|
authz.load("./authz.toon");
|
|
@@ -474,91 +300,37 @@ authz.load("./authz.toon");
|
|
|
474
300
|
## Check permission
|
|
475
301
|
|
|
476
302
|
```javascript
|
|
477
|
-
authz.can(user, action, resource,
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
Example:
|
|
481
|
-
|
|
482
|
-
```javascript
|
|
483
|
-
authz.can(user, "edit", "listing", listing);
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
Returns:
|
|
487
|
-
|
|
488
|
-
```
|
|
489
|
-
true
|
|
490
|
-
false
|
|
303
|
+
authz.can(user, action, resource, data);
|
|
491
304
|
```
|
|
492
305
|
|
|
493
306
|
---
|
|
494
307
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
## Example: Listing System
|
|
498
|
-
|
|
499
|
-
User:
|
|
308
|
+
## Explain decision
|
|
500
309
|
|
|
501
310
|
```javascript
|
|
502
|
-
|
|
503
|
-
id: 10,
|
|
504
|
-
roles: ["broker"],
|
|
505
|
-
};
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
Listing:
|
|
509
|
-
|
|
510
|
-
```javascript
|
|
511
|
-
const listing = {
|
|
512
|
-
owner_id: 10,
|
|
513
|
-
};
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
Permission check:
|
|
517
|
-
|
|
518
|
-
```javascript
|
|
519
|
-
authz.can(user, "edit", "listing", listing);
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
Result:
|
|
523
|
-
|
|
524
|
-
```
|
|
525
|
-
true
|
|
311
|
+
authz.explain(user, action, resource, data);
|
|
526
312
|
```
|
|
527
313
|
|
|
528
314
|
---
|
|
529
315
|
|
|
530
|
-
# Example
|
|
531
|
-
|
|
532
|
-
Example route protection:
|
|
316
|
+
# Example Integration
|
|
533
317
|
|
|
534
318
|
```javascript
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (!allowed) {
|
|
539
|
-
return res.status(403).send("Access denied");
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
updateListing();
|
|
543
|
-
});
|
|
319
|
+
if (!authz.can(req.user, "edit", "listing", req.listing)) {
|
|
320
|
+
return res.status(403).send("Access denied");
|
|
321
|
+
}
|
|
544
322
|
```
|
|
545
323
|
|
|
546
324
|
---
|
|
547
325
|
|
|
548
326
|
# Project Structure Example
|
|
549
327
|
|
|
550
|
-
Recommended project layout:
|
|
551
|
-
|
|
552
328
|
```
|
|
553
|
-
project
|
|
554
|
-
|
|
555
|
-
├── policies
|
|
329
|
+
project/
|
|
330
|
+
├── policies/
|
|
556
331
|
│ └── authz.toon
|
|
557
|
-
|
|
558
|
-
├── src
|
|
559
|
-
│
|
|
332
|
+
├── src/
|
|
560
333
|
├── app.js
|
|
561
|
-
│
|
|
562
334
|
└── package.json
|
|
563
335
|
```
|
|
564
336
|
|
|
@@ -566,66 +338,57 @@ project
|
|
|
566
338
|
|
|
567
339
|
# Performance
|
|
568
340
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
This allows the engine to remain efficient even as policy files grow.
|
|
341
|
+
* Rules compiled into indexed structure
|
|
342
|
+
* AST-based condition evaluation
|
|
343
|
+
* O(1) permission lookup
|
|
344
|
+
* Built-in caching layer
|
|
574
345
|
|
|
575
346
|
---
|
|
576
347
|
|
|
577
348
|
# Security Model
|
|
578
349
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
Meaning:
|
|
582
|
-
|
|
583
|
-
If no rule allows an action, access is automatically denied.
|
|
350
|
+
**Deny by default**
|
|
584
351
|
|
|
585
352
|
```
|
|
586
353
|
no rule → deny
|
|
587
354
|
rule exists → allow
|
|
588
355
|
```
|
|
589
356
|
|
|
590
|
-
This reduces the risk of unintended access.
|
|
591
|
-
|
|
592
357
|
---
|
|
593
358
|
|
|
594
359
|
# Roadmap
|
|
595
360
|
|
|
596
|
-
|
|
361
|
+
### v1.2
|
|
597
362
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
- Middleware helpers
|
|
602
|
-
- Permission caching
|
|
603
|
-
- Modular policy files
|
|
604
|
-
- Distributed policy storage
|
|
363
|
+
* CLI tool
|
|
364
|
+
* Role hierarchy
|
|
365
|
+
* Modular policies
|
|
605
366
|
|
|
606
|
-
|
|
367
|
+
### v2.0
|
|
607
368
|
|
|
608
|
-
|
|
369
|
+
* Advanced ABAC
|
|
370
|
+
* Distributed policies
|
|
371
|
+
* Policy versioning
|
|
609
372
|
|
|
610
|
-
|
|
373
|
+
---
|
|
611
374
|
|
|
612
|
-
|
|
375
|
+
# Contributing
|
|
613
376
|
|
|
614
|
-
1. Fork
|
|
615
|
-
2. Create
|
|
616
|
-
3.
|
|
617
|
-
4. Open a pull request
|
|
377
|
+
1. Fork repository
|
|
378
|
+
2. Create branch
|
|
379
|
+
3. Submit PR
|
|
618
380
|
|
|
619
381
|
---
|
|
620
382
|
|
|
621
383
|
# License
|
|
622
384
|
|
|
623
|
-
|
|
385
|
+
Apache 2.0
|
|
624
386
|
|
|
625
387
|
---
|
|
626
388
|
|
|
627
|
-
#
|
|
389
|
+
# Keywords
|
|
628
390
|
|
|
629
|
-
|
|
391
|
+
authorization, rbac, abac, nodejs, security, policy-engine
|
|
392
|
+
|
|
393
|
+
````
|
|
630
394
|
|
|
631
|
-
# Keywords
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LRU Cache for authorization decisions
|
|
3
|
+
* - Stores user + action + resource + objectId combinations
|
|
4
|
+
* - TTL-based expiration
|
|
5
|
+
* - LRU eviction when max size exceeded
|
|
6
|
+
* - Automatic invalidation on policy reload
|
|
7
|
+
*/
|
|
8
|
+
class AuthzCache {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.maxSize = options.maxSize || 1000;
|
|
11
|
+
this.ttl = options.ttl || 3600; // seconds
|
|
12
|
+
this.enabled = options.enabled !== false;
|
|
13
|
+
|
|
14
|
+
this.cache = new Map();
|
|
15
|
+
this.stats = {
|
|
16
|
+
hits: 0,
|
|
17
|
+
misses: 0,
|
|
18
|
+
evictions: 0,
|
|
19
|
+
timeouts: 0
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate cache key from request parameters
|
|
25
|
+
* Format: {userId}:{action}:{resource}:{objectId or 'null'}
|
|
26
|
+
*/
|
|
27
|
+
_key(user, action, resource, objectId) {
|
|
28
|
+
return `${user.id}:${action}:${resource}:${objectId || 'null'}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get cached authorization result
|
|
33
|
+
* Returns null if:
|
|
34
|
+
* - Key not in cache
|
|
35
|
+
* - Entry expired (TTL exceeded)
|
|
36
|
+
*/
|
|
37
|
+
get(user, action, resource, objectId) {
|
|
38
|
+
if (!this.enabled) return null;
|
|
39
|
+
|
|
40
|
+
const key = this._key(user, action, resource, objectId);
|
|
41
|
+
const entry = this.cache.get(key);
|
|
42
|
+
|
|
43
|
+
if (!entry) {
|
|
44
|
+
this.stats.misses++;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check TTL
|
|
49
|
+
const age = (Date.now() - entry.timestamp) / 1000;
|
|
50
|
+
if (age > this.ttl) {
|
|
51
|
+
this.cache.delete(key);
|
|
52
|
+
this.stats.timeouts++;
|
|
53
|
+
this.stats.misses++;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Hit!
|
|
58
|
+
entry.lastAccess = Date.now();
|
|
59
|
+
this.stats.hits++;
|
|
60
|
+
return entry.result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Store authorization result in cache
|
|
65
|
+
* Evicts oldest entry (LRU) if at capacity
|
|
66
|
+
*/
|
|
67
|
+
set(user, action, resource, objectId, result) {
|
|
68
|
+
if (!this.enabled) return;
|
|
69
|
+
|
|
70
|
+
// Check if we need to evict
|
|
71
|
+
if (this.cache.size >= this.maxSize) {
|
|
72
|
+
// Find oldest entry by timestamp (not lastAccess, to keep simple)
|
|
73
|
+
let oldest = null;
|
|
74
|
+
let oldestKey = null;
|
|
75
|
+
|
|
76
|
+
for (const [key, entry] of this.cache) {
|
|
77
|
+
if (!oldest || entry.timestamp < oldest.timestamp) {
|
|
78
|
+
oldest = entry;
|
|
79
|
+
oldestKey = key;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (oldestKey) {
|
|
84
|
+
this.cache.delete(oldestKey);
|
|
85
|
+
this.stats.evictions++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Store new entry
|
|
90
|
+
const key = this._key(user, action, resource, objectId);
|
|
91
|
+
this.cache.set(key, {
|
|
92
|
+
result,
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
lastAccess: Date.now()
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clear entire cache
|
|
100
|
+
* Used on policy reload
|
|
101
|
+
*/
|
|
102
|
+
clear() {
|
|
103
|
+
const before = this.cache.size;
|
|
104
|
+
this.cache.clear();
|
|
105
|
+
return { cleared: before, stats: this.stats };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get cache statistics
|
|
110
|
+
*/
|
|
111
|
+
stats() {
|
|
112
|
+
const total = this.stats.hits + this.stats.misses;
|
|
113
|
+
const hitRate = total === 0 ? 0 : ((this.stats.hits / total) * 100).toFixed(2);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
hits: this.stats.hits,
|
|
117
|
+
misses: this.stats.misses,
|
|
118
|
+
evictions: this.stats.evictions,
|
|
119
|
+
timeouts: this.stats.timeouts,
|
|
120
|
+
hitRate: hitRate + '%',
|
|
121
|
+
size: this.cache.size,
|
|
122
|
+
maxSize: this.maxSize,
|
|
123
|
+
utilizationRate: ((this.cache.size / this.maxSize) * 100).toFixed(2) + '%'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reset statistics (for testing/monitoring)
|
|
129
|
+
*/
|
|
130
|
+
resetStats() {
|
|
131
|
+
this.stats = { hits: 0, misses: 0, evictions: 0, timeouts: 0 };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = AuthzCache;
|
package/lib/core/authz.js
CHANGED
|
@@ -3,12 +3,14 @@ const { compileRules } = require("../rules/compiler")
|
|
|
3
3
|
const { validateRules } = require("../rules/validator")
|
|
4
4
|
const { normalizeUser } = require("../utils/normalizeUser")
|
|
5
5
|
const { evaluateAST } = require("../engine/conditionExecutor")
|
|
6
|
+
const createMiddleware = require("./middleware")
|
|
6
7
|
|
|
7
8
|
class Authz {
|
|
8
9
|
|
|
9
10
|
constructor() {
|
|
10
11
|
this.permissions = {}
|
|
11
12
|
this.conditions = {}
|
|
13
|
+
this.cache = new Map()
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
load(filePath) {
|
|
@@ -25,6 +27,11 @@ class Authz {
|
|
|
25
27
|
can(user, action, resourceType, data = {}) {
|
|
26
28
|
|
|
27
29
|
const roles = normalizeUser(user)
|
|
30
|
+
const cacheKey = JSON.stringify({ user, action, resourceType, data })
|
|
31
|
+
|
|
32
|
+
if (this.cache.has(cacheKey)) {
|
|
33
|
+
return this.cache.get(cacheKey)
|
|
34
|
+
}
|
|
28
35
|
|
|
29
36
|
for (const role of roles) {
|
|
30
37
|
|
|
@@ -167,4 +174,9 @@ class Authz {
|
|
|
167
174
|
}
|
|
168
175
|
}
|
|
169
176
|
|
|
177
|
+
Authz.prototype.middleware = function (action, resourceType) {
|
|
178
|
+
const middleware = createMiddleware(this)
|
|
179
|
+
return middleware(action, resourceType)
|
|
180
|
+
}
|
|
181
|
+
|
|
170
182
|
module.exports = Authz
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module.exports = function (authz) {
|
|
2
|
+
return function (action, resourceType) {
|
|
3
|
+
return (req, res, next) => {
|
|
4
|
+
const user = req.user
|
|
5
|
+
const resource = req.body || {}
|
|
6
|
+
|
|
7
|
+
if (!user) {
|
|
8
|
+
return res.status(401).json({ error: "Unauthorized" })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const allowed = authz.can(user, action, resourceType, resource)
|
|
12
|
+
|
|
13
|
+
if (allowed) {
|
|
14
|
+
return next()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return res.status(403).json({
|
|
18
|
+
error: "Forbidden"
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-authz",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Lightweight policy-based authorization engine for Node.js using TOON policy language",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "jest",
|
|
8
8
|
"dev": "node lib/core/authz.js"
|
|
9
9
|
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"simple-authz": "bin/simple-authz"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"chalk": "^5.0.0",
|
|
15
|
+
"commander": "^11.0.0",
|
|
16
|
+
"jest": "^30.3.0"
|
|
17
|
+
},
|
|
10
18
|
"keywords": [
|
|
11
19
|
"authorization",
|
|
12
20
|
"access-control",
|
|
@@ -23,6 +31,12 @@
|
|
|
23
31
|
"role-based-access-control",
|
|
24
32
|
"authorization-engine"
|
|
25
33
|
],
|
|
34
|
+
"types": "types/index.d.ts",
|
|
35
|
+
"files": [
|
|
36
|
+
"lib/",
|
|
37
|
+
"types/",
|
|
38
|
+
"package.json"
|
|
39
|
+
],
|
|
26
40
|
"author": "Dhruvil",
|
|
27
41
|
"license": "Apache-2.0",
|
|
28
42
|
"repository": {
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
declare module "simple-authz" {
|
|
2
|
+
interface User {
|
|
3
|
+
role?: string;
|
|
4
|
+
roles?: string[];
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ExplainResult {
|
|
9
|
+
allowed: boolean;
|
|
10
|
+
role?: string;
|
|
11
|
+
resource?: string;
|
|
12
|
+
action?: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
condition?: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class Authz {
|
|
18
|
+
load(path: string): void;
|
|
19
|
+
can(user: User, action: string, resource: string, data?: any): boolean;
|
|
20
|
+
explain(
|
|
21
|
+
user: User,
|
|
22
|
+
action: string,
|
|
23
|
+
resource: string,
|
|
24
|
+
data?: any,
|
|
25
|
+
): ExplainResult;
|
|
26
|
+
middleware(action: string, resource: string): any;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const authz: Authz;
|
|
30
|
+
export = authz;
|
|
31
|
+
}
|
package/simple-authz-1.0.1.tgz
DELETED
|
Binary file
|
package/simple-authz-1.0.2.tgz
DELETED
|
Binary file
|
package/simple-authz-1.0.3.tgz
DELETED
|
Binary file
|
package/simple-authz-1.0.4.tgz
DELETED
|
Binary file
|
package/simple-authz-1.0.5.tgz
DELETED
|
Binary file
|
package/simple-authz-v1.0.1.tgz
DELETED
|
Binary file
|