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 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
+
@@ -0,0 +1,6 @@
1
+ allow admin *
2
+
3
+ allow broker publish listing
4
+ allow broker feature listing
5
+
6
+ allow user view listing
@@ -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,3 @@
1
+ const Authz = require("./core/authz")
2
+
3
+ module.exports = new Authz()
@@ -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 }
@@ -0,0 +1,10 @@
1
+ function normalizeUser(user) {
2
+
3
+ if (user.roles) return user.roles
4
+
5
+ if (user.role) return [user.role]
6
+
7
+ return []
8
+ }
9
+
10
+ module.exports = { normalizeUser }
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