simple-authz 1.0.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 +503 -0
- package/lib/authz.rules +6 -0
- package/lib/core/authz.js +81 -0
- package/lib/engine/conditionCompiler.js +57 -0
- package/lib/engine/conditionExecutor.js +64 -0
- package/lib/index.js +3 -0
- package/lib/rules/compiler.js +36 -0
- package/lib/rules/parser.js +41 -0
- package/lib/rules/validator.js +50 -0
- package/lib/utils/normalizeUser.js +10 -0
- package/package.json +17 -0
- package/simple-authz-v1.0.0.tgz +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
|
|
2
|
+
````markdown
|
|
3
|
+
# Simple Authz
|
|
4
|
+
|
|
5
|
+
A lightweight and flexible **authorization engine for Node.js** built around a simple policy language called **TOON (Token-Oriented Object Notation)**.
|
|
6
|
+
|
|
7
|
+
Simple Authz allows developers to **centralize authorization logic** using policy files instead of scattering permission checks across application code.
|
|
8
|
+
|
|
9
|
+
The goal of this project is to make authorization **simple, fast, readable, and scalable**.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Table of Contents
|
|
14
|
+
|
|
15
|
+
- Overview
|
|
16
|
+
- Why Simple Authz
|
|
17
|
+
- Key Features
|
|
18
|
+
- Installation
|
|
19
|
+
- Quick Start
|
|
20
|
+
- Core Concepts
|
|
21
|
+
- Policy Language (TOON)
|
|
22
|
+
- Policy Examples
|
|
23
|
+
- Using Conditions
|
|
24
|
+
- Working with Roles
|
|
25
|
+
- Authorization API
|
|
26
|
+
- Example Integrations
|
|
27
|
+
- Project Structure
|
|
28
|
+
- Performance
|
|
29
|
+
- Security Model
|
|
30
|
+
- Roadmap
|
|
31
|
+
- Contributing
|
|
32
|
+
- License
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# Overview
|
|
37
|
+
|
|
38
|
+
Most applications implement authorization like this:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
if(user.role === "admin") { ... }
|
|
42
|
+
if(user.id === listing.owner_id) { ... }
|
|
43
|
+
if(user.permissions.includes("edit_listing")) { ... }
|
|
44
|
+
````
|
|
45
|
+
|
|
46
|
+
Over time this logic spreads across many files and becomes difficult to maintain.
|
|
47
|
+
|
|
48
|
+
**Simple Authz solves this by moving authorization rules into policy files.**
|
|
49
|
+
|
|
50
|
+
Example policy file:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
allow admin *
|
|
54
|
+
|
|
55
|
+
allow user view listing
|
|
56
|
+
|
|
57
|
+
allow broker publish listing
|
|
58
|
+
|
|
59
|
+
allow broker edit listing
|
|
60
|
+
when listing.owner_id == user.id
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Your application then only needs to ask:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
authz.can(user, "edit", "listing", listing)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
# Why Simple Authz
|
|
72
|
+
|
|
73
|
+
Benefits of centralized authorization:
|
|
74
|
+
|
|
75
|
+
* Clear separation between **business logic and security rules**
|
|
76
|
+
* Easy permission updates without touching application code
|
|
77
|
+
* Better maintainability for large projects
|
|
78
|
+
* Safer and more predictable access control
|
|
79
|
+
* Easier onboarding for new developers
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
# Key Features
|
|
84
|
+
|
|
85
|
+
* Simple and readable **policy language**
|
|
86
|
+
* Fast permission lookup
|
|
87
|
+
* Support for **multiple user roles**
|
|
88
|
+
* Logical **conditional rules**
|
|
89
|
+
* Policy **validation**
|
|
90
|
+
* Policy **AST compilation**
|
|
91
|
+
* Lightweight with minimal dependencies
|
|
92
|
+
* Works with any Node.js framework
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
# Installation
|
|
97
|
+
|
|
98
|
+
Install using npm:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm install simple-authz
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
# Quick Start
|
|
107
|
+
|
|
108
|
+
## 1. Import the library
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const Authz = require("simple-authz")
|
|
112
|
+
|
|
113
|
+
const authz = new Authz()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 2. Create a policy file
|
|
119
|
+
|
|
120
|
+
Create a file called:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
authz.toon
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Example policy:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
allow admin *
|
|
130
|
+
|
|
131
|
+
allow user view listing
|
|
132
|
+
|
|
133
|
+
allow broker publish listing
|
|
134
|
+
|
|
135
|
+
allow broker edit listing
|
|
136
|
+
when listing.owner_id == user.id
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 3. Load the policy
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
authz.load("./authz.toon")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 4. Check permissions
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
const allowed = authz.can(user, "edit", "listing", listing)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Example usage:
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
if(authz.can(user,"edit","listing",listing)){
|
|
159
|
+
updateListing()
|
|
160
|
+
}else{
|
|
161
|
+
throw new Error("Access denied")
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
# Core Concepts
|
|
168
|
+
|
|
169
|
+
Simple Authz follows a common authorization model:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
Subject → Action → Resource
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Component | Description |
|
|
176
|
+
| --------- | ------------------------------ |
|
|
177
|
+
| Subject | The user performing the action |
|
|
178
|
+
| Action | What the user wants to do |
|
|
179
|
+
| Resource | The object being accessed |
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
broker → edit → listing
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
# Policy Language (TOON)
|
|
190
|
+
|
|
191
|
+
Policies are written in **TOON format**.
|
|
192
|
+
|
|
193
|
+
Basic rule syntax:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
allow <role> <action> <resource>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
allow admin *
|
|
203
|
+
allow user view listing
|
|
204
|
+
allow broker publish listing
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
# Wildcard Permissions
|
|
210
|
+
|
|
211
|
+
You can use `*` to allow everything.
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
allow admin *
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
This allows the admin role to perform **any action on any resource**.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
# Conditional Rules
|
|
224
|
+
|
|
225
|
+
Policies can include conditions.
|
|
226
|
+
|
|
227
|
+
Syntax:
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
allow <role> <action> <resource>
|
|
231
|
+
when <condition>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
allow broker edit listing
|
|
238
|
+
when listing.owner_id == user.id
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Meaning:
|
|
242
|
+
|
|
243
|
+
A broker can only edit listings they own.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
# Condition Variables
|
|
248
|
+
|
|
249
|
+
Conditions can reference:
|
|
250
|
+
|
|
251
|
+
| Variable | Description |
|
|
252
|
+
| -------- | --------------------- |
|
|
253
|
+
| user | authenticated user |
|
|
254
|
+
| resource | target resource |
|
|
255
|
+
| context | optional request data |
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
allow user edit profile
|
|
261
|
+
when user.id == resource.id
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
# Policy Examples
|
|
267
|
+
|
|
268
|
+
## Example 1 – Basic Roles
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
allow admin *
|
|
272
|
+
|
|
273
|
+
allow user view listing
|
|
274
|
+
|
|
275
|
+
allow broker publish listing
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Example 2 – Ownership Rules
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
allow broker edit listing
|
|
284
|
+
when listing.owner_id == user.id
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Example 3 – Multiple Rules
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
allow admin *
|
|
293
|
+
|
|
294
|
+
allow user view listing
|
|
295
|
+
|
|
296
|
+
allow broker publish listing
|
|
297
|
+
|
|
298
|
+
allow broker edit listing
|
|
299
|
+
when listing.owner_id == user.id
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
# Working with Roles
|
|
305
|
+
|
|
306
|
+
Users can have one or multiple roles.
|
|
307
|
+
|
|
308
|
+
Example user:
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
const user = {
|
|
312
|
+
id: 10,
|
|
313
|
+
roles: ["broker"]
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Multiple roles:
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
const user = {
|
|
321
|
+
id: 5,
|
|
322
|
+
roles: ["user","editor"]
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The engine checks permissions against **all roles**.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
# Authorization API
|
|
331
|
+
|
|
332
|
+
## Load policy file
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
authz.load("./authz.toon")
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Check permission
|
|
341
|
+
|
|
342
|
+
```javascript
|
|
343
|
+
authz.can(user, action, resource, object)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
|
|
348
|
+
```javascript
|
|
349
|
+
authz.can(user,"edit","listing",listing)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
true
|
|
356
|
+
false
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
# Example Integration
|
|
362
|
+
|
|
363
|
+
## Example: Listing System
|
|
364
|
+
|
|
365
|
+
User:
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
const user = {
|
|
369
|
+
id: 10,
|
|
370
|
+
roles: ["broker"]
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Listing:
|
|
375
|
+
|
|
376
|
+
```javascript
|
|
377
|
+
const listing = {
|
|
378
|
+
owner_id: 10
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Permission check:
|
|
383
|
+
|
|
384
|
+
```javascript
|
|
385
|
+
authz.can(user,"edit","listing",listing)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Result:
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
true
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
# Example: Express.js Integration
|
|
397
|
+
|
|
398
|
+
Example route protection:
|
|
399
|
+
|
|
400
|
+
```javascript
|
|
401
|
+
app.put("/listing/:id", async (req,res)=>{
|
|
402
|
+
|
|
403
|
+
const allowed = authz.can(
|
|
404
|
+
req.user,
|
|
405
|
+
"edit",
|
|
406
|
+
"listing",
|
|
407
|
+
req.listing
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if(!allowed){
|
|
411
|
+
return res.status(403).send("Access denied")
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
updateListing()
|
|
415
|
+
})
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
# Project Structure Example
|
|
421
|
+
|
|
422
|
+
Recommended project layout:
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
project
|
|
426
|
+
│
|
|
427
|
+
├── policies
|
|
428
|
+
│ └── authz.toon
|
|
429
|
+
│
|
|
430
|
+
├── src
|
|
431
|
+
│
|
|
432
|
+
├── app.js
|
|
433
|
+
│
|
|
434
|
+
└── package.json
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
# Performance
|
|
440
|
+
|
|
441
|
+
Policies are compiled into an **internal permission index** for fast lookups.
|
|
442
|
+
|
|
443
|
+
Authorization checks are optimized to avoid scanning all rules.
|
|
444
|
+
|
|
445
|
+
This allows the engine to remain efficient even as policy files grow.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
# Security Model
|
|
450
|
+
|
|
451
|
+
Default behavior follows the **deny-by-default principle**.
|
|
452
|
+
|
|
453
|
+
Meaning:
|
|
454
|
+
|
|
455
|
+
If no rule allows an action, access is automatically denied.
|
|
456
|
+
|
|
457
|
+
```
|
|
458
|
+
no rule → deny
|
|
459
|
+
rule exists → allow
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
This reduces the risk of unintended access.
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
# Roadmap
|
|
467
|
+
|
|
468
|
+
Future versions may include:
|
|
469
|
+
|
|
470
|
+
* Policy hot reloading
|
|
471
|
+
* Role inheritance
|
|
472
|
+
* Policy debugger
|
|
473
|
+
* Middleware helpers
|
|
474
|
+
* Permission caching
|
|
475
|
+
* Modular policy files
|
|
476
|
+
* Distributed policy storage
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
# Contributing
|
|
481
|
+
|
|
482
|
+
Contributions are welcome.
|
|
483
|
+
|
|
484
|
+
Steps:
|
|
485
|
+
|
|
486
|
+
1. Fork the repository
|
|
487
|
+
2. Create a feature branch
|
|
488
|
+
3. Commit changes
|
|
489
|
+
4. Open a pull request
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
# License
|
|
494
|
+
|
|
495
|
+
MIT License
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
# Author
|
|
500
|
+
|
|
501
|
+
Simple Authz was created to simplify authorization logic in modern Node.js applications.
|
|
502
|
+
|
|
503
|
+
|
package/lib/authz.rules
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { parseRules } = require("../rules/parser")
|
|
2
|
+
const { compileRules } = require("../rules/compiler")
|
|
3
|
+
const { validateRules } = require("../rules/validator")
|
|
4
|
+
const { normalizeUser } = require("../utils/normalizeUser")
|
|
5
|
+
const { evaluateAST } = require("../engine/conditionExecutor")
|
|
6
|
+
|
|
7
|
+
class Authz {
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.permissions = {}
|
|
11
|
+
this.conditions = {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
load(filePath) {
|
|
15
|
+
|
|
16
|
+
const rules = parseRules(filePath)
|
|
17
|
+
validateRules(rules)
|
|
18
|
+
|
|
19
|
+
const compiled = compileRules(rules)
|
|
20
|
+
|
|
21
|
+
this.permissions = compiled.permissions
|
|
22
|
+
this.conditions = compiled.conditions
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
can(user, action, resourceType, data = {}) {
|
|
26
|
+
|
|
27
|
+
const roles = normalizeUser(user)
|
|
28
|
+
|
|
29
|
+
for (const role of roles) {
|
|
30
|
+
|
|
31
|
+
const rolePerm = this.permissions[role]
|
|
32
|
+
|
|
33
|
+
if (rolePerm) {
|
|
34
|
+
|
|
35
|
+
const resourcePerm = rolePerm[resourceType]
|
|
36
|
+
|
|
37
|
+
if (resourcePerm) {
|
|
38
|
+
|
|
39
|
+
if (resourcePerm.has("*") || resourcePerm.has(action)) {
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (rolePerm["*"]) {
|
|
45
|
+
|
|
46
|
+
if (rolePerm["*"].has("*") || rolePerm["*"].has(action)) {
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const roleCond = this.conditions[role]
|
|
53
|
+
|
|
54
|
+
if (!roleCond) continue
|
|
55
|
+
|
|
56
|
+
const resourceCond = roleCond[resourceType]
|
|
57
|
+
|
|
58
|
+
if (!resourceCond) continue
|
|
59
|
+
|
|
60
|
+
const actionCond = resourceCond[action]
|
|
61
|
+
|
|
62
|
+
if (!actionCond) continue
|
|
63
|
+
|
|
64
|
+
for (const cond of actionCond) {
|
|
65
|
+
|
|
66
|
+
const context = {
|
|
67
|
+
user,
|
|
68
|
+
[resourceType]: data
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (evaluateAST(cond, context)) {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = Authz
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function detectLogical(condition) {
|
|
2
|
+
|
|
3
|
+
if (condition.includes(" AND ")) {
|
|
4
|
+
return {
|
|
5
|
+
operator: "AND",
|
|
6
|
+
parts: condition.split(" AND ")
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (condition.includes(" OR ")) {
|
|
11
|
+
return {
|
|
12
|
+
operator: "OR",
|
|
13
|
+
parts: condition.split(" OR ")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseComparison(expr) {
|
|
21
|
+
|
|
22
|
+
const operators = ["==", "!=", ">=", "<=", ">", "<"]
|
|
23
|
+
|
|
24
|
+
for (const op of operators) {
|
|
25
|
+
|
|
26
|
+
if (expr.includes(op)) {
|
|
27
|
+
|
|
28
|
+
const [left, right] = expr.split(op).map(s => s.trim())
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
type: "comparison",
|
|
32
|
+
operator: op,
|
|
33
|
+
left,
|
|
34
|
+
right
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error(`Invalid condition: ${expr}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function compileCondition(condition) {
|
|
43
|
+
|
|
44
|
+
const logical = detectLogical(condition)
|
|
45
|
+
|
|
46
|
+
if (!logical) {
|
|
47
|
+
return parseComparison(condition)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
type: logical.operator,
|
|
52
|
+
left: parseComparison(logical.parts[0]),
|
|
53
|
+
right: parseComparison(logical.parts[1])
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { compileCondition }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function getValue(path, context) {
|
|
2
|
+
|
|
3
|
+
const parts = path.split(".")
|
|
4
|
+
let value = context
|
|
5
|
+
|
|
6
|
+
for (const p of parts) {
|
|
7
|
+
|
|
8
|
+
if (value === undefined) return undefined
|
|
9
|
+
value = value[p]
|
|
10
|
+
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function evaluateComparison(node, context) {
|
|
17
|
+
|
|
18
|
+
const leftVal = getValue(node.left, context)
|
|
19
|
+
|
|
20
|
+
let rightVal
|
|
21
|
+
|
|
22
|
+
if (node.right.startsWith('"') || node.right.startsWith("'")) {
|
|
23
|
+
rightVal = node.right.slice(1, -1)
|
|
24
|
+
} else {
|
|
25
|
+
rightVal = getValue(node.right, context)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
switch (node.operator) {
|
|
29
|
+
|
|
30
|
+
case "==": return leftVal == rightVal
|
|
31
|
+
case "!=": return leftVal != rightVal
|
|
32
|
+
case ">": return leftVal > rightVal
|
|
33
|
+
case "<": return leftVal < rightVal
|
|
34
|
+
case ">=": return leftVal >= rightVal
|
|
35
|
+
case "<=": return leftVal <= rightVal
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function evaluateAST(node, context) {
|
|
42
|
+
|
|
43
|
+
if (node.type === "comparison") {
|
|
44
|
+
return evaluateComparison(node, context)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (node.type === "AND") {
|
|
48
|
+
return (
|
|
49
|
+
evaluateAST(node.left, context) &&
|
|
50
|
+
evaluateAST(node.right, context)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (node.type === "OR") {
|
|
55
|
+
return (
|
|
56
|
+
evaluateAST(node.left, context) ||
|
|
57
|
+
evaluateAST(node.right, context)
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { evaluateAST }
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { compileCondition } = require("../engine/conditionCompiler")
|
|
2
|
+
|
|
3
|
+
function compileRules(rules) {
|
|
4
|
+
|
|
5
|
+
const permissions = {}
|
|
6
|
+
const conditions = {}
|
|
7
|
+
|
|
8
|
+
for (const rule of rules) {
|
|
9
|
+
|
|
10
|
+
const { role, action, resource, condition } = rule
|
|
11
|
+
|
|
12
|
+
if (condition) {
|
|
13
|
+
|
|
14
|
+
if (!conditions[role]) conditions[role] = {}
|
|
15
|
+
if (!conditions[role][resource]) conditions[role][resource] = {}
|
|
16
|
+
if (!conditions[role][resource][action])
|
|
17
|
+
conditions[role][resource][action] = []
|
|
18
|
+
|
|
19
|
+
const compiledCondition = compileCondition(condition)
|
|
20
|
+
|
|
21
|
+
conditions[role][resource][action].push(compiledCondition)
|
|
22
|
+
|
|
23
|
+
} else {
|
|
24
|
+
|
|
25
|
+
if (!permissions[role]) permissions[role] = {}
|
|
26
|
+
if (!permissions[role][resource])
|
|
27
|
+
permissions[role][resource] = new Set()
|
|
28
|
+
|
|
29
|
+
permissions[role][resource].add(action)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { permissions, conditions }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { compileRules }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
|
|
3
|
+
function parseRules(filePath) {
|
|
4
|
+
|
|
5
|
+
const content = fs.readFileSync(filePath, "utf8")
|
|
6
|
+
|
|
7
|
+
const lines = content.split("\n").map(l => l.trim())
|
|
8
|
+
|
|
9
|
+
const rules = []
|
|
10
|
+
let current = null
|
|
11
|
+
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
|
|
14
|
+
if (!line || line.startsWith("#")) continue
|
|
15
|
+
|
|
16
|
+
if (line === "rule") {
|
|
17
|
+
current = {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
else if (line === "end") {
|
|
21
|
+
rules.push(current)
|
|
22
|
+
current = null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
else if (current) {
|
|
26
|
+
|
|
27
|
+
const [key, ...rest] = line.split(" ")
|
|
28
|
+
const value = rest.join(" ")
|
|
29
|
+
|
|
30
|
+
if (key === "role") current.role = value
|
|
31
|
+
if (key === "action") current.action = value
|
|
32
|
+
if (key === "resource") current.resource = value
|
|
33
|
+
if (key === "condition") current.condition = value
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return rules
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { parseRules }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
function validateRules(rules) {
|
|
2
|
+
|
|
3
|
+
const errors = []
|
|
4
|
+
|
|
5
|
+
rules.forEach((rule, index) => {
|
|
6
|
+
|
|
7
|
+
const ruleNumber = index + 1
|
|
8
|
+
|
|
9
|
+
if (!rule.role) {
|
|
10
|
+
errors.push(`Rule ${ruleNumber}: missing role`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!rule.action) {
|
|
14
|
+
errors.push(`Rule ${ruleNumber}: missing action`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!rule.resource) {
|
|
18
|
+
errors.push(`Rule ${ruleNumber}: missing resource`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (rule.condition) {
|
|
22
|
+
|
|
23
|
+
const validOperators = [
|
|
24
|
+
"==", "!=", ">", "<", ">=", "<="
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const hasOperator = validOperators.some(op =>
|
|
28
|
+
rule.condition.includes(op)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if (!hasOperator) {
|
|
32
|
+
errors.push(
|
|
33
|
+
`Rule ${ruleNumber}: invalid condition "${rule.condition}"`
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (errors.length > 0) {
|
|
41
|
+
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Policy validation failed:\n\n" +
|
|
44
|
+
errors.join("\n")
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { validateRules }
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "simple-authz",
|
|
3
|
+
"version": "v1.0.0",
|
|
4
|
+
"description": "Simple authorization engine with TOON policy language",
|
|
5
|
+
"main": "lib/core/authz.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node bin/test.js",
|
|
8
|
+
"dev": "node lib/core/authz.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [],
|
|
11
|
+
"author": "Dhruvil",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/dhruvil05/simple-authz"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
Binary file
|