@owlmeans/iam 0.1.6 → 0.1.8
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 +46 -0
- package/agent-meta/instructions/iam.instructions.md +115 -0
- package/agent-meta/manifest.json +16 -0
- package/build/errors.d.ts +12 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +24 -0
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/types.d.ts +80 -0
- package/build/types.d.ts.map +1 -1
- package/build/utils.d.ts +13 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +8 -0
- package/build/utils.js.map +1 -0
- package/package.json +5 -4
- package/src/errors.ts +30 -0
- package/src/index.ts +1 -0
- package/src/types.ts +111 -0
- package/src/utils.ts +22 -0
- package/tests/utils.spec.ts +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @owlmeans/iam
|
|
2
|
+
|
|
3
|
+
Provider-agnostic IAM abstraction for the OwlMeans framework — `IamService` interface, client/realm types, and permission helpers.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
- `IamService` — unified interface for provisioning clients, realms, and permissions across IAM backends (e.g. Keycloak, built-in)
|
|
8
|
+
- `IamClient` / `IamCredentialsPair` / `IamResourceSpec` — shared contract types used by server-side IAM implementations
|
|
9
|
+
- `IamPermissionArgs` — describes a named permission including resource-scope and human-readable title
|
|
10
|
+
- `hasPermission` — utility to check whether a set of grants covers a given permission/resource pair
|
|
11
|
+
- Errors: `IamError` and subtypes for client-provisioning and permission failures
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @owlmeans/iam
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import type { IamService, IamClient } from '@owlmeans/iam'
|
|
23
|
+
import { hasPermission } from '@owlmeans/iam'
|
|
24
|
+
|
|
25
|
+
// Check whether an auth payload includes a required permission
|
|
26
|
+
const allowed = hasPermission(auth, { permission: 'manage-users', resourceScoped: false })
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Typically consumed via `@owlmeans/server-iam` which wires the concrete OIDC-backed implementation.
|
|
30
|
+
|
|
31
|
+
<!-- owlmeans:agent-guidance:start -->
|
|
32
|
+
## Agent guidance
|
|
33
|
+
|
|
34
|
+
This package ships embedded Claude Code skills and GitHub Copilot instructions under
|
|
35
|
+
`agent-meta/`. After installing your `@owlmeans/*` packages, run the OwlMeans
|
|
36
|
+
agent-skills installer to place them into your project's native locations
|
|
37
|
+
(`.claude/skills/` and `.github/instructions/`):
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx @owlmeans/agent-skills
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The embedded files are version-matched to this package release. Do not edit them
|
|
44
|
+
directly — they are regenerated on each publish. To contribute guidance edits,
|
|
45
|
+
open a PR against the source monorepo.
|
|
46
|
+
<!-- owlmeans:agent-guidance:end -->
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "IamService abstraction for provider-agnostic IAM provisioning. Load when wiring IAM operations, switching IAM mode, or implementing a new IAM provider."
|
|
3
|
+
applyTo: "**/services/iam*.ts, **/context.ts, **/types.ts"
|
|
4
|
+
---
|
|
5
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
6
|
+
|
|
7
|
+
# Using `@owlmeans/iam`
|
|
8
|
+
|
|
9
|
+
Provider-agnostic IAM abstraction. Defines `IamService` interface and related types. Used by both the platform backend (`viable-backend`) and the agent library (`@owlmeans/viable`). Concrete implementations live in `@owlmeans/iam-keycloak` (full proxy) and `@owlmeans/iam-integrated`.
|
|
10
|
+
|
|
11
|
+
## Public API surface
|
|
12
|
+
|
|
13
|
+
| Symbol | Kind | Purpose |
|
|
14
|
+
|--------|------|---------|
|
|
15
|
+
| `IamService` | type | Unified IAM interface — all provisioning + authorization operations |
|
|
16
|
+
| `IamClient` | type | Provisioned OIDC client `{ id?, clientId, secret?, name?, realm? }` |
|
|
17
|
+
| `IamCredentialsPair` | type | `{ token: string; realm: string }` |
|
|
18
|
+
| `IamPermissionArgs` | type | `{ permission?, resourceScoped?, title? }` — `permission` absent means unscoped resource name |
|
|
19
|
+
| `IamResourceSpec` | type | `{ name: string; displayName?: string }` |
|
|
20
|
+
| `IamPermissionDefinition` | type | Declared permission `{ name, resource, action?, resourceScoped?, title? }` |
|
|
21
|
+
| `IamGrantArgs` | type | `{ resources?: string[] }` — present = resource-scoped grant form |
|
|
22
|
+
| `IamGrant` | type | `{ profileId, clientId, permission, resources? }` |
|
|
23
|
+
| `hasPermission` | fn | `(auth, permission, { scope?, resourceId? }?)` — checks `Authorization.permissions` PermissionSet[]; an unscoped set satisfies a resourceId check |
|
|
24
|
+
| `DEFAULT_ALIAS` | const | Default service alias `'iam-service'` |
|
|
25
|
+
| `IAM_MODE_KEYCLOAK` | const | `'keycloak'` |
|
|
26
|
+
| `IAM_MODE_INTEGRATED` | const | `'integrated'` |
|
|
27
|
+
| `IamMode` | type | `'keycloak' \| 'integrated'` |
|
|
28
|
+
| `IamError` | class | Base IAM error |
|
|
29
|
+
| `IamClientError` | class | Thrown when an OIDC client is missing a required field (e.g. `secret`) |
|
|
30
|
+
| `IamResourceError` | class | Thrown when a KC resource returns with no name |
|
|
31
|
+
| `IamGrantError` | class | Grant subject missing or entity mismatch |
|
|
32
|
+
| `IamUnsupported` | class | Operation not supported by the active backend (e.g. resource-scoped grants on Keycloak) |
|
|
33
|
+
|
|
34
|
+
## Permission model (two grant forms)
|
|
35
|
+
|
|
36
|
+
Permissions are declared per entity client (project) with `ensurePermission` and granted to end-user
|
|
37
|
+
subjects with `grantPermission`:
|
|
38
|
+
|
|
39
|
+
- **Unscoped (project-wide)**: `grantPermission(entityId, clientId, profileId, 'article--modify')`
|
|
40
|
+
- **Resource-scoped**: `grantPermission(entityId, clientId, profileId, 'department--modify', { resources: ['dep-123'] })` —
|
|
41
|
+
the grant only applies to the listed resource ids.
|
|
42
|
+
|
|
43
|
+
Grants materialize as OwlMeans `PermissionSet[]` (`scope` = clientId; resource-scoped grants live in a
|
|
44
|
+
dedicated set per permission carrying `resources[]`, because `resources` applies to all keys of a set).
|
|
45
|
+
|
|
46
|
+
## `IamService` interface
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
interface IamService extends InitializedService {
|
|
50
|
+
// Get the admin OIDC provider config for a tenant (used in payment provisioning, RP config)
|
|
51
|
+
getEntityAdminConfig: (entityId: string) => Promise<OidcProviderConfig>
|
|
52
|
+
|
|
53
|
+
// Get an admin { token, realm } pair for raw low-level calls (avoid unless necessary)
|
|
54
|
+
getCredentialsPair: (entityId: string) => Promise<IamCredentialsPair>
|
|
55
|
+
|
|
56
|
+
// Provision an OIDC client for a tenant and return its credentials
|
|
57
|
+
ensureClient: (entityId: string, clientId: string) => Promise<IamClient>
|
|
58
|
+
|
|
59
|
+
// Provision a named permission on a client resource; returns the resource name
|
|
60
|
+
ensurePermission: (entityId: string, clientId: string, resource?: string, args?: IamPermissionArgs) => Promise<string>
|
|
61
|
+
|
|
62
|
+
// Provision a resource and assign it to the tenant's owner role (used in payment provisioning)
|
|
63
|
+
ensureResourceOwnership: (entityId: string, clientId: string, resource: IamResourceSpec) => Promise<void>
|
|
64
|
+
|
|
65
|
+
// --- Authorization (permission definitions & grants) ---
|
|
66
|
+
|
|
67
|
+
// List permission definitions registered for the entity's client
|
|
68
|
+
listPermissions: (entityId: string, clientId: string) => Promise<IamPermissionDefinition[]>
|
|
69
|
+
|
|
70
|
+
// Grant a permission to an end-user subject; args.resources = resource-scoped form
|
|
71
|
+
grantPermission: (entityId: string, clientId: string, profileId: string, permission: string, args?: IamGrantArgs) => Promise<IamGrant>
|
|
72
|
+
|
|
73
|
+
// Revoke; with args.resources only those ids are removed, else the whole grant
|
|
74
|
+
revokePermission: (entityId: string, clientId: string, profileId: string, permission: string, args?: IamGrantArgs) => Promise<void>
|
|
75
|
+
|
|
76
|
+
// List grants for the client, optionally for one subject
|
|
77
|
+
listGrants: (entityId: string, clientId: string, profileId?: string) => Promise<IamGrant[]>
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## `IamClient.realm` field
|
|
82
|
+
|
|
83
|
+
`realm` is set by the provider to the entity ID. Use it instead of accessing internal properties:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// ✓ correct
|
|
87
|
+
await meta.update('oidcRealm', client.realm ?? fallbackEntityId)
|
|
88
|
+
|
|
89
|
+
// ✗ never do this — internal Keycloak implementation detail
|
|
90
|
+
await meta.update('oidcRealm', (client as any)._realm)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Selecting the IAM provider
|
|
94
|
+
|
|
95
|
+
The platform wires the correct implementation via `IAM_MODE` environment variable. Default is `'keycloak'`. Do not read `IAM_MODE` directly from feature code — it belongs in the service-factory only:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
export const makeIamService = (mode = process.env.IAM_MODE ?? 'keycloak') => {
|
|
99
|
+
if (mode === 'integrated') return makeIamIntegratedService(VIABLE_IAM)
|
|
100
|
+
return makeIamKeycloakService(VIABLE_IAM, { oidcProductAlias: OIDC_PRODUCT })
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Rules
|
|
105
|
+
|
|
106
|
+
- Always depend on `IamService` from `@owlmeans/iam`, not on `KeycloakApiService` directly.
|
|
107
|
+
- Do not expose `getCredentialsPair` to request handlers — it returns a raw admin token.
|
|
108
|
+
- `IamClientError` means the provisioned client was invalid (missing secret, null token). The proxy already throws it; callers only need to handle it, not re-throw wrapped errors.
|
|
109
|
+
- The `realm` field on `IamClient` is guaranteed to be set by all concrete implementations.
|
|
110
|
+
|
|
111
|
+
## Related instructions
|
|
112
|
+
|
|
113
|
+
- `@owlmeans/iam-keycloak` (internal) — full proxy over Keycloak; see `iam-keycloak.instructions.md`
|
|
114
|
+
- `@owlmeans/keycloak` (internal) — low-level KC admin API; see `keycloak.instructions.md`
|
|
115
|
+
- `@owlmeans/server-iam` — IAM gate asserting both grant forms; see `server-iam.instructions.md`
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"package": "@owlmeans/iam",
|
|
4
|
+
"version": "0.1.8",
|
|
5
|
+
"generatedAt": "2026-06-11T16:30:27.190Z",
|
|
6
|
+
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
|
+
"entries": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "instruction",
|
|
10
|
+
"name": "iam",
|
|
11
|
+
"category": "package-specific",
|
|
12
|
+
"file": "instructions/iam.instructions.md",
|
|
13
|
+
"canonicalPath": ".github/instructions/iam.instructions.md"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
package/build/errors.d.ts
CHANGED
|
@@ -11,4 +11,16 @@ export declare class IamResourceError extends IamError {
|
|
|
11
11
|
static typeName: string;
|
|
12
12
|
constructor(message?: string);
|
|
13
13
|
}
|
|
14
|
+
export declare class IamGrantError extends IamError {
|
|
15
|
+
static typeName: string;
|
|
16
|
+
constructor(message?: string);
|
|
17
|
+
}
|
|
18
|
+
export declare class IamUnsupported extends IamError {
|
|
19
|
+
static typeName: string;
|
|
20
|
+
constructor(message?: string);
|
|
21
|
+
}
|
|
22
|
+
export declare class IamUserError extends IamError {
|
|
23
|
+
static typeName: string;
|
|
24
|
+
constructor(message?: string);
|
|
25
|
+
}
|
|
14
26
|
//# sourceMappingURL=errors.d.ts.map
|
package/build/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,qBAAa,QAAS,SAAQ,cAAc;IAC1C,OAAuB,QAAQ,SAAkC;gBAErD,OAAO,SAAU;CAI9B;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAAwC;gBAE3D,OAAO,SAAU;CAI9B;AAED,qBAAa,gBAAiB,SAAQ,QAAQ;IAC5C,OAAuB,QAAQ,SAA0C;gBAE7D,OAAO,SAAU;CAI9B"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,qBAAa,QAAS,SAAQ,cAAc;IAC1C,OAAuB,QAAQ,SAAkC;gBAErD,OAAO,SAAU;CAI9B;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAAwC;gBAE3D,OAAO,SAAU;CAI9B;AAED,qBAAa,gBAAiB,SAAQ,QAAQ;IAC5C,OAAuB,QAAQ,SAA0C;gBAE7D,OAAO,SAAU;CAI9B;AAED,qBAAa,aAAc,SAAQ,QAAQ;IACzC,OAAuB,QAAQ,SAAuC;gBAE1D,OAAO,SAAU;CAI9B;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAA6C;gBAEhE,OAAO,SAAU;CAI9B;AAED,qBAAa,YAAa,SAAQ,QAAQ;IACxC,OAAuB,QAAQ,SAAsC;gBAEzD,OAAO,SAAU;CAI9B"}
|
package/build/errors.js
CHANGED
|
@@ -20,7 +20,31 @@ export class IamResourceError extends IamError {
|
|
|
20
20
|
this.type = IamResourceError.typeName;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
export class IamGrantError extends IamError {
|
|
24
|
+
static typeName = `IamGrant${ResilientError.typeName}`;
|
|
25
|
+
constructor(message = 'error') {
|
|
26
|
+
super(`grant:${message}`);
|
|
27
|
+
this.type = IamGrantError.typeName;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class IamUnsupported extends IamError {
|
|
31
|
+
static typeName = `IamUnsupported${ResilientError.typeName}`;
|
|
32
|
+
constructor(message = 'error') {
|
|
33
|
+
super(`unsupported:${message}`);
|
|
34
|
+
this.type = IamUnsupported.typeName;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class IamUserError extends IamError {
|
|
38
|
+
static typeName = `IamUser${ResilientError.typeName}`;
|
|
39
|
+
constructor(message = 'error') {
|
|
40
|
+
super(`user:${message}`);
|
|
41
|
+
this.type = IamUserError.typeName;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
23
44
|
ResilientError.registerErrorClass(IamError);
|
|
24
45
|
ResilientError.registerErrorClass(IamClientError);
|
|
25
46
|
ResilientError.registerErrorClass(IamResourceError);
|
|
47
|
+
ResilientError.registerErrorClass(IamGrantError);
|
|
48
|
+
ResilientError.registerErrorClass(IamUnsupported);
|
|
49
|
+
ResilientError.registerErrorClass(IamUserError);
|
|
26
50
|
//# sourceMappingURL=errors.js.map
|
package/build/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,OAAO,QAAS,SAAQ,cAAc;IACnC,MAAM,CAAU,QAAQ,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEjE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,OAAO,EAAE,CAAC,CAAA;QAC1C,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAA;IAC/B,CAAC;;AAGH,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,YAAY,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEvE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;;AAGH,MAAM,OAAO,gBAAiB,SAAQ,QAAQ;IACrC,MAAM,CAAU,QAAQ,GAAG,cAAc,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEzE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,YAAY,OAAO,EAAE,CAAC,CAAA;QAC5B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,QAAQ,CAAA;IACvC,CAAC;;AAGH,cAAc,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;AAC3C,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA;AACjD,cAAc,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAA"}
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,OAAO,QAAS,SAAQ,cAAc;IACnC,MAAM,CAAU,QAAQ,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEjE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,OAAO,EAAE,CAAC,CAAA;QAC1C,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAA;IAC/B,CAAC;;AAGH,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,YAAY,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEvE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;;AAGH,MAAM,OAAO,gBAAiB,SAAQ,QAAQ;IACrC,MAAM,CAAU,QAAQ,GAAG,cAAc,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEzE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,YAAY,OAAO,EAAE,CAAC,CAAA;QAC5B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,QAAQ,CAAA;IACvC,CAAC;;AAGH,MAAM,OAAO,aAAc,SAAQ,QAAQ;IAClC,MAAM,CAAU,QAAQ,GAAG,WAAW,cAAc,CAAC,QAAQ,EAAE,CAAA;IAEtE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,SAAS,OAAO,EAAE,CAAC,CAAA;QACzB,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAA;IACpC,CAAC;;AAGH,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,iBAAiB,cAAc,CAAC,QAAQ,EAAE,CAAA;IAE5E,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,eAAe,OAAO,EAAE,CAAC,CAAA;QAC/B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;;AAGH,MAAM,OAAO,YAAa,SAAQ,QAAQ;IACjC,MAAM,CAAU,QAAQ,GAAG,UAAU,cAAc,CAAC,QAAQ,EAAE,CAAA;IAErE,YAAY,OAAO,GAAG,OAAO;QAC3B,KAAK,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAA;QACxB,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAA;IACnC,CAAC;;AAGH,cAAc,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;AAC3C,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA;AACjD,cAAc,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAA;AACnD,cAAc,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAA;AAChD,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA;AACjD,cAAc,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAA"}
|
package/build/index.d.ts
CHANGED
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA"}
|
package/build/index.js
CHANGED
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA"}
|
package/build/types.d.ts
CHANGED
|
@@ -15,11 +15,64 @@ export interface IamCredentialsPair {
|
|
|
15
15
|
export interface IamPermissionArgs {
|
|
16
16
|
/** Action name. When absent the permission is unscoped (project-wide). */
|
|
17
17
|
permission?: string;
|
|
18
|
+
/** Declares that grants of this permission may be bound to specific resource ids. */
|
|
19
|
+
resourceScoped?: boolean;
|
|
20
|
+
/** Optional human-readable title for the permission definition. */
|
|
21
|
+
title?: string;
|
|
18
22
|
}
|
|
19
23
|
export interface IamResourceSpec {
|
|
20
24
|
name: string;
|
|
21
25
|
displayName?: string;
|
|
22
26
|
}
|
|
27
|
+
/** A permission declared for an entity's client (project). */
|
|
28
|
+
export interface IamPermissionDefinition {
|
|
29
|
+
/** Canonical name: "res--action" or bare "res" when unscoped. */
|
|
30
|
+
name: string;
|
|
31
|
+
resource: string;
|
|
32
|
+
action?: string;
|
|
33
|
+
resourceScoped?: boolean;
|
|
34
|
+
title?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface IamGrantArgs {
|
|
37
|
+
/** Resource ids for the resource-scoped grant form; omit for an unscoped (project-wide) grant. */
|
|
38
|
+
resources?: string[];
|
|
39
|
+
}
|
|
40
|
+
/** A permission granted to an end-user subject of an entity's client. */
|
|
41
|
+
export interface IamGrant {
|
|
42
|
+
/** Backend-specific subject id: integrated = IdentityProfile.profileId, keycloak = KC user id. */
|
|
43
|
+
profileId: string;
|
|
44
|
+
clientId: string;
|
|
45
|
+
/** Canonical permission name. */
|
|
46
|
+
permission: string;
|
|
47
|
+
/** Present only for resource-scoped grants. */
|
|
48
|
+
resources?: string[];
|
|
49
|
+
}
|
|
50
|
+
/** An end-user of an entity (customer-wide; shared across that entity's projects). */
|
|
51
|
+
export interface IamUser {
|
|
52
|
+
/** Backend-specific subject id: integrated = IdentityProfile.profileId, keycloak = KC user id. */
|
|
53
|
+
profileId: string;
|
|
54
|
+
/** Primary login identifier (email for the integrated OTP path). */
|
|
55
|
+
email?: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
/** AuthRole value. */
|
|
58
|
+
role: string;
|
|
59
|
+
disabled?: boolean;
|
|
60
|
+
/** Convenience count of permission grants the user holds (across clients, or for one client). */
|
|
61
|
+
grantCount?: number;
|
|
62
|
+
}
|
|
63
|
+
/** Args to invite/create an end-user under an entity. */
|
|
64
|
+
export interface IamUserInvite {
|
|
65
|
+
email: string;
|
|
66
|
+
name?: string;
|
|
67
|
+
/** AuthRole value; defaults to the backend's standard end-user role. */
|
|
68
|
+
role?: string;
|
|
69
|
+
}
|
|
70
|
+
/** Args to update an existing end-user. */
|
|
71
|
+
export interface IamUserUpdate {
|
|
72
|
+
name?: string;
|
|
73
|
+
role?: string;
|
|
74
|
+
disabled?: boolean;
|
|
75
|
+
}
|
|
23
76
|
/** Unified IAM provider interface — all platform/agent code calls only this, never a backend directly */
|
|
24
77
|
export interface IamService extends InitializedService {
|
|
25
78
|
getEntityAdminConfig: (entityId: string) => Promise<OidcProviderConfig>;
|
|
@@ -35,5 +88,32 @@ export interface IamService extends InitializedService {
|
|
|
35
88
|
* the entity-owner role. Used by payment provisioning for account/project/wl resources.
|
|
36
89
|
*/
|
|
37
90
|
ensureResourceOwnership: (entityId: string, clientId: string, resource: IamResourceSpec) => Promise<void>;
|
|
91
|
+
/** Lists permission definitions registered for the entity's client. */
|
|
92
|
+
listPermissions: (entityId: string, clientId: string) => Promise<IamPermissionDefinition[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Grants a permission to an end-user subject. With args.resources the grant is
|
|
95
|
+
* resource-scoped (bound to those resource ids); without it the grant is project-wide.
|
|
96
|
+
*/
|
|
97
|
+
grantPermission: (entityId: string, clientId: string, profileId: string, permission: string, args?: IamGrantArgs) => Promise<IamGrant>;
|
|
98
|
+
/**
|
|
99
|
+
* Revokes a grant. With args.resources only those resource ids are removed;
|
|
100
|
+
* without it the whole grant is removed.
|
|
101
|
+
*/
|
|
102
|
+
revokePermission: (entityId: string, clientId: string, profileId: string, permission: string, args?: IamGrantArgs) => Promise<void>;
|
|
103
|
+
/** Lists grants for the entity's client, optionally for a single subject. */
|
|
104
|
+
listGrants: (entityId: string, clientId: string, profileId?: string) => Promise<IamGrant[]>;
|
|
105
|
+
/**
|
|
106
|
+
* Lists the entity's end-users. With clientId, narrows to users holding at least
|
|
107
|
+
* one grant for that client (per-project view).
|
|
108
|
+
*/
|
|
109
|
+
listUsers: (entityId: string, clientId?: string) => Promise<IamUser[]>;
|
|
110
|
+
/** Loads a single end-user by subject id, or null when absent. */
|
|
111
|
+
getUser: (entityId: string, profileId: string) => Promise<IamUser | null>;
|
|
112
|
+
/** Creates (or resolves, idempotently by email) an end-user under the entity. */
|
|
113
|
+
inviteUser: (entityId: string, invite: IamUserInvite) => Promise<IamUser>;
|
|
114
|
+
/** Updates an end-user's mutable fields (name, role, disabled). */
|
|
115
|
+
updateUser: (entityId: string, profileId: string, update: IamUserUpdate) => Promise<IamUser>;
|
|
116
|
+
/** Removes an end-user from the entity (their grants go with them). */
|
|
117
|
+
removeUser: (entityId: string, profileId: string) => Promise<void>;
|
|
38
118
|
}
|
|
39
119
|
//# sourceMappingURL=types.d.ts.map
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAExD,MAAM,WAAW,SAAS;IACxB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,6FAA6F;IAC7F,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/build/utils.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Authorization } from '@owlmeans/auth';
|
|
2
|
+
export interface HasPermissionOptions {
|
|
3
|
+
/** Restrict the check to PermissionSets of this scope (the project's clientId). */
|
|
4
|
+
scope?: string;
|
|
5
|
+
/** Resource-scoped check: the set must list this id — unless the set itself is unscoped. */
|
|
6
|
+
resourceId?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* True when any PermissionSet grants `permission`. An unscoped set (no resources)
|
|
10
|
+
* satisfies a resourceId check too — a project-wide grant covers every resource.
|
|
11
|
+
*/
|
|
12
|
+
export declare const hasPermission: (auth: Authorization, permission: string, opts?: HasPermissionOptions) => boolean;
|
|
13
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,aAAa,EACnB,YAAY,MAAM,EAClB,OAAO,oBAAoB,KAC1B,OAIO,CAAA"}
|
package/build/utils.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when any PermissionSet grants `permission`. An unscoped set (no resources)
|
|
3
|
+
* satisfies a resourceId check too — a project-wide grant covers every resource.
|
|
4
|
+
*/
|
|
5
|
+
export const hasPermission = (auth, permission, opts) => auth.permissions?.some(set => (opts?.scope == null || set.scope === opts.scope)
|
|
6
|
+
&& set.permissions[permission] === true
|
|
7
|
+
&& (opts?.resourceId == null || set.resources == null || set.resources.includes(opts.resourceId))) ?? false;
|
|
8
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,IAAmB,EACnB,UAAkB,EAClB,IAA2B,EAClB,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CACzC,CAAC,IAAI,EAAE,KAAK,IAAI,IAAI,IAAI,GAAG,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC;OAC9C,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,KAAK,IAAI;OACpC,CAAC,IAAI,EAAE,UAAU,IAAI,IAAI,IAAI,GAAG,CAAC,SAAS,IAAI,IAAI,IAAI,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAClG,IAAI,KAAK,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/iam",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@owlmeans/
|
|
26
|
-
"@owlmeans/
|
|
27
|
-
"@owlmeans/
|
|
25
|
+
"@owlmeans/auth": "^0.1.8",
|
|
26
|
+
"@owlmeans/context": "^0.1.8",
|
|
27
|
+
"@owlmeans/error": "^0.1.8",
|
|
28
|
+
"@owlmeans/oidc": "^0.1.8"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@owlmeans/dep-config": "workspace:*",
|
package/src/errors.ts
CHANGED
|
@@ -27,6 +27,36 @@ export class IamResourceError extends IamError {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export class IamGrantError extends IamError {
|
|
31
|
+
public static override typeName = `IamGrant${ResilientError.typeName}`
|
|
32
|
+
|
|
33
|
+
constructor(message = 'error') {
|
|
34
|
+
super(`grant:${message}`)
|
|
35
|
+
this.type = IamGrantError.typeName
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class IamUnsupported extends IamError {
|
|
40
|
+
public static override typeName = `IamUnsupported${ResilientError.typeName}`
|
|
41
|
+
|
|
42
|
+
constructor(message = 'error') {
|
|
43
|
+
super(`unsupported:${message}`)
|
|
44
|
+
this.type = IamUnsupported.typeName
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class IamUserError extends IamError {
|
|
49
|
+
public static override typeName = `IamUser${ResilientError.typeName}`
|
|
50
|
+
|
|
51
|
+
constructor(message = 'error') {
|
|
52
|
+
super(`user:${message}`)
|
|
53
|
+
this.type = IamUserError.typeName
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
30
57
|
ResilientError.registerErrorClass(IamError)
|
|
31
58
|
ResilientError.registerErrorClass(IamClientError)
|
|
32
59
|
ResilientError.registerErrorClass(IamResourceError)
|
|
60
|
+
ResilientError.registerErrorClass(IamGrantError)
|
|
61
|
+
ResilientError.registerErrorClass(IamUnsupported)
|
|
62
|
+
ResilientError.registerErrorClass(IamUserError)
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface IamCredentialsPair {
|
|
|
18
18
|
export interface IamPermissionArgs {
|
|
19
19
|
/** Action name. When absent the permission is unscoped (project-wide). */
|
|
20
20
|
permission?: string
|
|
21
|
+
/** Declares that grants of this permission may be bound to specific resource ids. */
|
|
22
|
+
resourceScoped?: boolean
|
|
23
|
+
/** Optional human-readable title for the permission definition. */
|
|
24
|
+
title?: string
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export interface IamResourceSpec {
|
|
@@ -25,6 +29,61 @@ export interface IamResourceSpec {
|
|
|
25
29
|
displayName?: string
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
/** A permission declared for an entity's client (project). */
|
|
33
|
+
export interface IamPermissionDefinition {
|
|
34
|
+
/** Canonical name: "res--action" or bare "res" when unscoped. */
|
|
35
|
+
name: string
|
|
36
|
+
resource: string
|
|
37
|
+
action?: string
|
|
38
|
+
resourceScoped?: boolean
|
|
39
|
+
title?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface IamGrantArgs {
|
|
43
|
+
/** Resource ids for the resource-scoped grant form; omit for an unscoped (project-wide) grant. */
|
|
44
|
+
resources?: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A permission granted to an end-user subject of an entity's client. */
|
|
48
|
+
export interface IamGrant {
|
|
49
|
+
/** Backend-specific subject id: integrated = IdentityProfile.profileId, keycloak = KC user id. */
|
|
50
|
+
profileId: string
|
|
51
|
+
clientId: string
|
|
52
|
+
/** Canonical permission name. */
|
|
53
|
+
permission: string
|
|
54
|
+
/** Present only for resource-scoped grants. */
|
|
55
|
+
resources?: string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** An end-user of an entity (customer-wide; shared across that entity's projects). */
|
|
59
|
+
export interface IamUser {
|
|
60
|
+
/** Backend-specific subject id: integrated = IdentityProfile.profileId, keycloak = KC user id. */
|
|
61
|
+
profileId: string
|
|
62
|
+
/** Primary login identifier (email for the integrated OTP path). */
|
|
63
|
+
email?: string
|
|
64
|
+
name?: string
|
|
65
|
+
/** AuthRole value. */
|
|
66
|
+
role: string
|
|
67
|
+
disabled?: boolean
|
|
68
|
+
/** Convenience count of permission grants the user holds (across clients, or for one client). */
|
|
69
|
+
grantCount?: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Args to invite/create an end-user under an entity. */
|
|
73
|
+
export interface IamUserInvite {
|
|
74
|
+
email: string
|
|
75
|
+
name?: string
|
|
76
|
+
/** AuthRole value; defaults to the backend's standard end-user role. */
|
|
77
|
+
role?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Args to update an existing end-user. */
|
|
81
|
+
export interface IamUserUpdate {
|
|
82
|
+
name?: string
|
|
83
|
+
role?: string
|
|
84
|
+
disabled?: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
28
87
|
/** Unified IAM provider interface — all platform/agent code calls only this, never a backend directly */
|
|
29
88
|
export interface IamService extends InitializedService {
|
|
30
89
|
// --- Admin config (backend → OIDC RP config, payment provisioning) ---
|
|
@@ -54,4 +113,56 @@ export interface IamService extends InitializedService {
|
|
|
54
113
|
clientId: string,
|
|
55
114
|
resource: IamResourceSpec
|
|
56
115
|
) => Promise<void>
|
|
116
|
+
|
|
117
|
+
// --- Authorization (permission definitions & grants) ---
|
|
118
|
+
|
|
119
|
+
/** Lists permission definitions registered for the entity's client. */
|
|
120
|
+
listPermissions: (entityId: string, clientId: string) => Promise<IamPermissionDefinition[]>
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Grants a permission to an end-user subject. With args.resources the grant is
|
|
124
|
+
* resource-scoped (bound to those resource ids); without it the grant is project-wide.
|
|
125
|
+
*/
|
|
126
|
+
grantPermission: (
|
|
127
|
+
entityId: string,
|
|
128
|
+
clientId: string,
|
|
129
|
+
profileId: string,
|
|
130
|
+
permission: string,
|
|
131
|
+
args?: IamGrantArgs
|
|
132
|
+
) => Promise<IamGrant>
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Revokes a grant. With args.resources only those resource ids are removed;
|
|
136
|
+
* without it the whole grant is removed.
|
|
137
|
+
*/
|
|
138
|
+
revokePermission: (
|
|
139
|
+
entityId: string,
|
|
140
|
+
clientId: string,
|
|
141
|
+
profileId: string,
|
|
142
|
+
permission: string,
|
|
143
|
+
args?: IamGrantArgs
|
|
144
|
+
) => Promise<void>
|
|
145
|
+
|
|
146
|
+
/** Lists grants for the entity's client, optionally for a single subject. */
|
|
147
|
+
listGrants: (entityId: string, clientId: string, profileId?: string) => Promise<IamGrant[]>
|
|
148
|
+
|
|
149
|
+
// --- End-user management (customer-wide users, shared per entityId) ---
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Lists the entity's end-users. With clientId, narrows to users holding at least
|
|
153
|
+
* one grant for that client (per-project view).
|
|
154
|
+
*/
|
|
155
|
+
listUsers: (entityId: string, clientId?: string) => Promise<IamUser[]>
|
|
156
|
+
|
|
157
|
+
/** Loads a single end-user by subject id, or null when absent. */
|
|
158
|
+
getUser: (entityId: string, profileId: string) => Promise<IamUser | null>
|
|
159
|
+
|
|
160
|
+
/** Creates (or resolves, idempotently by email) an end-user under the entity. */
|
|
161
|
+
inviteUser: (entityId: string, invite: IamUserInvite) => Promise<IamUser>
|
|
162
|
+
|
|
163
|
+
/** Updates an end-user's mutable fields (name, role, disabled). */
|
|
164
|
+
updateUser: (entityId: string, profileId: string, update: IamUserUpdate) => Promise<IamUser>
|
|
165
|
+
|
|
166
|
+
/** Removes an end-user from the entity (their grants go with them). */
|
|
167
|
+
removeUser: (entityId: string, profileId: string) => Promise<void>
|
|
57
168
|
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Authorization } from '@owlmeans/auth'
|
|
2
|
+
|
|
3
|
+
export interface HasPermissionOptions {
|
|
4
|
+
/** Restrict the check to PermissionSets of this scope (the project's clientId). */
|
|
5
|
+
scope?: string
|
|
6
|
+
/** Resource-scoped check: the set must list this id — unless the set itself is unscoped. */
|
|
7
|
+
resourceId?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* True when any PermissionSet grants `permission`. An unscoped set (no resources)
|
|
12
|
+
* satisfies a resourceId check too — a project-wide grant covers every resource.
|
|
13
|
+
*/
|
|
14
|
+
export const hasPermission = (
|
|
15
|
+
auth: Authorization,
|
|
16
|
+
permission: string,
|
|
17
|
+
opts?: HasPermissionOptions
|
|
18
|
+
): boolean => auth.permissions?.some(set =>
|
|
19
|
+
(opts?.scope == null || set.scope === opts.scope)
|
|
20
|
+
&& set.permissions[permission] === true
|
|
21
|
+
&& (opts?.resourceId == null || set.resources == null || set.resources.includes(opts.resourceId))
|
|
22
|
+
) ?? false
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { hasPermission } from '@owlmeans/iam'
|
|
3
|
+
import type { Authorization } from '@owlmeans/auth'
|
|
4
|
+
|
|
5
|
+
const auth: Authorization = {
|
|
6
|
+
scopes: [],
|
|
7
|
+
permissions: [
|
|
8
|
+
// unscoped (project-wide) grant — shared set per client
|
|
9
|
+
{ scope: 'my-project', permissions: { 'article--modify': true } },
|
|
10
|
+
// resource-scoped grant — dedicated set carrying its resource ids
|
|
11
|
+
{
|
|
12
|
+
scope: 'my-project',
|
|
13
|
+
title: 'department--modify',
|
|
14
|
+
permissions: { 'department--modify': true },
|
|
15
|
+
resources: ['dep-12345678']
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('@owlmeans/iam — hasPermission', () => {
|
|
21
|
+
test('grants an unscoped permission', () => {
|
|
22
|
+
expect(hasPermission(auth, 'article--modify')).toBe(true)
|
|
23
|
+
expect(hasPermission(auth, 'article--delete')).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('grants a resource-scoped permission only for listed resource ids', () => {
|
|
27
|
+
expect(hasPermission(auth, 'department--modify', { resourceId: 'dep-12345678' })).toBe(true)
|
|
28
|
+
expect(hasPermission(auth, 'department--modify', { resourceId: 'dep-87654321' })).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('an unscoped set satisfies a resourceId check (project-wide grant covers every resource)', () => {
|
|
32
|
+
expect(hasPermission(auth, 'article--modify', { resourceId: 'art-12345678' })).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('scope option restricts the check to one client', () => {
|
|
36
|
+
expect(hasPermission(auth, 'article--modify', { scope: 'my-project' })).toBe(true)
|
|
37
|
+
expect(hasPermission(auth, 'article--modify', { scope: 'other-project' })).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
})
|