@autorix/express 0.1.1 → 0.1.2
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 +55 -8
- package/dist/index.cjs +25 -4
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +25 -4
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ const policyProvider = new MemoryPolicyProvider();
|
|
|
46
46
|
|
|
47
47
|
// Create enforcer
|
|
48
48
|
const enforcer = {
|
|
49
|
-
can: async (input: { action: string;
|
|
49
|
+
can: async (input: { action: string; resource: string; context: any }) => {
|
|
50
50
|
// ✅ IMPORTANT: If your app allows unauthenticated requests, guard here
|
|
51
51
|
// to avoid storage/providers crashing when principal is null.
|
|
52
52
|
if (!input.context?.principal) {
|
|
@@ -62,8 +62,8 @@ const enforcer = {
|
|
|
62
62
|
const result = evaluateAll({
|
|
63
63
|
policies: policies.map(p => p.document),
|
|
64
64
|
action: input.action,
|
|
65
|
-
resource: input.resource,
|
|
66
|
-
|
|
65
|
+
resource: input.resource, // ← String for matching (e.g., 'post/123')
|
|
66
|
+
ctx: input.context, // ← Contains resource object in ctx.resource
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
return { allowed: result.allowed, reason: result.reason };
|
|
@@ -209,6 +209,49 @@ authorize({
|
|
|
209
209
|
})
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
+
## 🔑 Understanding Resources
|
|
213
|
+
|
|
214
|
+
Autorix uses **two separate concepts** for resources:
|
|
215
|
+
|
|
216
|
+
1. **Resource String** (for pattern matching in policies)
|
|
217
|
+
- Format: `type/id` or `type/*`
|
|
218
|
+
- Used in policy `Resource` field
|
|
219
|
+
- Example: `'post/123'`, `'document/*'`
|
|
220
|
+
|
|
221
|
+
2. **Resource Object** (for attribute-based conditions)
|
|
222
|
+
- Contains resource properties/attributes
|
|
223
|
+
- Used in policy `Condition` blocks
|
|
224
|
+
- Example: `{ type: 'post', id: '123', authorId: 'u1' }`
|
|
225
|
+
|
|
226
|
+
**How they work together:**
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Policy matches by string pattern
|
|
230
|
+
Statement: [{
|
|
231
|
+
Effect: 'Allow',
|
|
232
|
+
Action: ['post:delete'],
|
|
233
|
+
Resource: 'post/*', // ← Matches 'post/123'
|
|
234
|
+
Condition: {
|
|
235
|
+
StringEquals: {
|
|
236
|
+
'resource.authorId': '${principal.id}' // ← Uses resource object
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}]
|
|
240
|
+
|
|
241
|
+
// Middleware automatically handles both
|
|
242
|
+
authorize({
|
|
243
|
+
action: 'post:delete',
|
|
244
|
+
resource: {
|
|
245
|
+
type: 'post',
|
|
246
|
+
idFrom: (req) => req.params.id, // → Builds string: 'post/123'
|
|
247
|
+
loader: async (id) => {
|
|
248
|
+
const post = await db.posts.findById(id);
|
|
249
|
+
return { authorId: post.authorId }; // → Object for conditions
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
```
|
|
254
|
+
|
|
212
255
|
## 🎯 Usage Examples
|
|
213
256
|
|
|
214
257
|
### Role-Based Access Control (RBAC)
|
|
@@ -253,16 +296,16 @@ app.get('/admin/settings',
|
|
|
253
296
|
### Attribute-Based Access Control (ABAC)
|
|
254
297
|
|
|
255
298
|
```typescript
|
|
256
|
-
// Policy with conditions
|
|
299
|
+
// Policy with conditions - checks resource attributes
|
|
257
300
|
await provider.setPolicy('owner-only', {
|
|
258
301
|
Version: '2024-01-01',
|
|
259
302
|
Statement: [{
|
|
260
303
|
Effect: 'Allow',
|
|
261
304
|
Action: ['post:update', 'post:delete'],
|
|
262
|
-
Resource: ['post/*'],
|
|
305
|
+
Resource: ['post/*'], // ← Matches 'post/123' string pattern
|
|
263
306
|
Condition: {
|
|
264
307
|
StringEquals: {
|
|
265
|
-
'
|
|
308
|
+
'resource.authorId': '${principal.id}' // ← Checks resource object property
|
|
266
309
|
}
|
|
267
310
|
}
|
|
268
311
|
}]
|
|
@@ -277,12 +320,16 @@ app.put('/posts/:id',
|
|
|
277
320
|
idFrom: (req) => req.params.id,
|
|
278
321
|
loader: async (id) => {
|
|
279
322
|
const post = await db.posts.findById(id);
|
|
280
|
-
|
|
323
|
+
// Return object with properties for conditions
|
|
324
|
+
return {
|
|
325
|
+
authorId: post.authorId, // ← This becomes resource.authorId in conditions
|
|
326
|
+
status: post.status
|
|
327
|
+
};
|
|
281
328
|
}
|
|
282
329
|
}
|
|
283
330
|
}),
|
|
284
331
|
(req, res) => {
|
|
285
|
-
// Only post
|
|
332
|
+
// Only post author can update
|
|
286
333
|
res.json({ message: 'Post updated!' });
|
|
287
334
|
}
|
|
288
335
|
);
|
package/dist/index.cjs
CHANGED
|
@@ -125,17 +125,31 @@ function autorixExpress(opts) {
|
|
|
125
125
|
const context = await buildRequestContext(req, opts);
|
|
126
126
|
req.autorix = {
|
|
127
127
|
context,
|
|
128
|
-
can: /* @__PURE__ */ __name(async (action,
|
|
128
|
+
can: /* @__PURE__ */ __name(async (action, resourceObj, ctxExtra) => {
|
|
129
|
+
let resourceString = "*";
|
|
130
|
+
if (typeof resourceObj === "object" && resourceObj && "type" in resourceObj) {
|
|
131
|
+
const resType = resourceObj.type;
|
|
132
|
+
const resId = resourceObj.id;
|
|
133
|
+
resourceString = resId ? `${resType}/${resId}` : `${resType}/*`;
|
|
134
|
+
} else if (typeof resourceObj === "string") {
|
|
135
|
+
resourceString = resourceObj;
|
|
136
|
+
} else if (resourceObj === void 0) {
|
|
137
|
+
const parts = action.split(":");
|
|
138
|
+
if (parts.length > 1) {
|
|
139
|
+
resourceString = `${parts[0]}/*`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
129
142
|
const decision = await opts.enforcer.can({
|
|
130
143
|
action,
|
|
144
|
+
resource: resourceString,
|
|
131
145
|
context: {
|
|
132
146
|
...context,
|
|
147
|
+
resource: resourceObj,
|
|
133
148
|
attributes: {
|
|
134
149
|
...context.attributes ?? {},
|
|
135
150
|
...ctxExtra ?? {}
|
|
136
151
|
}
|
|
137
|
-
}
|
|
138
|
-
resource
|
|
152
|
+
}
|
|
139
153
|
});
|
|
140
154
|
opts.onDecision?.({
|
|
141
155
|
...decision,
|
|
@@ -161,9 +175,16 @@ async function resolveResource(spec, req) {
|
|
|
161
175
|
if (typeof spec === "string") return {
|
|
162
176
|
type: spec
|
|
163
177
|
};
|
|
164
|
-
if ("loader" in spec) {
|
|
178
|
+
if ("loader" in spec && typeof spec.loader === "function" && typeof spec.idFrom === "function") {
|
|
165
179
|
const id = spec.idFrom(req);
|
|
166
180
|
const data = await spec.loader(id, req);
|
|
181
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
182
|
+
return {
|
|
183
|
+
type: spec.type,
|
|
184
|
+
id,
|
|
185
|
+
...data
|
|
186
|
+
};
|
|
187
|
+
}
|
|
167
188
|
return {
|
|
168
189
|
type: spec.type,
|
|
169
190
|
id,
|
package/dist/index.d.cts
CHANGED
|
@@ -17,19 +17,19 @@ type AutorixRequestContext = {
|
|
|
17
17
|
type ResourceSpec = string | {
|
|
18
18
|
type: string;
|
|
19
19
|
id?: string;
|
|
20
|
-
|
|
20
|
+
[key: string]: unknown;
|
|
21
21
|
} | {
|
|
22
22
|
type: string;
|
|
23
23
|
idFrom: (req: Request) => string;
|
|
24
|
-
loader: (id: string, req: Request) => Promise<unknown
|
|
24
|
+
loader: (id: string, req: Request) => Promise<Record<string, unknown>>;
|
|
25
25
|
};
|
|
26
26
|
type GetContextFn = (req: Request) => Partial<AutorixRequestContext> | Promise<Partial<AutorixRequestContext>>;
|
|
27
27
|
type AutorixExpressOptions = {
|
|
28
28
|
enforcer: {
|
|
29
29
|
can: (input: {
|
|
30
30
|
action: string;
|
|
31
|
+
resource: string;
|
|
31
32
|
context: AutorixRequestContext;
|
|
32
|
-
resource?: unknown;
|
|
33
33
|
}) => Promise<{
|
|
34
34
|
allowed: boolean;
|
|
35
35
|
reason?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -17,19 +17,19 @@ type AutorixRequestContext = {
|
|
|
17
17
|
type ResourceSpec = string | {
|
|
18
18
|
type: string;
|
|
19
19
|
id?: string;
|
|
20
|
-
|
|
20
|
+
[key: string]: unknown;
|
|
21
21
|
} | {
|
|
22
22
|
type: string;
|
|
23
23
|
idFrom: (req: Request) => string;
|
|
24
|
-
loader: (id: string, req: Request) => Promise<unknown
|
|
24
|
+
loader: (id: string, req: Request) => Promise<Record<string, unknown>>;
|
|
25
25
|
};
|
|
26
26
|
type GetContextFn = (req: Request) => Partial<AutorixRequestContext> | Promise<Partial<AutorixRequestContext>>;
|
|
27
27
|
type AutorixExpressOptions = {
|
|
28
28
|
enforcer: {
|
|
29
29
|
can: (input: {
|
|
30
30
|
action: string;
|
|
31
|
+
resource: string;
|
|
31
32
|
context: AutorixRequestContext;
|
|
32
|
-
resource?: unknown;
|
|
33
33
|
}) => Promise<{
|
|
34
34
|
allowed: boolean;
|
|
35
35
|
reason?: string;
|
package/dist/index.js
CHANGED
|
@@ -92,17 +92,31 @@ function autorixExpress(opts) {
|
|
|
92
92
|
const context = await buildRequestContext(req, opts);
|
|
93
93
|
req.autorix = {
|
|
94
94
|
context,
|
|
95
|
-
can: /* @__PURE__ */ __name(async (action,
|
|
95
|
+
can: /* @__PURE__ */ __name(async (action, resourceObj, ctxExtra) => {
|
|
96
|
+
let resourceString = "*";
|
|
97
|
+
if (typeof resourceObj === "object" && resourceObj && "type" in resourceObj) {
|
|
98
|
+
const resType = resourceObj.type;
|
|
99
|
+
const resId = resourceObj.id;
|
|
100
|
+
resourceString = resId ? `${resType}/${resId}` : `${resType}/*`;
|
|
101
|
+
} else if (typeof resourceObj === "string") {
|
|
102
|
+
resourceString = resourceObj;
|
|
103
|
+
} else if (resourceObj === void 0) {
|
|
104
|
+
const parts = action.split(":");
|
|
105
|
+
if (parts.length > 1) {
|
|
106
|
+
resourceString = `${parts[0]}/*`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
96
109
|
const decision = await opts.enforcer.can({
|
|
97
110
|
action,
|
|
111
|
+
resource: resourceString,
|
|
98
112
|
context: {
|
|
99
113
|
...context,
|
|
114
|
+
resource: resourceObj,
|
|
100
115
|
attributes: {
|
|
101
116
|
...context.attributes ?? {},
|
|
102
117
|
...ctxExtra ?? {}
|
|
103
118
|
}
|
|
104
|
-
}
|
|
105
|
-
resource
|
|
119
|
+
}
|
|
106
120
|
});
|
|
107
121
|
opts.onDecision?.({
|
|
108
122
|
...decision,
|
|
@@ -128,9 +142,16 @@ async function resolveResource(spec, req) {
|
|
|
128
142
|
if (typeof spec === "string") return {
|
|
129
143
|
type: spec
|
|
130
144
|
};
|
|
131
|
-
if ("loader" in spec) {
|
|
145
|
+
if ("loader" in spec && typeof spec.loader === "function" && typeof spec.idFrom === "function") {
|
|
132
146
|
const id = spec.idFrom(req);
|
|
133
147
|
const data = await spec.loader(id, req);
|
|
148
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
149
|
+
return {
|
|
150
|
+
type: spec.type,
|
|
151
|
+
id,
|
|
152
|
+
...data
|
|
153
|
+
};
|
|
154
|
+
}
|
|
134
155
|
return {
|
|
135
156
|
type: spec.type,
|
|
136
157
|
id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autorix/express",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Express.js integration for Autorix policy-based authorization (RBAC + ABAC)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authorization",
|
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
"@types/express": "^5.0.6",
|
|
56
56
|
"@types/supertest": "^6.0.3",
|
|
57
57
|
"express": "^5.2.1",
|
|
58
|
-
"supertest": "^7.2.2"
|
|
58
|
+
"supertest": "^7.2.2",
|
|
59
|
+
"@autorix/storage": "^0.1.0"
|
|
59
60
|
},
|
|
60
61
|
"engines": {
|
|
61
62
|
"node": ">=18"
|