@nebutra/permissions 2.0.0 → 3.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 +6 -60
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +3 -22
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/types.d.ts +1 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -10
- package/dist/providers/openfga.d.ts +0 -58
- package/dist/providers/openfga.d.ts.map +0 -1
- package/dist/providers/openfga.js +0 -212
package/README.md
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
> **Status: Foundation** — CASL in-process evaluation supports deterministic role inheritance, ABAC conditions, and field-level rules.
|
|
1
|
+
> **Status: Foundation** — CASL in-process evaluation supports deterministic role inheritance, ABAC conditions, and field-level rules.
|
|
2
2
|
|
|
3
3
|
# @nebutra/permissions
|
|
4
4
|
|
|
5
|
-
RBAC (Role-Based Access Control) and ABAC (Attribute-Based Access Control) permissions engine for the Nebutra-Sailor monorepo. Built on CASL
|
|
5
|
+
RBAC (Role-Based Access Control) and ABAC (Attribute-Based Access Control) permissions engine for the Nebutra-Sailor monorepo. Built on CASL — the only supported provider.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **CASL-based in-process evaluation** — Fast, no network calls, great for UI + API middleware
|
|
10
|
-
- **OpenFGA integration** — Store-scoped REST checks, writes, deletes, and list-objects calls for managed/self-hosted Zanzibar relationship graphs
|
|
11
10
|
- **Role hierarchy** — Roles inherit permissions from parent roles; child rules override inherited grants deterministically
|
|
12
11
|
- **ABAC conditions** — Dynamic field resolution at evaluation time
|
|
13
12
|
- **Field-level permissions** — Restrict access to specific fields through CASL-backed checks
|
|
14
13
|
- **React hooks & components** — `usePermission()`, `<Can>` component for UI gate-keeping
|
|
15
14
|
- **Hono middleware** — Automatic permission checks in API routes
|
|
16
|
-
- **Provider auto-detection** — Automatically picks CASL or OpenFGA based on env vars
|
|
17
15
|
|
|
18
16
|
## Installation
|
|
19
17
|
|
|
@@ -172,7 +170,7 @@ permissions.can(context, "read", "Document", undefined, "internalNotes"); // fal
|
|
|
172
170
|
|
|
173
171
|
## Provider Configuration
|
|
174
172
|
|
|
175
|
-
### CASL (
|
|
173
|
+
### CASL (In-Process, only supported provider)
|
|
176
174
|
|
|
177
175
|
Fast, no network calls. Great for UI and API middleware.
|
|
178
176
|
|
|
@@ -182,50 +180,11 @@ import { createCASLProvider } from "@nebutra/permissions/casl";
|
|
|
182
180
|
const provider = createCASLProvider();
|
|
183
181
|
```
|
|
184
182
|
|
|
185
|
-
**Auto-detection:** Uses CASL if no `OPENFGA_API_URL` env var is set.
|
|
186
|
-
|
|
187
|
-
### OpenFGA (Relationship-Based)
|
|
188
|
-
|
|
189
|
-
OpenFGA uses the store-scoped REST API and fails closed when configuration or network calls are invalid. This package does not manage OpenFGA authorization models or tuple migrations.
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
import { createOpenFGAProvider } from "@nebutra/permissions/openfga";
|
|
193
|
-
|
|
194
|
-
const provider = createOpenFGAProvider(
|
|
195
|
-
{
|
|
196
|
-
apiUrl: "http://openfga.internal:8080",
|
|
197
|
-
storeId: "store_abc123",
|
|
198
|
-
authToken: process.env.OPENFGA_AUTH_TOKEN,
|
|
199
|
-
},
|
|
200
|
-
roles
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
// Write relationship tuples
|
|
204
|
-
await provider.write([
|
|
205
|
-
{ user: "user_123", relation: "member", object: "team:acme" },
|
|
206
|
-
{ user: "team:acme", relation: "parent", object: "org:company" },
|
|
207
|
-
]);
|
|
208
|
-
|
|
209
|
-
// Check access
|
|
210
|
-
const allowed = await provider.check(
|
|
211
|
-
"user_123",
|
|
212
|
-
"can_edit",
|
|
213
|
-
"document:doc_456"
|
|
214
|
-
);
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
**Auto-detection:** Uses OpenFGA if `OPENFGA_API_URL` env var is set.
|
|
218
|
-
|
|
219
183
|
## Environment Variables
|
|
220
184
|
|
|
221
185
|
```env
|
|
222
|
-
# Provider selection (
|
|
223
|
-
PERMISSIONS_PROVIDER=casl
|
|
224
|
-
|
|
225
|
-
# OpenFGA configuration
|
|
226
|
-
OPENFGA_API_URL=http://openfga:8080 # Triggers OpenFGA provider
|
|
227
|
-
OPENFGA_STORE_ID=abc123 # Required for OpenFGA REST calls
|
|
228
|
-
OPENFGA_AUTH_TOKEN=secret # Optional for managed OpenFGA
|
|
186
|
+
# Provider selection (optional — casl is the only supported provider)
|
|
187
|
+
PERMISSIONS_PROVIDER=casl
|
|
229
188
|
```
|
|
230
189
|
|
|
231
190
|
## API Reference
|
|
@@ -236,11 +195,8 @@ Initialize the global permissions manager (singleton).
|
|
|
236
195
|
|
|
237
196
|
```typescript
|
|
238
197
|
const permissions = createPermissions({
|
|
239
|
-
provider: "
|
|
198
|
+
provider: "casl",
|
|
240
199
|
roles: customRoles,
|
|
241
|
-
openFgaApiUrl: "http://openfga:8080",
|
|
242
|
-
openFgaStoreId: "abc123",
|
|
243
|
-
openFgaAuthToken: process.env.OPENFGA_AUTH_TOKEN,
|
|
244
200
|
});
|
|
245
201
|
```
|
|
246
202
|
|
|
@@ -406,16 +362,6 @@ const provider = new CASLProvider();
|
|
|
406
362
|
provider.clearCache();
|
|
407
363
|
```
|
|
408
364
|
|
|
409
|
-
### OpenFGA connection errors
|
|
410
|
-
|
|
411
|
-
Verify `OPENFGA_API_URL` is reachable:
|
|
412
|
-
|
|
413
|
-
```bash
|
|
414
|
-
curl http://openfga:8080/health
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
Check logs for `OpenFGA check error` messages.
|
|
418
|
-
|
|
419
365
|
## License
|
|
420
366
|
|
|
421
367
|
MIT
|
package/dist/factory.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,MAAM,EACN,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,cAAc,EACd,QAAQ,EACR,cAAc,EACf,MAAM,SAAS,CAAC;AAIjB,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,YAAY,CAAyB;gBAEjC,MAAM,CAAC,EAAE,gBAAgB;IAUrC,OAAO,CAAC,cAAc;IAetB,eAAe,IAAI,sBAAsB;IAIzC,GAAG,CACD,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO;IAIV,MAAM,CACJ,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO;IAIV,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE;IAI/C,UAAU,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI;IAI5C,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO;IAIpD,WAAW,IAAI,kBAAkB;CAGlC;AAED,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,kBAAkB,CAG/E;AAED,wBAAgB,cAAc,IAAI,kBAAkB,CAKnD;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
|
package/dist/factory.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { logger } from "@nebutra/logger";
|
|
2
2
|
import { CASLProvider } from "./providers/casl";
|
|
3
|
-
import { OpenFGAProvider } from "./providers/openfga";
|
|
4
3
|
import { getDefaultRoles } from "./roles";
|
|
5
4
|
let permissionsInstance = null;
|
|
6
5
|
export class PermissionsManager {
|
|
@@ -10,23 +9,8 @@ export class PermissionsManager {
|
|
|
10
9
|
const detectedType = this.detectProvider(config?.provider);
|
|
11
10
|
this.providerType = detectedType;
|
|
12
11
|
const roles = config?.roles || getDefaultRoles();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// the optional `?:` modifier on OpenFGAProviderConfig stays satisfied.
|
|
16
|
-
const openFgaConfig = {};
|
|
17
|
-
if (config?.openFgaApiUrl !== undefined)
|
|
18
|
-
openFgaConfig.apiUrl = config.openFgaApiUrl;
|
|
19
|
-
if (config?.openFgaAuthToken !== undefined)
|
|
20
|
-
openFgaConfig.authToken = config.openFgaAuthToken;
|
|
21
|
-
if (config?.openFgaStoreId !== undefined)
|
|
22
|
-
openFgaConfig.storeId = config.openFgaStoreId;
|
|
23
|
-
this.provider = new OpenFGAProvider(openFgaConfig, roles);
|
|
24
|
-
logger.info("Initialized OpenFGA permissions provider");
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
this.provider = new CASLProvider(roles);
|
|
28
|
-
logger.info("Initialized CASL permissions provider");
|
|
29
|
-
}
|
|
12
|
+
this.provider = new CASLProvider(roles);
|
|
13
|
+
logger.info("Initialized CASL permissions provider");
|
|
30
14
|
}
|
|
31
15
|
detectProvider(explicit) {
|
|
32
16
|
if (explicit) {
|
|
@@ -34,13 +18,10 @@ export class PermissionsManager {
|
|
|
34
18
|
}
|
|
35
19
|
if (process.env.PERMISSIONS_PROVIDER) {
|
|
36
20
|
const provider = process.env.PERMISSIONS_PROVIDER;
|
|
37
|
-
if (
|
|
21
|
+
if (provider === "casl") {
|
|
38
22
|
return provider;
|
|
39
23
|
}
|
|
40
24
|
}
|
|
41
|
-
if (process.env.OPENFGA_API_URL) {
|
|
42
|
-
return "openfga";
|
|
43
|
-
}
|
|
44
25
|
return "casl";
|
|
45
26
|
}
|
|
46
27
|
getProviderType() {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ export { createPermissions, getPermissions, PermissionsManager, resetPermissions
|
|
|
2
2
|
export { attachPermissionContext, type RequirePermissionOptions, requirePermission, } from "./middleware";
|
|
3
3
|
export { canPlatform, isPlatformStaffRole, normalizePlatformStaffRole, PLATFORM_STAFF_ROLES, type PlatformAbility, type PlatformAction, type PlatformResource, type PlatformStaffRole, platformAbilityFor, platformRoleHierarchy, } from "./platform";
|
|
4
4
|
export { buildAbilityFor, CASLProvider, createCASLProvider, createPrismaAbility, getPrismaQuery, } from "./providers/casl";
|
|
5
|
-
export { createOpenFGAProvider, type OpenFGACheckRequest, type OpenFGACheckResponse, type OpenFGAListObjectsRequest, OpenFGAProvider, type OpenFGAProviderConfig, type OpenFGATuple, type OpenFGAWriteRequest, } from "./providers/openfga";
|
|
6
5
|
export { Can, Cannot, type CannotProps, type CanProps, PermissionProvider as PermissionProviderComponent, type PermissionProviderProps, type UsePermissionOptions, usePermission, usePermissionContext, } from "./react";
|
|
7
6
|
export { DEFAULT_ROLES, getDefaultRoles, getRoleDefinition, getRoleHierarchy, } from "./roles";
|
|
8
7
|
export { type AbilityOptions, type Action, ForbiddenError, type PermissionConfig, type PermissionContext, type PermissionProvider, type PermissionProviderType, type PermissionRule, type Resource, type Role, type RoleDefinition, } from "./types";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,uBAAuB,EACvB,KAAK,wBAAwB,EAC7B,iBAAiB,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,uBAAuB,EACvB,KAAK,wBAAwB,EAC7B,iBAAiB,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,0BAA0B,EAC1B,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,GAAG,EACH,MAAM,EACN,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,kBAAkB,IAAI,2BAA2B,EACjD,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,aAAa,EACb,oBAAoB,GACrB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,MAAM,EACX,cAAc,EACd,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,IAAI,EACT,KAAK,cAAc,GACpB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,6 @@ export { createPermissions, getPermissions, PermissionsManager, resetPermissions
|
|
|
2
2
|
export { attachPermissionContext, requirePermission, } from "./middleware";
|
|
3
3
|
export { canPlatform, isPlatformStaffRole, normalizePlatformStaffRole, PLATFORM_STAFF_ROLES, platformAbilityFor, platformRoleHierarchy, } from "./platform";
|
|
4
4
|
export { buildAbilityFor, CASLProvider, createCASLProvider, createPrismaAbility, getPrismaQuery, } from "./providers/casl";
|
|
5
|
-
export { createOpenFGAProvider, OpenFGAProvider, } from "./providers/openfga";
|
|
6
5
|
export { Can, Cannot, PermissionProvider as PermissionProviderComponent, usePermission, usePermissionContext, } from "./react";
|
|
7
6
|
export { DEFAULT_ROLES, getDefaultRoles, getRoleDefinition, getRoleHierarchy, } from "./roles";
|
|
8
7
|
export { ForbiddenError, } from "./types";
|
package/dist/types.d.ts
CHANGED
|
@@ -28,13 +28,10 @@ export interface PermissionProvider {
|
|
|
28
28
|
defineRole(definition: RoleDefinition): void;
|
|
29
29
|
buildAbilityFor(context: PermissionContext): unknown;
|
|
30
30
|
}
|
|
31
|
-
export type PermissionProviderType = "casl"
|
|
31
|
+
export type PermissionProviderType = "casl";
|
|
32
32
|
export interface PermissionConfig {
|
|
33
33
|
provider?: PermissionProviderType;
|
|
34
34
|
roles?: RoleDefinition[];
|
|
35
|
-
openFgaApiUrl?: string;
|
|
36
|
-
openFgaStoreId?: string | undefined;
|
|
37
|
-
openFgaAuthToken?: string | undefined;
|
|
38
35
|
}
|
|
39
36
|
export interface AbilityOptions {
|
|
40
37
|
resolveAction?: boolean;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GACd,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,OAAO,GACP,MAAM,CAAC;AAEX,MAAM,MAAM,QAAQ,GAChB,UAAU,GACV,SAAS,GACT,WAAW,GACX,MAAM,GACN,SAAS,GACT,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,aAAa,GACb,eAAe,GACf,YAAY,GACZ,UAAU,GACV,MAAM,CAAC;AAEX,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,eAAe,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhG,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,QAAQ,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,EAAE,CAAC;IACzB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,CACD,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC;IACX,MAAM,CACJ,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC;IACX,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9C,UAAU,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAAC;IAC7C,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC;CACtD;AAED,MAAM,MAAM,sBAAsB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GACd,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,OAAO,GACP,MAAM,CAAC;AAEX,MAAM,MAAM,QAAQ,GAChB,UAAU,GACV,SAAS,GACT,WAAW,GACX,MAAM,GACN,SAAS,GACT,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,aAAa,GACb,eAAe,GACf,YAAY,GACZ,UAAU,GACV,MAAM,CAAC;AAEX,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,eAAe,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhG,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,QAAQ,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,EAAE,CAAC;IACzB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,CACD,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC;IACX,MAAM,CACJ,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC;IACX,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9C,UAAU,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAAC;IAC7C,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC;CACtD;AAED,MAAM,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,qBAAa,cAAe,SAAQ,KAAK;IAE9B,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,QAAQ;IAClB,OAAO,CAAC,EAAE,iBAAiB;gBAF3B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,iBAAiB,YAAA;CAKrC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/permissions",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,15 +9,14 @@
|
|
|
9
9
|
"status": "foundation",
|
|
10
10
|
"productionReady": false,
|
|
11
11
|
"requires": [
|
|
12
|
-
"CASL (in-process, no external creds)"
|
|
13
|
-
"or OPENFGA_API_URL + OPENFGA_STORE_ID for Zanzibar-style"
|
|
12
|
+
"CASL (in-process, no external creds)"
|
|
14
13
|
],
|
|
15
14
|
"gaps": [
|
|
16
|
-
"
|
|
15
|
+
"Policies are evaluated in-process only; there is no external relationship store for cross-service authorization"
|
|
17
16
|
],
|
|
18
17
|
"featureId": "permissions",
|
|
19
18
|
"category": "iam",
|
|
20
|
-
"summary": "RBAC/ABAC engine — CASL (in-process)
|
|
19
|
+
"summary": "RBAC/ABAC engine — CASL (in-process)"
|
|
21
20
|
},
|
|
22
21
|
"main": "./dist/index.js",
|
|
23
22
|
"types": "./dist/index.d.ts",
|
|
@@ -30,10 +29,6 @@
|
|
|
30
29
|
"types": "./dist/providers/casl.d.ts",
|
|
31
30
|
"import": "./dist/providers/casl.js"
|
|
32
31
|
},
|
|
33
|
-
"./openfga": {
|
|
34
|
-
"types": "./dist/providers/openfga.d.ts",
|
|
35
|
-
"import": "./dist/providers/openfga.js"
|
|
36
|
-
},
|
|
37
32
|
"./react": {
|
|
38
33
|
"types": "./dist/react.d.ts",
|
|
39
34
|
"import": "./dist/react.js"
|
|
@@ -55,7 +50,7 @@
|
|
|
55
50
|
"@casl/ability": "^6.7.0",
|
|
56
51
|
"@casl/prisma": "^1.5.1",
|
|
57
52
|
"zod": "^4.3.6",
|
|
58
|
-
"@nebutra/logger": "
|
|
53
|
+
"@nebutra/logger": "3.0.0"
|
|
59
54
|
},
|
|
60
55
|
"peerDependencies": {
|
|
61
56
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { Action, PermissionContext, PermissionProvider, PermissionRule, Resource, RoleDefinition } from "../types";
|
|
2
|
-
export interface OpenFGATuple {
|
|
3
|
-
user: string;
|
|
4
|
-
relation: string;
|
|
5
|
-
object: string;
|
|
6
|
-
}
|
|
7
|
-
export interface OpenFGAProviderConfig {
|
|
8
|
-
apiUrl?: string | undefined;
|
|
9
|
-
storeId?: string | undefined;
|
|
10
|
-
authToken?: string | undefined;
|
|
11
|
-
}
|
|
12
|
-
export interface OpenFGACheckRequest {
|
|
13
|
-
tuple_key: {
|
|
14
|
-
user: string;
|
|
15
|
-
relation: string;
|
|
16
|
-
object: string;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
export interface OpenFGACheckResponse {
|
|
20
|
-
allowed: boolean;
|
|
21
|
-
}
|
|
22
|
-
export interface OpenFGAWriteRequest {
|
|
23
|
-
writes?: {
|
|
24
|
-
tuple_keys: OpenFGATuple[];
|
|
25
|
-
};
|
|
26
|
-
deletes?: {
|
|
27
|
-
tuple_keys: OpenFGATuple[];
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
export interface OpenFGAListObjectsRequest {
|
|
31
|
-
user: string;
|
|
32
|
-
relation: string;
|
|
33
|
-
type: string;
|
|
34
|
-
}
|
|
35
|
-
export declare class OpenFGAProvider implements PermissionProvider {
|
|
36
|
-
private apiUrl;
|
|
37
|
-
private storeId;
|
|
38
|
-
private authToken;
|
|
39
|
-
private roleDefinitions;
|
|
40
|
-
constructor(config?: string | OpenFGAProviderConfig, roles?: RoleDefinition[]);
|
|
41
|
-
defineRole(definition: RoleDefinition): void;
|
|
42
|
-
getRulesForRole(role: string): PermissionRule[];
|
|
43
|
-
private endpoint;
|
|
44
|
-
private headers;
|
|
45
|
-
check(user: string, relation: string, object: string): Promise<boolean>;
|
|
46
|
-
write(tuples: OpenFGATuple[]): Promise<void>;
|
|
47
|
-
delete(tuples: OpenFGATuple[]): Promise<void>;
|
|
48
|
-
listObjects(user: string, relation: string, type: string): Promise<string[]>;
|
|
49
|
-
can(_context: PermissionContext, _action: Action, _resource: Resource, _subject?: unknown): boolean;
|
|
50
|
-
cannot(context: PermissionContext, action: Action, resource: Resource, subject?: unknown): boolean;
|
|
51
|
-
canAsync(context: PermissionContext, action: Action, resource: Resource, subject?: unknown): Promise<boolean>;
|
|
52
|
-
private formatUser;
|
|
53
|
-
private formatObject;
|
|
54
|
-
private extractSubjectId;
|
|
55
|
-
buildAbilityFor(_context: PermissionContext): unknown;
|
|
56
|
-
}
|
|
57
|
-
export declare function createOpenFGAProvider(config?: string | OpenFGAProviderConfig, roles?: RoleDefinition[]): OpenFGAProvider;
|
|
58
|
-
//# sourceMappingURL=openfga.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"openfga.d.ts","sourceRoot":"","sources":["../../src/providers/openfga.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,EACd,QAAQ,EACR,cAAc,EACf,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE;QACP,UAAU,EAAE,YAAY,EAAE,CAAC;KAC5B,CAAC;IACF,OAAO,CAAC,EAAE;QACR,UAAU,EAAE,YAAY,EAAE,CAAC;KAC5B,CAAC;CACH;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,eAAgB,YAAW,kBAAkB;IACxD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,eAAe,CAA0C;gBAErD,MAAM,CAAC,EAAE,MAAM,GAAG,qBAAqB,EAAE,KAAK,CAAC,EAAE,cAAc,EAAE;IAgB7E,UAAU,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI;IAI5C,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE;IAK/C,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,OAAO;IAUT,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkCvE,KAAK,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA6B5C,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA6B7C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAgClF,GAAG,CACD,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,QAAQ,EACnB,QAAQ,CAAC,EAAE,OAAO,GACjB,OAAO;IAOV,MAAM,CACJ,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO;IAIJ,QAAQ,CACZ,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC,OAAO,CAAC;IAWnB,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,YAAY;IAoBpB,OAAO,CAAC,gBAAgB;IAoBxB,eAAe,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO;CAGtD;AAED,wBAAgB,qBAAqB,CACnC,MAAM,CAAC,EAAE,MAAM,GAAG,qBAAqB,EACvC,KAAK,CAAC,EAAE,cAAc,EAAE,GACvB,eAAe,CAEjB"}
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { logger } from "@nebutra/logger";
|
|
2
|
-
export class OpenFGAProvider {
|
|
3
|
-
apiUrl;
|
|
4
|
-
storeId;
|
|
5
|
-
authToken;
|
|
6
|
-
roleDefinitions = new Map();
|
|
7
|
-
constructor(config, roles) {
|
|
8
|
-
const resolvedConfig = typeof config === "string" ? { apiUrl: config } : config;
|
|
9
|
-
this.apiUrl = resolvedConfig?.apiUrl || process.env.OPENFGA_API_URL || "http://localhost:8080";
|
|
10
|
-
const resolvedStoreId = resolvedConfig?.storeId || process.env.OPENFGA_STORE_ID;
|
|
11
|
-
if (resolvedStoreId !== undefined)
|
|
12
|
-
this.storeId = resolvedStoreId;
|
|
13
|
-
const resolvedAuthToken = resolvedConfig?.authToken || process.env.OPENFGA_AUTH_TOKEN;
|
|
14
|
-
if (resolvedAuthToken !== undefined)
|
|
15
|
-
this.authToken = resolvedAuthToken;
|
|
16
|
-
if (roles) {
|
|
17
|
-
for (const role of roles) {
|
|
18
|
-
this.roleDefinitions.set(role.role, role);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
defineRole(definition) {
|
|
23
|
-
this.roleDefinitions.set(definition.role, definition);
|
|
24
|
-
}
|
|
25
|
-
getRulesForRole(role) {
|
|
26
|
-
const definition = this.roleDefinitions.get(role);
|
|
27
|
-
return definition?.rules || [];
|
|
28
|
-
}
|
|
29
|
-
endpoint(path) {
|
|
30
|
-
if (!this.storeId) {
|
|
31
|
-
logger.error("OpenFGA store id is required", { path });
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
const apiUrl = this.apiUrl.replace(/\/+$/, "");
|
|
35
|
-
const storeId = encodeURIComponent(this.storeId);
|
|
36
|
-
return `${apiUrl}/stores/${storeId}/${path}`;
|
|
37
|
-
}
|
|
38
|
-
headers() {
|
|
39
|
-
const headers = { "Content-Type": "application/json" };
|
|
40
|
-
if (this.authToken) {
|
|
41
|
-
headers.Authorization = `Bearer ${this.authToken}`;
|
|
42
|
-
}
|
|
43
|
-
return headers;
|
|
44
|
-
}
|
|
45
|
-
async check(user, relation, object) {
|
|
46
|
-
const request = {
|
|
47
|
-
tuple_key: {
|
|
48
|
-
object,
|
|
49
|
-
relation,
|
|
50
|
-
user,
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
const endpoint = this.endpoint("check");
|
|
54
|
-
if (!endpoint) {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
const response = await fetch(endpoint, {
|
|
59
|
-
method: "POST",
|
|
60
|
-
headers: this.headers(),
|
|
61
|
-
body: JSON.stringify(request),
|
|
62
|
-
});
|
|
63
|
-
if (!response.ok) {
|
|
64
|
-
logger.error("OpenFGA check failed", { status: response.status, user, relation, object });
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
const data = await response.json();
|
|
68
|
-
return data.allowed === true;
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
logger.error("OpenFGA check error", { error, user, relation, object });
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
async write(tuples) {
|
|
76
|
-
const request = {
|
|
77
|
-
writes: {
|
|
78
|
-
tuple_keys: tuples,
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
const endpoint = this.endpoint("write");
|
|
82
|
-
if (!endpoint) {
|
|
83
|
-
throw new Error("OpenFGA write failed: missing store id");
|
|
84
|
-
}
|
|
85
|
-
try {
|
|
86
|
-
const response = await fetch(endpoint, {
|
|
87
|
-
method: "POST",
|
|
88
|
-
headers: this.headers(),
|
|
89
|
-
body: JSON.stringify(request),
|
|
90
|
-
});
|
|
91
|
-
if (!response.ok) {
|
|
92
|
-
logger.error("OpenFGA write failed", { status: response.status });
|
|
93
|
-
throw new Error(`OpenFGA write failed: ${response.statusText}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
catch (error) {
|
|
97
|
-
logger.error("OpenFGA write error", { error });
|
|
98
|
-
throw error;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
async delete(tuples) {
|
|
102
|
-
const request = {
|
|
103
|
-
deletes: {
|
|
104
|
-
tuple_keys: tuples,
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
const endpoint = this.endpoint("write");
|
|
108
|
-
if (!endpoint) {
|
|
109
|
-
throw new Error("OpenFGA delete failed: missing store id");
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
const response = await fetch(endpoint, {
|
|
113
|
-
method: "POST",
|
|
114
|
-
headers: this.headers(),
|
|
115
|
-
body: JSON.stringify(request),
|
|
116
|
-
});
|
|
117
|
-
if (!response.ok) {
|
|
118
|
-
logger.error("OpenFGA delete failed", { status: response.status });
|
|
119
|
-
throw new Error(`OpenFGA delete failed: ${response.statusText}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
logger.error("OpenFGA delete error", { error });
|
|
124
|
-
throw error;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
async listObjects(user, relation, type) {
|
|
128
|
-
const request = {
|
|
129
|
-
relation,
|
|
130
|
-
type,
|
|
131
|
-
user,
|
|
132
|
-
};
|
|
133
|
-
const endpoint = this.endpoint("list-objects");
|
|
134
|
-
if (!endpoint) {
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
const response = await fetch(endpoint, {
|
|
139
|
-
method: "POST",
|
|
140
|
-
headers: this.headers(),
|
|
141
|
-
body: JSON.stringify(request),
|
|
142
|
-
});
|
|
143
|
-
if (!response.ok) {
|
|
144
|
-
logger.error("OpenFGA listObjects failed", { status: response.status });
|
|
145
|
-
return [];
|
|
146
|
-
}
|
|
147
|
-
const data = await response.json();
|
|
148
|
-
return data.objects || [];
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
logger.error("OpenFGA listObjects error", { error });
|
|
152
|
-
return [];
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
can(_context, _action, _resource, _subject) {
|
|
156
|
-
logger.warn("OpenFGAProvider.can() is synchronous wrapper - use checkAsync() for actual checks");
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
cannot(context, action, resource, subject) {
|
|
160
|
-
return !this.can(context, action, resource, subject);
|
|
161
|
-
}
|
|
162
|
-
async canAsync(context, action, resource, subject) {
|
|
163
|
-
const user = this.formatUser(context.userId);
|
|
164
|
-
const object = this.formatObject(resource, subject);
|
|
165
|
-
if (!user || !object) {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
return this.check(user, action, object);
|
|
169
|
-
}
|
|
170
|
-
formatUser(userId) {
|
|
171
|
-
const trimmed = userId.trim();
|
|
172
|
-
if (!trimmed) {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
return trimmed.includes(":") ? trimmed : `user:${trimmed}`;
|
|
176
|
-
}
|
|
177
|
-
formatObject(resource, subject) {
|
|
178
|
-
const objectId = this.extractSubjectId(subject);
|
|
179
|
-
if (!objectId) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
if (objectId.includes(":")) {
|
|
183
|
-
return objectId;
|
|
184
|
-
}
|
|
185
|
-
const objectType = String(resource).trim();
|
|
186
|
-
if (!objectType) {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
return `${objectType}:${objectId}`;
|
|
190
|
-
}
|
|
191
|
-
extractSubjectId(subject) {
|
|
192
|
-
if (typeof subject === "string") {
|
|
193
|
-
const trimmed = subject.trim();
|
|
194
|
-
return trimmed || null;
|
|
195
|
-
}
|
|
196
|
-
if (subject === null || typeof subject !== "object" || Array.isArray(subject)) {
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
const id = subject.id;
|
|
200
|
-
if (typeof id !== "string") {
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
const trimmed = id.trim();
|
|
204
|
-
return trimmed || null;
|
|
205
|
-
}
|
|
206
|
-
buildAbilityFor(_context) {
|
|
207
|
-
return this;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
export function createOpenFGAProvider(config, roles) {
|
|
211
|
-
return new OpenFGAProvider(config, roles);
|
|
212
|
-
}
|