simple-authz 1.0.5 → 1.2.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 CHANGED
@@ -1,3 +1,5 @@
1
+ ---
2
+
1
3
  # Simple Authz
2
4
 
3
5
  A lightweight and flexible **authorization engine for Node.js** built around a simple policy language called **TOON (Token-Oriented Object Notation)**.
@@ -31,6 +33,7 @@ We recommend testing thoroughly before using it in production.
31
33
  - Using Conditions
32
34
  - Working with Roles
33
35
  - Authorization API
36
+ - Middleware (Express)
34
37
  - Example Integrations
35
38
  - Project Structure
36
39
  - Performance
@@ -55,66 +58,38 @@ Over time this logic spreads across many files and becomes difficult to maintain
55
58
 
56
59
  **Simple Authz solves this by moving authorization rules into policy files.**
57
60
 
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
61
  ---
88
62
 
89
63
  # Why Simple Authz
90
64
 
91
65
  Benefits of centralized authorization:
92
66
 
93
- - Clear separation between **business logic and security rules**
94
- - Easy permission updates without touching application code
95
- - Better maintainability for large projects
96
- - Safer and more predictable access control
97
- - Easier onboarding for new developers
67
+ * Clear separation between **business logic and security rules**
68
+ * Easy permission updates without touching application code
69
+ * Better maintainability for large projects
70
+ * Safer and more predictable access control
71
+ * Easier onboarding for new developers
98
72
 
99
73
  ---
100
74
 
101
75
  # Key Features
102
76
 
103
- - Simple and readable **policy language**
104
- - Fast permission lookup
105
- - Support for **multiple user roles**
106
- - Logical **conditional rules**
107
- - Policy **validation**
108
- - Policy **AST compilation**
109
- - Lightweight with minimal dependencies
110
- - Works with any Node.js framework
77
+ * Simple and readable **policy language (TOON)**
78
+ * Fast permission lookup (compiled rules)
79
+ * Built-in **caching for repeated checks**
80
+ * Support for **multiple user roles**
81
+ * Logical **conditional rules (AND / OR)**
82
+ * Policy **validation**
83
+ * Policy **AST compilation (no eval)**
84
+ * Express middleware support
85
+ * Debugging via `authz.explain()`
86
+ * TypeScript support
87
+ * Lightweight with minimal dependencies
111
88
 
112
89
  ---
113
90
 
114
91
  # Installation
115
92
 
116
- Install using npm:
117
-
118
93
  ```bash
119
94
  npm install simple-authz
120
95
  ```
@@ -126,36 +101,24 @@ npm install simple-authz
126
101
  ## 1. Import the library
127
102
 
128
103
  ```javascript
129
- const Authz = require("simple-authz");
130
-
131
- const authz = new Authz();
104
+ const authz = require("simple-authz");
132
105
  ```
133
106
 
134
107
  ---
135
108
 
136
109
  ## 2. Create a policy file
137
110
 
138
- Create a file called:
139
-
140
111
  ```
141
112
  authz.toon
142
113
  ```
143
114
 
144
- Example policy:
145
-
146
- ```
115
+ ```toon
147
116
  rule
148
117
  role admin
149
118
  action *
150
119
  resource *
151
120
  end
152
121
 
153
- rule
154
- role broker
155
- action publish
156
- resource listing
157
- end
158
-
159
122
  rule
160
123
  role user
161
124
  action edit
@@ -177,34 +140,12 @@ authz.load("./authz.toon");
177
140
  ## 4. Check permissions
178
141
 
179
142
  ```javascript
180
- const allowed = authz.can(user, "edit", "listing", listing);
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
- }
143
+ authz.can(user, "edit", "listing", listing);
191
144
  ```
192
145
 
193
146
  ---
194
147
 
195
- ## 5. `authz.explain()` (Optional)
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
148
+ ## 5. Debug with `authz.explain()`
208
149
 
209
150
  ```javascript
210
151
  const result = authz.explain(user, "edit", "listing", listing);
@@ -212,7 +153,7 @@ const result = authz.explain(user, "edit", "listing", listing);
212
153
  console.log(result);
213
154
  ```
214
155
 
215
- ### Example Output
156
+ Example output:
216
157
 
217
158
  ```javascript
218
159
  {
@@ -224,207 +165,125 @@ console.log(result);
224
165
  }
225
166
  ```
226
167
 
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
168
  ---
261
169
 
262
- # Policy Language (TOON)
263
-
264
- Policies are written in **TOON format**.
170
+ # Middleware (Express)
265
171
 
266
- Basic rule syntax:
172
+ Protect routes easily:
267
173
 
268
- ```
269
- rule
270
- role <Role>
271
- action <Action>
272
- resource <Resource>
273
- end
274
- ```
174
+ ```javascript
175
+ const express = require("express");
176
+ const authz = require("simple-authz");
275
177
 
276
- Example:
178
+ const app = express();
179
+ app.use(express.json());
277
180
 
278
- ```
279
- rule
280
- role admin
281
- action *
282
- resource *
283
- end
284
-
285
- rule
286
- role broker
287
- action publish
288
- resource listing
289
- end
181
+ authz.load("./authz.toon");
290
182
 
291
- rule
292
- role user
293
- action edit
294
- resource listing
295
- condition listing.owner_id == user.id
296
- end
297
- ```
183
+ app.post(
184
+ "/listing",
185
+ authz.middleware("edit", "listing"),
186
+ (req, res) => {
187
+ res.send("Allowed");
188
+ }
189
+ );
190
+ ```
191
+
192
+ > ⚠️ **Security note:** by default the middleware checks against `req.body`.
193
+ > That's fine for actions where the body *is* the thing being created, but
194
+ > if your policy has conditions like `listing.owner_id == user.id`, a client
195
+ > can put whatever `owner_id` it wants in its own request body and bypass
196
+ > the check. Pass a third argument — an (optionally async) function that
197
+ > loads the **real** resource from your database — whenever a condition
198
+ > depends on data the client shouldn't be trusted to supply:
199
+ >
200
+ > ```javascript
201
+ > app.post(
202
+ > "/listing/:id",
203
+ > authz.middleware("edit", "listing", async (req) => {
204
+ > return await db.listings.findById(req.params.id);
205
+ > }),
206
+ > (req, res) => {
207
+ > res.send("Allowed");
208
+ > }
209
+ > );
210
+ > ```
298
211
 
299
212
  ---
300
213
 
301
- # Wildcard Permissions
302
-
303
- You can use `*` to allow everything.
304
-
305
- Example:
214
+ # Core Concepts
306
215
 
307
216
  ```
308
- rule
309
- role admin
310
- action *
311
- resource *
312
- end
217
+ Subject → Action → Resource
313
218
  ```
314
219
 
315
- This allows the admin role to perform **any action on any resource**.
220
+ | Component | Description |
221
+ | --------- | ------------- |
222
+ | Subject | User |
223
+ | Action | Operation |
224
+ | Resource | Target object |
316
225
 
317
226
  ---
318
227
 
319
- # Conditional Rules
320
-
321
- Policies can include conditions.
228
+ # Policy Language (TOON)
322
229
 
323
- Syntax:
230
+ Basic syntax:
324
231
 
325
232
  ```
326
233
  rule
327
234
  role <role>
328
235
  action <action>
329
236
  resource <resource>
330
- condition <condition1> OR <condition2>
331
- end
332
- ```
333
-
334
- Example:
335
-
336
- ```
337
- rule
338
- role user
339
- action view
340
- resource listing
341
- condition listing.status == "public" OR listing.owner_id == user.id
342
237
  end
343
238
  ```
344
239
 
345
- Meaning:
346
-
347
- A broker can only edit listings they own.
240
+ The parser is strict on purpose: a stray `end`, a `rule` opened twice without
241
+ closing it, an unrecognized key (e.g. a typo'd `roel`), or a key with no
242
+ value all throw a clear error with the line number, instead of silently
243
+ dropping or mis-parsing part of your policy. `authz.load()` runs full
244
+ validation too — every condition is actually compiled during validation, so
245
+ a broken condition (missing operand, mixed `AND`/`OR`, etc.) is reported
246
+ against its rule number at load time rather than failing later, mid-request,
247
+ somewhere inside the evaluator.
348
248
 
349
249
  ---
350
250
 
351
- # Condition Variables
352
-
353
- Conditions can reference:
354
-
355
- | Variable | Description |
356
- | -------- | --------------------- |
357
- | user | authenticated user |
358
- | resource | target resource |
359
- | context | optional request data |
360
-
361
- Example:
251
+ # Wildcards
362
252
 
363
253
  ```
364
- rule
365
- role user
366
- action edit
367
- resource profile
368
- condition user.id == resource.id
369
- end
254
+ action *
255
+ resource *
370
256
  ```
371
257
 
372
258
  ---
373
259
 
374
- # Policy Examples
260
+ # Conditional Rules
375
261
 
376
- ## Example 1 – Basic Roles
262
+ Example:
377
263
 
378
264
  ```
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
265
+ condition listing.owner_id == user.id AND listing.status != "published"
396
266
  ```
397
267
 
398
- ---
268
+ Chains of any length work: `a == 1 AND b == 2 AND c == 3`. You can also
269
+ compare against numbers and booleans directly (`listing.views > 100`), not
270
+ just strings.
399
271
 
400
- ## Example 2 Ownership Rules
272
+ Only one operator type per condition — `AND` and `OR` can't be mixed in the
273
+ same condition (`a == 1 AND b == 2 OR c == 3` throws an error). Split that
274
+ into two separate rules instead.
401
275
 
402
- ```
403
- rule
404
- role broker
405
- action edit
406
- resource listing
407
- condition listing.owner_id == user.id
408
- end
409
- ```
276
+ String literals are quote-safe: a value like
277
+ `doc.title == "Terms AND Conditions"` is treated as one literal, not
278
+ accidentally split on the "AND" inside it.
410
279
 
411
280
  ---
412
281
 
413
- ## Example 3 – Multiple Rules
414
-
415
- ```
416
- rule
417
- role admin
418
- action *
419
- resource *
420
- end
282
+ # Policy Examples
421
283
 
422
- rule
423
- role broker
424
- action publish
425
- resource listing
426
- end
284
+ ### Ownership Rule
427
285
 
286
+ ```
428
287
  rule
429
288
  role user
430
289
  action edit
@@ -437,33 +296,23 @@ end
437
296
 
438
297
  # Working with Roles
439
298
 
440
- Users can have one or multiple roles.
441
-
442
- Example user:
443
-
444
299
  ```javascript
445
- const user = {
446
- id: 10,
447
- roles: ["broker"],
448
- };
300
+ user.role = "admin";
449
301
  ```
450
302
 
451
- Multiple roles:
303
+ or
452
304
 
453
305
  ```javascript
454
- const user = {
455
- id: 5,
456
- roles: ["user", "editor"],
457
- };
306
+ user.roles = ["user", "editor"];
458
307
  ```
459
308
 
460
- The engine checks permissions against **all roles**.
309
+ Access is granted if **any role matches**.
461
310
 
462
311
  ---
463
312
 
464
313
  # Authorization API
465
314
 
466
- ## Load policy file
315
+ ## Load policy
467
316
 
468
317
  ```javascript
469
318
  authz.load("./authz.toon");
@@ -474,91 +323,37 @@ authz.load("./authz.toon");
474
323
  ## Check permission
475
324
 
476
325
  ```javascript
477
- authz.can(user, action, resource, object);
478
- ```
479
-
480
- Example:
481
-
482
- ```javascript
483
- authz.can(user, "edit", "listing", listing);
484
- ```
485
-
486
- Returns:
487
-
488
- ```
489
- true
490
- false
326
+ authz.can(user, action, resource, data);
491
327
  ```
492
328
 
493
329
  ---
494
330
 
495
- # Example Integration
496
-
497
- ## Example: Listing System
498
-
499
- User:
500
-
501
- ```javascript
502
- const user = {
503
- id: 10,
504
- roles: ["broker"],
505
- };
506
- ```
507
-
508
- Listing:
331
+ ## Explain decision
509
332
 
510
333
  ```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
334
+ authz.explain(user, action, resource, data);
526
335
  ```
527
336
 
528
337
  ---
529
338
 
530
- # Example: Express.js Integration
531
-
532
- Example route protection:
339
+ # Example Integration
533
340
 
534
341
  ```javascript
535
- app.put("/listing/:id", async (req, res) => {
536
- const allowed = authz.can(req.user, "edit", "listing", req.listing);
537
-
538
- if (!allowed) {
539
- return res.status(403).send("Access denied");
540
- }
541
-
542
- updateListing();
543
- });
342
+ if (!authz.can(req.user, "edit", "listing", req.listing)) {
343
+ return res.status(403).send("Access denied");
344
+ }
544
345
  ```
545
346
 
546
347
  ---
547
348
 
548
349
  # Project Structure Example
549
350
 
550
- Recommended project layout:
551
-
552
351
  ```
553
- project
554
-
555
- ├── policies
352
+ project/
353
+ ├── policies/
556
354
  │ └── authz.toon
557
-
558
- ├── src
559
-
355
+ ├── src/
560
356
  ├── app.js
561
-
562
357
  └── package.json
563
358
  ```
564
359
 
@@ -566,66 +361,75 @@ project
566
361
 
567
362
  # Performance
568
363
 
569
- Policies are compiled into an **internal permission index** for fast lookups.
364
+ * Rules compiled into indexed structure
365
+ * AST-based condition evaluation
366
+ * O(1) permission lookup
367
+ * Built-in LRU cache with TTL-based expiration, keyed by user + action +
368
+ resource + object id (falls back to a data snapshot if there's no id)
570
369
 
571
- Authorization checks are optimized to avoid scanning all rules.
370
+ **Caching caveat:** because the cache keys on object *identity* (id/owner_id),
371
+ if a resource's *other* fields change while its id stays the same (e.g. a
372
+ document gets locked) within the TTL window, a stale decision can be served.
373
+ Call `authz.clearCache()` right after any write that could flip a condition's
374
+ outcome, or lower `cacheTTL` / set `cacheEnabled: false` for policies with
375
+ conditions on fast-changing fields:
572
376
 
573
- This allows the engine to remain efficient even as policy files grow.
377
+ ```javascript
378
+ const authz = new Authz({ cacheEnabled: true, cacheSize: 1000, cacheTTL: 60 });
379
+ ```
574
380
 
575
381
  ---
576
382
 
577
- # Security Model
383
+ # CLI
578
384
 
579
- Default behavior follows the **deny-by-default principle**.
385
+ ```bash
386
+ npx simple-authz validate ./policies/authz.toon
387
+ npx simple-authz check ./policies/authz.toon --user '{"id":1,"roles":["user"]}' --action edit --resource listing --context '{"owner_id":1}'
388
+ npx simple-authz benchmark ./policies/authz.toon --iterations 10000
389
+ ```
580
390
 
581
- Meaning:
391
+ * `validate` — parses and validates a policy file, lists roles found
392
+ * `check` — runs `authz.explain()` against a policy and prints the decision
393
+ * `benchmark` — runs repeated checks and prints throughput + cache stats
582
394
 
583
- If no rule allows an action, access is automatically denied.
395
+ ---
396
+
397
+ # Security Model
398
+
399
+ **Deny by default**
584
400
 
585
401
  ```
586
402
  no rule → deny
587
403
  rule exists → allow
588
404
  ```
589
405
 
590
- This reduces the risk of unintended access.
591
-
592
406
  ---
593
407
 
594
408
  # Roadmap
595
409
 
596
- Future versions may include:
410
+ ### v1.2
597
411
 
598
- - Policy hot reloading
599
- - Role inheritance
600
- - Policy debugger
601
- - Middleware helpers
602
- - Permission caching
603
- - Modular policy files
604
- - Distributed policy storage
412
+ * Role hierarchy
413
+ * Modular policies
605
414
 
606
- ---
415
+ ### v2.0
607
416
 
608
- # Contributing
609
-
610
- Contributions are welcome.
611
-
612
- Steps:
613
-
614
- 1. Fork the repository
615
- 2. Create a feature branch
616
- 3. Commit changes
617
- 4. Open a pull request
417
+ * Advanced ABAC
418
+ * Distributed policies
419
+ * Policy versioning
618
420
 
619
421
  ---
620
422
 
621
- # License
423
+ # Contributing
622
424
 
623
- This project is licensed under the [Apache 2.0 license](LICENSE).
425
+ 1. Fork repository
426
+ 2. Create branch
427
+ 3. Submit PR
624
428
 
625
429
  ---
626
430
 
627
- # Author
431
+ # License
628
432
 
629
- Simple Authz was created to simplify authorization logic in modern Node.js applications.
433
+ Apache 2.0
630
434
 
631
- # Keywords
435
+ ---