simple-authz 1.0.4 → 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 +156 -309
- 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-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)**.
|
|
@@ -8,26 +28,37 @@ The goal of this project is to make authorization **simple, fast, readable, and
|
|
|
8
28
|
|
|
9
29
|
---
|
|
10
30
|
|
|
31
|
+
## ⚠️ Development Status
|
|
32
|
+
|
|
33
|
+
**simple-authz** is currently under active development.
|
|
34
|
+
|
|
35
|
+
Some features may change, and there may be inconsistencies between the documentation and the package behavior.
|
|
36
|
+
|
|
37
|
+
We recommend testing thoroughly before using it in production.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
11
41
|
# Table of Contents
|
|
12
42
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
- Overview
|
|
44
|
+
- Why Simple Authz
|
|
45
|
+
- Key Features
|
|
46
|
+
- Installation
|
|
47
|
+
- Quick Start
|
|
48
|
+
- Core Concepts
|
|
49
|
+
- Policy Language (TOON)
|
|
50
|
+
- Policy Examples
|
|
51
|
+
- Using Conditions
|
|
52
|
+
- Working with Roles
|
|
53
|
+
- Authorization API
|
|
54
|
+
- Middleware (Express)
|
|
55
|
+
- Example Integrations
|
|
56
|
+
- Project Structure
|
|
57
|
+
- Performance
|
|
58
|
+
- Security Model
|
|
59
|
+
- Roadmap
|
|
60
|
+
- Contributing
|
|
61
|
+
- License
|
|
31
62
|
|
|
32
63
|
---
|
|
33
64
|
|
|
@@ -39,31 +70,12 @@ Most applications implement authorization like this:
|
|
|
39
70
|
if(user.role === "admin") { ... }
|
|
40
71
|
if(user.id === listing.owner_id) { ... }
|
|
41
72
|
if(user.permissions.includes("edit_listing")) { ... }
|
|
42
|
-
|
|
73
|
+
````
|
|
43
74
|
|
|
44
75
|
Over time this logic spreads across many files and becomes difficult to maintain.
|
|
45
76
|
|
|
46
77
|
**Simple Authz solves this by moving authorization rules into policy files.**
|
|
47
78
|
|
|
48
|
-
Example policy file:
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
allow admin *
|
|
52
|
-
|
|
53
|
-
allow user view listing
|
|
54
|
-
|
|
55
|
-
allow broker publish listing
|
|
56
|
-
|
|
57
|
-
allow broker edit listing
|
|
58
|
-
condition listing.owner_id == user.id
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Your application then only needs to ask:
|
|
62
|
-
|
|
63
|
-
```javascript
|
|
64
|
-
authz.can(user, "edit", "listing", listing)
|
|
65
|
-
```
|
|
66
|
-
|
|
67
79
|
---
|
|
68
80
|
|
|
69
81
|
# Why Simple Authz
|
|
@@ -80,21 +92,22 @@ Benefits of centralized authorization:
|
|
|
80
92
|
|
|
81
93
|
# Key Features
|
|
82
94
|
|
|
83
|
-
* Simple and readable **policy language**
|
|
84
|
-
* Fast permission lookup
|
|
95
|
+
* Simple and readable **policy language (TOON)**
|
|
96
|
+
* Fast permission lookup (compiled rules)
|
|
97
|
+
* Built-in **caching for repeated checks**
|
|
85
98
|
* Support for **multiple user roles**
|
|
86
|
-
* Logical **conditional rules**
|
|
99
|
+
* Logical **conditional rules (AND / OR)**
|
|
87
100
|
* Policy **validation**
|
|
88
|
-
* Policy **AST compilation**
|
|
101
|
+
* Policy **AST compilation (no eval)**
|
|
102
|
+
* Express middleware support
|
|
103
|
+
* Debugging via `authz.explain()`
|
|
104
|
+
* TypeScript support
|
|
89
105
|
* Lightweight with minimal dependencies
|
|
90
|
-
* Works with any Node.js framework
|
|
91
106
|
|
|
92
107
|
---
|
|
93
108
|
|
|
94
109
|
# Installation
|
|
95
110
|
|
|
96
|
-
Install using npm:
|
|
97
|
-
|
|
98
111
|
```bash
|
|
99
112
|
npm install simple-authz
|
|
100
113
|
```
|
|
@@ -106,32 +119,30 @@ npm install simple-authz
|
|
|
106
119
|
## 1. Import the library
|
|
107
120
|
|
|
108
121
|
```javascript
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const authz = new Authz()
|
|
122
|
+
const authz = require("simple-authz");
|
|
112
123
|
```
|
|
113
124
|
|
|
114
125
|
---
|
|
115
126
|
|
|
116
127
|
## 2. Create a policy file
|
|
117
128
|
|
|
118
|
-
Create a file called:
|
|
119
|
-
|
|
120
129
|
```
|
|
121
130
|
authz.toon
|
|
122
131
|
```
|
|
123
132
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
allow broker publish listing
|
|
133
|
+
```toon
|
|
134
|
+
rule
|
|
135
|
+
role admin
|
|
136
|
+
action *
|
|
137
|
+
resource *
|
|
138
|
+
end
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
rule
|
|
141
|
+
role user
|
|
142
|
+
action edit
|
|
143
|
+
resource listing
|
|
144
|
+
condition listing.owner_id == user.id
|
|
145
|
+
end
|
|
135
146
|
```
|
|
136
147
|
|
|
137
148
|
---
|
|
@@ -139,7 +150,7 @@ condition listing.owner_id == user.id
|
|
|
139
150
|
## 3. Load the policy
|
|
140
151
|
|
|
141
152
|
```javascript
|
|
142
|
-
authz.load("./authz.toon")
|
|
153
|
+
authz.load("./authz.toon");
|
|
143
154
|
```
|
|
144
155
|
|
|
145
156
|
---
|
|
@@ -147,42 +158,20 @@ authz.load("./authz.toon")
|
|
|
147
158
|
## 4. Check permissions
|
|
148
159
|
|
|
149
160
|
```javascript
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Example usage:
|
|
154
|
-
|
|
155
|
-
```javascript
|
|
156
|
-
if(authz.can(user,"edit", "listing", listing)){
|
|
157
|
-
updateListing()
|
|
158
|
-
}else{
|
|
159
|
-
throw new Error("Access denied")
|
|
160
|
-
}
|
|
161
|
+
authz.can(user, "edit", "listing", listing);
|
|
161
162
|
```
|
|
162
163
|
|
|
163
164
|
---
|
|
164
165
|
|
|
165
|
-
## 5. `authz.explain()`
|
|
166
|
-
|
|
167
|
-
The `authz.explain()` method helps debug authorization decisions by returning detailed information about **why access was allowed or denied**.
|
|
168
|
-
|
|
169
|
-
Unlike `authz.can()`, which returns only `true` or `false`, this method returns an object explaining the result.
|
|
170
|
-
|
|
171
|
-
### Usage
|
|
172
|
-
|
|
173
|
-
```javascript
|
|
174
|
-
authz.explain(user, "edit", "listing", listing)
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### Example
|
|
166
|
+
## 5. Debug with `authz.explain()`
|
|
178
167
|
|
|
179
168
|
```javascript
|
|
180
|
-
const result = authz.explain(user, "edit", "listing", listing)
|
|
169
|
+
const result = authz.explain(user, "edit", "listing", listing);
|
|
181
170
|
|
|
182
|
-
console.log(result)
|
|
171
|
+
console.log(result);
|
|
183
172
|
```
|
|
184
173
|
|
|
185
|
-
|
|
174
|
+
Example output:
|
|
186
175
|
|
|
187
176
|
```javascript
|
|
188
177
|
{
|
|
@@ -194,188 +183,116 @@ console.log(result)
|
|
|
194
183
|
}
|
|
195
184
|
```
|
|
196
185
|
|
|
197
|
-
If no rule matches:
|
|
198
|
-
|
|
199
|
-
```javascript
|
|
200
|
-
{
|
|
201
|
-
allowed: false,
|
|
202
|
-
reason: "no matching rule"
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Use `authz.explain()` for **debugging and development**, and `authz.can()` for **regular authorization checks**.
|
|
207
|
-
|
|
208
186
|
---
|
|
209
187
|
|
|
210
|
-
#
|
|
188
|
+
# Middleware (Express)
|
|
211
189
|
|
|
212
|
-
|
|
190
|
+
Protect routes easily:
|
|
213
191
|
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
|
|
192
|
+
```javascript
|
|
193
|
+
const express = require("express");
|
|
194
|
+
const authz = require("simple-authz");
|
|
217
195
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
| Subject | The user performing the action |
|
|
221
|
-
| Action | What the user wants to do |
|
|
222
|
-
| Resource | The object being accessed |
|
|
196
|
+
const app = express();
|
|
197
|
+
app.use(express.json());
|
|
223
198
|
|
|
224
|
-
|
|
199
|
+
authz.load("./authz.toon");
|
|
225
200
|
|
|
226
|
-
|
|
227
|
-
|
|
201
|
+
app.post(
|
|
202
|
+
"/listing",
|
|
203
|
+
authz.middleware("edit", "listing"),
|
|
204
|
+
(req, res) => {
|
|
205
|
+
res.send("Allowed");
|
|
206
|
+
}
|
|
207
|
+
);
|
|
228
208
|
```
|
|
229
209
|
|
|
230
210
|
---
|
|
231
211
|
|
|
232
|
-
#
|
|
233
|
-
|
|
234
|
-
Policies are written in **TOON format**.
|
|
235
|
-
|
|
236
|
-
Basic rule syntax:
|
|
212
|
+
# Core Concepts
|
|
237
213
|
|
|
238
214
|
```
|
|
239
|
-
|
|
215
|
+
Subject → Action → Resource
|
|
240
216
|
```
|
|
241
217
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
allow broker publish listing
|
|
248
|
-
```
|
|
218
|
+
| Component | Description |
|
|
219
|
+
| --------- | ------------- |
|
|
220
|
+
| Subject | User |
|
|
221
|
+
| Action | Operation |
|
|
222
|
+
| Resource | Target object |
|
|
249
223
|
|
|
250
224
|
---
|
|
251
225
|
|
|
252
|
-
#
|
|
226
|
+
# Policy Language (TOON)
|
|
253
227
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
Example:
|
|
228
|
+
Basic syntax:
|
|
257
229
|
|
|
258
230
|
```
|
|
259
|
-
|
|
231
|
+
rule
|
|
232
|
+
role <role>
|
|
233
|
+
action <action>
|
|
234
|
+
resource <resource>
|
|
235
|
+
end
|
|
260
236
|
```
|
|
261
237
|
|
|
262
|
-
This allows the admin role to perform **any action on any resource**.
|
|
263
|
-
|
|
264
238
|
---
|
|
265
239
|
|
|
266
|
-
#
|
|
267
|
-
|
|
268
|
-
Policies can include conditions.
|
|
269
|
-
|
|
270
|
-
Syntax:
|
|
271
|
-
|
|
272
|
-
```
|
|
273
|
-
allow <role> <action> <resource>
|
|
274
|
-
condition <condition>
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
Example:
|
|
240
|
+
# Wildcards
|
|
278
241
|
|
|
279
242
|
```
|
|
280
|
-
|
|
281
|
-
|
|
243
|
+
action *
|
|
244
|
+
resource *
|
|
282
245
|
```
|
|
283
246
|
|
|
284
|
-
Meaning:
|
|
285
|
-
|
|
286
|
-
A broker can only edit listings they own.
|
|
287
|
-
|
|
288
247
|
---
|
|
289
248
|
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
Conditions can reference:
|
|
293
|
-
|
|
294
|
-
| Variable | Description |
|
|
295
|
-
| -------- | --------------------- |
|
|
296
|
-
| user | authenticated user |
|
|
297
|
-
| resource | target resource |
|
|
298
|
-
| context | optional request data |
|
|
249
|
+
# Conditional Rules
|
|
299
250
|
|
|
300
251
|
Example:
|
|
301
252
|
|
|
302
253
|
```
|
|
303
|
-
|
|
304
|
-
condition user.id == resource.id
|
|
254
|
+
condition listing.owner_id == user.id AND listing.status != "published"
|
|
305
255
|
```
|
|
306
256
|
|
|
307
257
|
---
|
|
308
258
|
|
|
309
259
|
# Policy Examples
|
|
310
260
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
```
|
|
314
|
-
allow admin *
|
|
315
|
-
|
|
316
|
-
allow user view listing
|
|
317
|
-
|
|
318
|
-
allow broker publish listing
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
---
|
|
322
|
-
|
|
323
|
-
## Example 2 – Ownership Rules
|
|
261
|
+
### Ownership Rule
|
|
324
262
|
|
|
325
263
|
```
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
## Example 3 – Multiple Rules
|
|
333
|
-
|
|
334
|
-
```
|
|
335
|
-
allow admin *
|
|
336
|
-
|
|
337
|
-
allow user view listing
|
|
338
|
-
|
|
339
|
-
allow broker publish listing
|
|
340
|
-
|
|
341
|
-
allow broker edit listing
|
|
342
|
-
condition listing.owner_id == user.id
|
|
264
|
+
rule
|
|
265
|
+
role user
|
|
266
|
+
action edit
|
|
267
|
+
resource listing
|
|
268
|
+
condition listing.owner_id == user.id
|
|
269
|
+
end
|
|
343
270
|
```
|
|
344
271
|
|
|
345
272
|
---
|
|
346
273
|
|
|
347
274
|
# Working with Roles
|
|
348
275
|
|
|
349
|
-
Users can have one or multiple roles.
|
|
350
|
-
|
|
351
|
-
Example user:
|
|
352
|
-
|
|
353
276
|
```javascript
|
|
354
|
-
|
|
355
|
-
id: 10,
|
|
356
|
-
roles: ["broker"]
|
|
357
|
-
}
|
|
277
|
+
user.role = "admin";
|
|
358
278
|
```
|
|
359
279
|
|
|
360
|
-
|
|
280
|
+
or
|
|
361
281
|
|
|
362
282
|
```javascript
|
|
363
|
-
|
|
364
|
-
id: 5,
|
|
365
|
-
roles: ["user","editor"]
|
|
366
|
-
}
|
|
283
|
+
user.roles = ["user", "editor"];
|
|
367
284
|
```
|
|
368
285
|
|
|
369
|
-
|
|
286
|
+
Access is granted if **any role matches**.
|
|
370
287
|
|
|
371
288
|
---
|
|
372
289
|
|
|
373
290
|
# Authorization API
|
|
374
291
|
|
|
375
|
-
## Load policy
|
|
292
|
+
## Load policy
|
|
376
293
|
|
|
377
294
|
```javascript
|
|
378
|
-
authz.load("./authz.toon")
|
|
295
|
+
authz.load("./authz.toon");
|
|
379
296
|
```
|
|
380
297
|
|
|
381
298
|
---
|
|
@@ -383,97 +300,37 @@ authz.load("./authz.toon")
|
|
|
383
300
|
## Check permission
|
|
384
301
|
|
|
385
302
|
```javascript
|
|
386
|
-
authz.can(user, action, resource,
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
Example:
|
|
390
|
-
|
|
391
|
-
```javascript
|
|
392
|
-
authz.can(user,"edit","listing",listing)
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
Returns:
|
|
396
|
-
|
|
397
|
-
```
|
|
398
|
-
true
|
|
399
|
-
false
|
|
303
|
+
authz.can(user, action, resource, data);
|
|
400
304
|
```
|
|
401
305
|
|
|
402
306
|
---
|
|
403
307
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
## Example: Listing System
|
|
407
|
-
|
|
408
|
-
User:
|
|
308
|
+
## Explain decision
|
|
409
309
|
|
|
410
310
|
```javascript
|
|
411
|
-
|
|
412
|
-
id: 10,
|
|
413
|
-
roles: ["broker"]
|
|
414
|
-
}
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
Listing:
|
|
418
|
-
|
|
419
|
-
```javascript
|
|
420
|
-
const listing = {
|
|
421
|
-
owner_id: 10
|
|
422
|
-
}
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
Permission check:
|
|
426
|
-
|
|
427
|
-
```javascript
|
|
428
|
-
authz.can(user,"edit","listing",listing)
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
Result:
|
|
432
|
-
|
|
433
|
-
```
|
|
434
|
-
true
|
|
311
|
+
authz.explain(user, action, resource, data);
|
|
435
312
|
```
|
|
436
313
|
|
|
437
314
|
---
|
|
438
315
|
|
|
439
|
-
# Example
|
|
440
|
-
|
|
441
|
-
Example route protection:
|
|
316
|
+
# Example Integration
|
|
442
317
|
|
|
443
318
|
```javascript
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
req.user,
|
|
448
|
-
"edit",
|
|
449
|
-
"listing",
|
|
450
|
-
req.listing
|
|
451
|
-
)
|
|
452
|
-
|
|
453
|
-
if(!allowed){
|
|
454
|
-
return res.status(403).send("Access denied")
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
updateListing()
|
|
458
|
-
})
|
|
319
|
+
if (!authz.can(req.user, "edit", "listing", req.listing)) {
|
|
320
|
+
return res.status(403).send("Access denied");
|
|
321
|
+
}
|
|
459
322
|
```
|
|
460
323
|
|
|
461
324
|
---
|
|
462
325
|
|
|
463
326
|
# Project Structure Example
|
|
464
327
|
|
|
465
|
-
Recommended project layout:
|
|
466
|
-
|
|
467
328
|
```
|
|
468
|
-
project
|
|
469
|
-
|
|
470
|
-
├── policies
|
|
329
|
+
project/
|
|
330
|
+
├── policies/
|
|
471
331
|
│ └── authz.toon
|
|
472
|
-
|
|
473
|
-
├── src
|
|
474
|
-
│
|
|
332
|
+
├── src/
|
|
475
333
|
├── app.js
|
|
476
|
-
│
|
|
477
334
|
└── package.json
|
|
478
335
|
```
|
|
479
336
|
|
|
@@ -481,67 +338,57 @@ project
|
|
|
481
338
|
|
|
482
339
|
# Performance
|
|
483
340
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
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
|
|
489
345
|
|
|
490
346
|
---
|
|
491
347
|
|
|
492
348
|
# Security Model
|
|
493
349
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
Meaning:
|
|
497
|
-
|
|
498
|
-
If no rule allows an action, access is automatically denied.
|
|
350
|
+
**Deny by default**
|
|
499
351
|
|
|
500
352
|
```
|
|
501
353
|
no rule → deny
|
|
502
354
|
rule exists → allow
|
|
503
355
|
```
|
|
504
356
|
|
|
505
|
-
This reduces the risk of unintended access.
|
|
506
|
-
|
|
507
357
|
---
|
|
508
358
|
|
|
509
359
|
# Roadmap
|
|
510
360
|
|
|
511
|
-
|
|
361
|
+
### v1.2
|
|
512
362
|
|
|
513
|
-
*
|
|
514
|
-
* Role
|
|
515
|
-
*
|
|
516
|
-
* Middleware helpers
|
|
517
|
-
* Permission caching
|
|
518
|
-
* Modular policy files
|
|
519
|
-
* Distributed policy storage
|
|
363
|
+
* CLI tool
|
|
364
|
+
* Role hierarchy
|
|
365
|
+
* Modular policies
|
|
520
366
|
|
|
521
|
-
|
|
367
|
+
### v2.0
|
|
522
368
|
|
|
523
|
-
|
|
369
|
+
* Advanced ABAC
|
|
370
|
+
* Distributed policies
|
|
371
|
+
* Policy versioning
|
|
524
372
|
|
|
525
|
-
|
|
373
|
+
---
|
|
526
374
|
|
|
527
|
-
|
|
375
|
+
# Contributing
|
|
528
376
|
|
|
529
|
-
1. Fork
|
|
530
|
-
2. Create
|
|
531
|
-
3.
|
|
532
|
-
4. Open a pull request
|
|
377
|
+
1. Fork repository
|
|
378
|
+
2. Create branch
|
|
379
|
+
3. Submit PR
|
|
533
380
|
|
|
534
381
|
---
|
|
535
382
|
|
|
536
383
|
# License
|
|
537
384
|
|
|
538
|
-
|
|
385
|
+
Apache 2.0
|
|
539
386
|
|
|
540
387
|
---
|
|
541
388
|
|
|
542
|
-
#
|
|
389
|
+
# Keywords
|
|
543
390
|
|
|
544
|
-
|
|
391
|
+
authorization, rbac, abac, nodejs, security, policy-engine
|
|
545
392
|
|
|
393
|
+
````
|
|
546
394
|
|
|
547
|
-
# 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-v1.0.1.tgz
DELETED
|
Binary file
|