@mahiframework/permissions 0.1.4
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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/assignee.d.ts +57 -0
- package/dist/assignee.d.ts.map +1 -0
- package/dist/assignee.js +46 -0
- package/dist/assignee.js.map +1 -0
- package/dist/commands/permissions-cache-reset.d.ts +20 -0
- package/dist/commands/permissions-cache-reset.d.ts.map +1 -0
- package/dist/commands/permissions-cache-reset.js +24 -0
- package/dist/commands/permissions-cache-reset.js.map +1 -0
- package/dist/commands/permissions-check.d.ts +37 -0
- package/dist/commands/permissions-check.d.ts.map +1 -0
- package/dist/commands/permissions-check.js +92 -0
- package/dist/commands/permissions-check.js.map +1 -0
- package/dist/commands/permissions-show.d.ts +20 -0
- package/dist/commands/permissions-show.d.ts.map +1 -0
- package/dist/commands/permissions-show.js +49 -0
- package/dist/commands/permissions-show.js.map +1 -0
- package/dist/errors.d.ts +77 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +106 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/listeners/invalidate-permission-cache.listener.d.ts +39 -0
- package/dist/listeners/invalidate-permission-cache.listener.d.ts.map +1 -0
- package/dist/listeners/invalidate-permission-cache.listener.js +50 -0
- package/dist/listeners/invalidate-permission-cache.listener.js.map +1 -0
- package/dist/middleware/current-subject.d.ts +23 -0
- package/dist/middleware/current-subject.d.ts.map +1 -0
- package/dist/middleware/current-subject.js +31 -0
- package/dist/middleware/current-subject.js.map +1 -0
- package/dist/middleware/permission.d.ts +19 -0
- package/dist/middleware/permission.d.ts.map +1 -0
- package/dist/middleware/permission.js +34 -0
- package/dist/middleware/permission.js.map +1 -0
- package/dist/middleware/role-or-permission.d.ts +29 -0
- package/dist/middleware/role-or-permission.d.ts.map +1 -0
- package/dist/middleware/role-or-permission.js +45 -0
- package/dist/middleware/role-or-permission.js.map +1 -0
- package/dist/middleware/role.d.ts +21 -0
- package/dist/middleware/role.d.ts.map +1 -0
- package/dist/middleware/role.js +36 -0
- package/dist/middleware/role.js.map +1 -0
- package/dist/migrations/0001_create_permission_tables.d.ts +63 -0
- package/dist/migrations/0001_create_permission_tables.d.ts.map +1 -0
- package/dist/migrations/0001_create_permission_tables.js +113 -0
- package/dist/migrations/0001_create_permission_tables.js.map +1 -0
- package/dist/models/permission.model.d.ts +58 -0
- package/dist/models/permission.model.d.ts.map +1 -0
- package/dist/models/permission.model.js +18 -0
- package/dist/models/permission.model.js.map +1 -0
- package/dist/models/role.model.d.ts +60 -0
- package/dist/models/role.model.d.ts.map +1 -0
- package/dist/models/role.model.js +18 -0
- package/dist/models/role.model.js.map +1 -0
- package/dist/permission-map.d.ts +97 -0
- package/dist/permission-map.d.ts.map +1 -0
- package/dist/permission-map.js +93 -0
- package/dist/permission-map.js.map +1 -0
- package/dist/permission-registrar.d.ts +183 -0
- package/dist/permission-registrar.d.ts.map +1 -0
- package/dist/permission-registrar.js +573 -0
- package/dist/permission-registrar.js.map +1 -0
- package/dist/permissions-config.d.ts +80 -0
- package/dist/permissions-config.d.ts.map +1 -0
- package/dist/permissions-config.js +22 -0
- package/dist/permissions-config.js.map +1 -0
- package/dist/permissions-facade.d.ts +74 -0
- package/dist/permissions-facade.d.ts.map +1 -0
- package/dist/permissions-facade.js +131 -0
- package/dist/permissions-facade.js.map +1 -0
- package/dist/permissions-service-provider.d.ts +91 -0
- package/dist/permissions-service-provider.d.ts.map +1 -0
- package/dist/permissions-service-provider.js +137 -0
- package/dist/permissions-service-provider.js.map +1 -0
- package/dist/relations.d.ts +63 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +62 -0
- package/dist/relations.js.map +1 -0
- package/dist/request-cache.d.ts +50 -0
- package/dist/request-cache.d.ts.map +1 -0
- package/dist/request-cache.js +40 -0
- package/dist/request-cache.js.map +1 -0
- package/dist/tokens.d.ts +15 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +15 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type BelongsToMany } from "@mahiframework/database";
|
|
2
|
+
import type { DateTime } from "@mahiframework/datetime";
|
|
3
|
+
import { Role } from "./role.model.js";
|
|
4
|
+
/**
|
|
5
|
+
* A single named thing a subject may do: `"posts.edit"`, `"billing.view"`.
|
|
6
|
+
*
|
|
7
|
+
* A permission is a *name*, not a rule. It carries no model class, no
|
|
8
|
+
* row, no callback — holding `"posts.edit"` says nothing about *which*
|
|
9
|
+
* post. That is the Gate's job, and the deliberate reason this package's
|
|
10
|
+
* gate hook abstains the moment a check carries a model argument (see
|
|
11
|
+
* `PermissionsServiceProvider.gates()`).
|
|
12
|
+
*
|
|
13
|
+
* `guard_name` scopes the permission to one of the app's auth guards, so
|
|
14
|
+
* an `api`-guard permission cannot satisfy a `web`-guard check. It is NOT
|
|
15
|
+
* NULL and has no wildcard value: `(name, guard_name)` is unique, and a
|
|
16
|
+
* nullable column cannot be made unique portably (`nullsNotDistinct` is
|
|
17
|
+
* Postgres 15+ only and throws on SQLite/MySQL), so "applies to every
|
|
18
|
+
* guard" would have meant unlimited duplicate rows in dev.
|
|
19
|
+
*/
|
|
20
|
+
export interface PermissionAttributes {
|
|
21
|
+
/** A snowflake, hence `bigint`: a 64-bit id does not fit a `number`. */
|
|
22
|
+
id: bigint;
|
|
23
|
+
name: string;
|
|
24
|
+
/** The auth guard this permission belongs to, e.g. `"web"`. Never null. */
|
|
25
|
+
guard_name: string;
|
|
26
|
+
created_at: DateTime;
|
|
27
|
+
updated_at: DateTime;
|
|
28
|
+
/** Roles granting this permission, across `role_has_permissions`. */
|
|
29
|
+
roles: BelongsToMany<Role>;
|
|
30
|
+
}
|
|
31
|
+
declare const Permission_base: import("@mahiframework/database").ModelStatics<PermissionAttributes, {
|
|
32
|
+
readonly table: "permissions";
|
|
33
|
+
readonly primaryKey: "id";
|
|
34
|
+
readonly keyType: import("@mahiframework/database").KeyStrategy<bigint>;
|
|
35
|
+
readonly morphName: "Permission";
|
|
36
|
+
}> & (new (attributes?: Partial<import("@mahiframework/database").WritableAttributes<PermissionAttributes, {
|
|
37
|
+
readonly table: "permissions";
|
|
38
|
+
readonly primaryKey: "id";
|
|
39
|
+
readonly keyType: import("@mahiframework/database").KeyStrategy<bigint>;
|
|
40
|
+
readonly morphName: "Permission";
|
|
41
|
+
}>> | undefined) => import("@mahiframework/database").BaseModel & import("@mahiframework/database").ResolvedAttributes<PermissionAttributes> & {
|
|
42
|
+
relations: import("@mahiframework/database").RelationBuildersFor<PermissionAttributes>;
|
|
43
|
+
} & import("@mahiframework/database").HasAttributes<PermissionAttributes>);
|
|
44
|
+
export declare class Permission extends Permission_base {
|
|
45
|
+
static relationships: {
|
|
46
|
+
roles: {
|
|
47
|
+
readonly type: "belongsToMany";
|
|
48
|
+
readonly related: () => import("@mahiframework/database").ModelLike;
|
|
49
|
+
readonly options: import("@mahiframework/database").BelongsToManyOptions<any, any>;
|
|
50
|
+
readonly __brand?: {
|
|
51
|
+
kind: "belongsToMany";
|
|
52
|
+
related: Role;
|
|
53
|
+
} | undefined;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=permission.model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission.model.d.ts","sourceRoot":"","sources":["../../src/models/permission.model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAEnF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAEvC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,oBAAoB;IACnC,wEAAwE;IACxE,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,QAAQ,CAAC;IACrB,UAAU,EAAE,QAAQ,CAAC;IACrB,qEAAqE;IACrE,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;CAC5B;;;;;;;;;;;;;;AAED,qBAAa,UAAW,SAAQ,eAK9B;IACA,OAAgB,aAAa;;;;;;;;;;MAM3B;CACH"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Model, belongsToMany } from "@mahiframework/database";
|
|
2
|
+
import { snowflake } from "@mahiframework/snowflake";
|
|
3
|
+
import { Role } from "./role.model.js";
|
|
4
|
+
export class Permission extends Model()({
|
|
5
|
+
table: "permissions",
|
|
6
|
+
primaryKey: "id",
|
|
7
|
+
keyType: snowflake(),
|
|
8
|
+
morphName: "Permission",
|
|
9
|
+
}) {
|
|
10
|
+
static relationships = {
|
|
11
|
+
roles: belongsToMany(() => Role, {
|
|
12
|
+
pivotTable: "role_has_permissions",
|
|
13
|
+
foreignPivotKey: "permission_id",
|
|
14
|
+
relatedPivotKey: "role_id",
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=permission.model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission.model.js","sourceRoot":"","sources":["../../src/models/permission.model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,aAAa,EAAsB,MAAM,yBAAyB,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAErD,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AA8BvC,MAAM,OAAO,UAAW,SAAQ,KAAK,EAAwB,CAAC;IAC5D,KAAK,EAAE,aAAa;IACpB,UAAU,EAAE,IAAI;IAChB,OAAO,EAAE,SAAS,EAAE;IACpB,SAAS,EAAE,YAAY;CACxB,CAAC;IACA,MAAM,CAAU,aAAa,GAAG;QAC9B,KAAK,EAAE,aAAa,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE;YAC/B,UAAU,EAAE,sBAAsB;YAClC,eAAe,EAAE,eAAe;YAChC,eAAe,EAAE,SAAS;SAC3B,CAAC;KACH,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type BelongsToMany } from "@mahiframework/database";
|
|
2
|
+
import type { DateTime } from "@mahiframework/datetime";
|
|
3
|
+
import { Permission } from "./permission.model.js";
|
|
4
|
+
/**
|
|
5
|
+
* A named bundle of permissions a subject can hold.
|
|
6
|
+
*
|
|
7
|
+
* The indirection is the whole point: a subject holding `"editor"`
|
|
8
|
+
* inherits every permission attached to it, so changing what an editor
|
|
9
|
+
* may do is one write instead of one per user. A permission can also be
|
|
10
|
+
* granted to a subject directly (`model_has_permissions`), which
|
|
11
|
+
* supplements roles rather than replacing them.
|
|
12
|
+
*
|
|
13
|
+
* `guard_name` is NOT NULL and scoped exactly as `Permission`'s is; see
|
|
14
|
+
* that model for why there is no wildcard guard.
|
|
15
|
+
*
|
|
16
|
+
* There is intentionally no `users` relation here. The inverse of the
|
|
17
|
+
* polymorphic assignment is a `morphedByMany`, which needs the *app's*
|
|
18
|
+
* model class, and this package does not know it. The docs show the three
|
|
19
|
+
* lines an app writes to declare it, and flag the `type`-defaulting
|
|
20
|
+
* asymmetry between `morphToMany` and `morphedByMany`.
|
|
21
|
+
*/
|
|
22
|
+
export interface RoleAttributes {
|
|
23
|
+
/** A snowflake, hence `bigint`: a 64-bit id does not fit a `number`. */
|
|
24
|
+
id: bigint;
|
|
25
|
+
name: string;
|
|
26
|
+
/** The auth guard this role belongs to, e.g. `"web"`. Never null. */
|
|
27
|
+
guard_name: string;
|
|
28
|
+
created_at: DateTime;
|
|
29
|
+
updated_at: DateTime;
|
|
30
|
+
/** Permissions this role grants, across `role_has_permissions`. */
|
|
31
|
+
permissions: BelongsToMany<Permission>;
|
|
32
|
+
}
|
|
33
|
+
declare const Role_base: import("@mahiframework/database").ModelStatics<RoleAttributes, {
|
|
34
|
+
readonly table: "roles";
|
|
35
|
+
readonly primaryKey: "id";
|
|
36
|
+
readonly keyType: import("@mahiframework/database").KeyStrategy<bigint>;
|
|
37
|
+
readonly morphName: "Role";
|
|
38
|
+
}> & (new (attributes?: Partial<import("@mahiframework/database").WritableAttributes<RoleAttributes, {
|
|
39
|
+
readonly table: "roles";
|
|
40
|
+
readonly primaryKey: "id";
|
|
41
|
+
readonly keyType: import("@mahiframework/database").KeyStrategy<bigint>;
|
|
42
|
+
readonly morphName: "Role";
|
|
43
|
+
}>> | undefined) => import("@mahiframework/database").BaseModel & import("@mahiframework/database").ResolvedAttributes<RoleAttributes> & {
|
|
44
|
+
relations: import("@mahiframework/database").RelationBuildersFor<RoleAttributes>;
|
|
45
|
+
} & import("@mahiframework/database").HasAttributes<RoleAttributes>);
|
|
46
|
+
export declare class Role extends Role_base {
|
|
47
|
+
static relationships: {
|
|
48
|
+
permissions: {
|
|
49
|
+
readonly type: "belongsToMany";
|
|
50
|
+
readonly related: () => import("@mahiframework/database").ModelLike;
|
|
51
|
+
readonly options: import("@mahiframework/database").BelongsToManyOptions<any, any>;
|
|
52
|
+
readonly __brand?: {
|
|
53
|
+
kind: "belongsToMany";
|
|
54
|
+
related: Permission;
|
|
55
|
+
} | undefined;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=role.model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role.model.d.ts","sourceRoot":"","sources":["../../src/models/role.model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAEnF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,QAAQ,CAAC;IACrB,UAAU,EAAE,QAAQ,CAAC;IACrB,mEAAmE;IACnE,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;CACxC;;;;;;;;;;;;;;AAED,qBAAa,IAAK,SAAQ,SAKxB;IACA,OAAgB,aAAa;;;;;;;;;;MAM3B;CACH"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Model, belongsToMany } from "@mahiframework/database";
|
|
2
|
+
import { snowflake } from "@mahiframework/snowflake";
|
|
3
|
+
import { Permission } from "./permission.model.js";
|
|
4
|
+
export class Role extends Model()({
|
|
5
|
+
table: "roles",
|
|
6
|
+
primaryKey: "id",
|
|
7
|
+
keyType: snowflake(),
|
|
8
|
+
morphName: "Role",
|
|
9
|
+
}) {
|
|
10
|
+
static relationships = {
|
|
11
|
+
permissions: belongsToMany(() => Permission, {
|
|
12
|
+
pivotTable: "role_has_permissions",
|
|
13
|
+
foreignPivotKey: "role_id",
|
|
14
|
+
relatedPivotKey: "permission_id",
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=role.model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role.model.js","sourceRoot":"","sources":["../../src/models/role.model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,aAAa,EAAsB,MAAM,yBAAyB,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAErD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAgCnD,MAAM,OAAO,IAAK,SAAQ,KAAK,EAAkB,CAAC;IAChD,KAAK,EAAE,OAAO;IACd,UAAU,EAAE,IAAI;IAChB,OAAO,EAAE,SAAS,EAAE;IACpB,SAAS,EAAE,MAAM;CAClB,CAAC;IACA,MAAM,CAAU,aAAa,GAAG;QAC9B,WAAW,EAAE,aAAa,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE;YAC3C,UAAU,EAAE,sBAAsB;YAClC,eAAe,EAAE,SAAS;YAC1B,eAAe,EAAE,eAAe;SACjC,CAAC;KACH,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cached snapshot of every role, every permission, and which
|
|
3
|
+
* permissions each role grants.
|
|
4
|
+
*
|
|
5
|
+
* ONE cache entry for the whole table set, which is spatie's model and
|
|
6
|
+
* the only one `@mahiframework/cache` can support: it has no tags, so
|
|
7
|
+
* there is no flush-by-pattern, and every key written must be a key
|
|
8
|
+
* nameable later. One key is nameable. It is also bounded by the number
|
|
9
|
+
* of roles and permissions an app defines (tens, not millions), unlike a
|
|
10
|
+
* per-subject cache which grows with the user table.
|
|
11
|
+
*
|
|
12
|
+
* What is NOT in here: who holds what. `model_has_roles` scales with
|
|
13
|
+
* users, so caching it under one key would mean one entry growing without
|
|
14
|
+
* bound and invalidated by every assignment anywhere. Assignments are a
|
|
15
|
+
* query, memoised per request (see `request-cache.ts`).
|
|
16
|
+
*
|
|
17
|
+
* ## The serialisation boundary
|
|
18
|
+
*
|
|
19
|
+
* `RedisCacheStore` and `FileCacheStore` persist with `JSON.stringify`,
|
|
20
|
+
* and `JSON.stringify` THROWS on a `bigint` — deliberately, per
|
|
21
|
+
* `@mahiframework/snowflake`: an id cannot be serialised without a
|
|
22
|
+
* decision being made about it. The decision here is a decimal string,
|
|
23
|
+
* matching `Resource` and `Model.toJSON()`.
|
|
24
|
+
*
|
|
25
|
+
* So the cached form (`SerializedPermissionMap`) carries strings and the
|
|
26
|
+
* in-memory form (`PermissionMap`) carries `bigint`, with
|
|
27
|
+
* `serializeMap`/`deserializeMap` the only crossing. Getting this wrong
|
|
28
|
+
* is a bug that cannot be reproduced on `ArrayCacheStore`, which passes a
|
|
29
|
+
* `bigint` through happily — it would appear only once an app switched to
|
|
30
|
+
* redis or file, i.e. in production. Hence one module, two functions, and
|
|
31
|
+
* a test that round-trips through `JSON.parse(JSON.stringify(...))`.
|
|
32
|
+
*/
|
|
33
|
+
/** A role as cached: its identity plus the permissions it grants. */
|
|
34
|
+
export interface MappedRole {
|
|
35
|
+
id: bigint;
|
|
36
|
+
name: string;
|
|
37
|
+
guardName: string;
|
|
38
|
+
/** Permission ids from `role_has_permissions`. */
|
|
39
|
+
permissionIds: bigint[];
|
|
40
|
+
}
|
|
41
|
+
/** A permission as cached. */
|
|
42
|
+
export interface MappedPermission {
|
|
43
|
+
id: bigint;
|
|
44
|
+
name: string;
|
|
45
|
+
guardName: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The in-memory map, with lookup indexes derived once at
|
|
49
|
+
* deserialisation rather than scanned per check.
|
|
50
|
+
*
|
|
51
|
+
* Keyed by `name` + `guardName` together, because the same name can exist
|
|
52
|
+
* once per guard and a `web` role must not answer an `api` lookup.
|
|
53
|
+
*/
|
|
54
|
+
export interface PermissionMap {
|
|
55
|
+
roles: MappedRole[];
|
|
56
|
+
permissions: MappedPermission[];
|
|
57
|
+
/** `"{guard}\u0000{name}"` -> role. */
|
|
58
|
+
roleByName: Map<string, MappedRole>;
|
|
59
|
+
/** `"{guard}\u0000{name}"` -> permission. */
|
|
60
|
+
permissionByName: Map<string, MappedPermission>;
|
|
61
|
+
/** Role id -> role, for turning an assignment row into permissions. */
|
|
62
|
+
roleById: Map<bigint, MappedRole>;
|
|
63
|
+
/** Permission id -> permission, for naming a directly-assigned permission. */
|
|
64
|
+
permissionById: Map<bigint, MappedPermission>;
|
|
65
|
+
}
|
|
66
|
+
/** The JSON-safe form actually written to the cache store. */
|
|
67
|
+
export interface SerializedPermissionMap {
|
|
68
|
+
roles: Array<{
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
guardName: string;
|
|
72
|
+
permissionIds: string[];
|
|
73
|
+
}>;
|
|
74
|
+
permissions: Array<{
|
|
75
|
+
id: string;
|
|
76
|
+
name: string;
|
|
77
|
+
guardName: string;
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* `"{guard}\0{name}"`.
|
|
82
|
+
*
|
|
83
|
+
* A NUL separator rather than `:` because a permission name is
|
|
84
|
+
* app-chosen and may contain anything printable (`"billing:view"` is a
|
|
85
|
+
* perfectly ordinary name), whereas a guard name comes from
|
|
86
|
+
* `config/auth.ts`. Without a separator that cannot appear in either
|
|
87
|
+
* half, `("web", "a:b")` and `("web:a", "b")` would collide — and the
|
|
88
|
+
* collision would grant a permission nobody assigned.
|
|
89
|
+
*/
|
|
90
|
+
export declare function mapKey(guardName: string, name: string): string;
|
|
91
|
+
/** Strip the `bigint`s out, for the cache store. */
|
|
92
|
+
export declare function serializeMap(map: PermissionMap): SerializedPermissionMap;
|
|
93
|
+
/** Put the `bigint`s back, and build the lookup indexes. */
|
|
94
|
+
export declare function deserializeMap(serialized: SerializedPermissionMap): PermissionMap;
|
|
95
|
+
/** Assemble a map and its indexes from freshly-read rows. */
|
|
96
|
+
export declare function buildMap(roles: MappedRole[], permissions: MappedPermission[]): PermissionMap;
|
|
97
|
+
//# sourceMappingURL=permission-map.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-map.d.ts","sourceRoot":"","sources":["../src/permission-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,qEAAqE;AACrE,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,8BAA8B;AAC9B,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,uCAAuC;IACvC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACpC,6CAA6C;IAC7C,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAChD,uEAAuE;IACvE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAClC,8EAA8E;IAC9E,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC/C;AAED,8DAA8D;AAC9D,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACvF,WAAW,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrE;AAED;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,oDAAoD;AACpD,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,uBAAuB,CAcxE;AAED,4DAA4D;AAC5D,wBAAgB,cAAc,CAAC,UAAU,EAAE,uBAAuB,GAAG,aAAa,CAejF;AAED,6DAA6D;AAC7D,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,WAAW,EAAE,gBAAgB,EAAE,GAAG,aAAa,CAiB5F"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cached snapshot of every role, every permission, and which
|
|
3
|
+
* permissions each role grants.
|
|
4
|
+
*
|
|
5
|
+
* ONE cache entry for the whole table set, which is spatie's model and
|
|
6
|
+
* the only one `@mahiframework/cache` can support: it has no tags, so
|
|
7
|
+
* there is no flush-by-pattern, and every key written must be a key
|
|
8
|
+
* nameable later. One key is nameable. It is also bounded by the number
|
|
9
|
+
* of roles and permissions an app defines (tens, not millions), unlike a
|
|
10
|
+
* per-subject cache which grows with the user table.
|
|
11
|
+
*
|
|
12
|
+
* What is NOT in here: who holds what. `model_has_roles` scales with
|
|
13
|
+
* users, so caching it under one key would mean one entry growing without
|
|
14
|
+
* bound and invalidated by every assignment anywhere. Assignments are a
|
|
15
|
+
* query, memoised per request (see `request-cache.ts`).
|
|
16
|
+
*
|
|
17
|
+
* ## The serialisation boundary
|
|
18
|
+
*
|
|
19
|
+
* `RedisCacheStore` and `FileCacheStore` persist with `JSON.stringify`,
|
|
20
|
+
* and `JSON.stringify` THROWS on a `bigint` — deliberately, per
|
|
21
|
+
* `@mahiframework/snowflake`: an id cannot be serialised without a
|
|
22
|
+
* decision being made about it. The decision here is a decimal string,
|
|
23
|
+
* matching `Resource` and `Model.toJSON()`.
|
|
24
|
+
*
|
|
25
|
+
* So the cached form (`SerializedPermissionMap`) carries strings and the
|
|
26
|
+
* in-memory form (`PermissionMap`) carries `bigint`, with
|
|
27
|
+
* `serializeMap`/`deserializeMap` the only crossing. Getting this wrong
|
|
28
|
+
* is a bug that cannot be reproduced on `ArrayCacheStore`, which passes a
|
|
29
|
+
* `bigint` through happily — it would appear only once an app switched to
|
|
30
|
+
* redis or file, i.e. in production. Hence one module, two functions, and
|
|
31
|
+
* a test that round-trips through `JSON.parse(JSON.stringify(...))`.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* `"{guard}\0{name}"`.
|
|
35
|
+
*
|
|
36
|
+
* A NUL separator rather than `:` because a permission name is
|
|
37
|
+
* app-chosen and may contain anything printable (`"billing:view"` is a
|
|
38
|
+
* perfectly ordinary name), whereas a guard name comes from
|
|
39
|
+
* `config/auth.ts`. Without a separator that cannot appear in either
|
|
40
|
+
* half, `("web", "a:b")` and `("web:a", "b")` would collide — and the
|
|
41
|
+
* collision would grant a permission nobody assigned.
|
|
42
|
+
*/
|
|
43
|
+
export function mapKey(guardName, name) {
|
|
44
|
+
return `${guardName}\u0000${name}`;
|
|
45
|
+
}
|
|
46
|
+
/** Strip the `bigint`s out, for the cache store. */
|
|
47
|
+
export function serializeMap(map) {
|
|
48
|
+
return {
|
|
49
|
+
roles: map.roles.map((role) => ({
|
|
50
|
+
id: String(role.id),
|
|
51
|
+
name: role.name,
|
|
52
|
+
guardName: role.guardName,
|
|
53
|
+
permissionIds: role.permissionIds.map(String),
|
|
54
|
+
})),
|
|
55
|
+
permissions: map.permissions.map((permission) => ({
|
|
56
|
+
id: String(permission.id),
|
|
57
|
+
name: permission.name,
|
|
58
|
+
guardName: permission.guardName,
|
|
59
|
+
})),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Put the `bigint`s back, and build the lookup indexes. */
|
|
63
|
+
export function deserializeMap(serialized) {
|
|
64
|
+
const roles = serialized.roles.map((role) => ({
|
|
65
|
+
id: BigInt(role.id),
|
|
66
|
+
name: role.name,
|
|
67
|
+
guardName: role.guardName,
|
|
68
|
+
permissionIds: role.permissionIds.map(BigInt),
|
|
69
|
+
}));
|
|
70
|
+
const permissions = serialized.permissions.map((permission) => ({
|
|
71
|
+
id: BigInt(permission.id),
|
|
72
|
+
name: permission.name,
|
|
73
|
+
guardName: permission.guardName,
|
|
74
|
+
}));
|
|
75
|
+
return buildMap(roles, permissions);
|
|
76
|
+
}
|
|
77
|
+
/** Assemble a map and its indexes from freshly-read rows. */
|
|
78
|
+
export function buildMap(roles, permissions) {
|
|
79
|
+
const roleByName = new Map();
|
|
80
|
+
const roleById = new Map();
|
|
81
|
+
const permissionByName = new Map();
|
|
82
|
+
const permissionById = new Map();
|
|
83
|
+
for (const role of roles) {
|
|
84
|
+
roleByName.set(mapKey(role.guardName, role.name), role);
|
|
85
|
+
roleById.set(role.id, role);
|
|
86
|
+
}
|
|
87
|
+
for (const permission of permissions) {
|
|
88
|
+
permissionByName.set(mapKey(permission.guardName, permission.name), permission);
|
|
89
|
+
permissionById.set(permission.id, permission);
|
|
90
|
+
}
|
|
91
|
+
return { roles, permissions, roleByName, permissionByName, roleById, permissionById };
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=permission-map.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-map.js","sourceRoot":"","sources":["../src/permission-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AA4CH;;;;;;;;;GASG;AACH,MAAM,UAAU,MAAM,CAAC,SAAiB,EAAE,IAAY;IACpD,OAAO,GAAG,SAAS,SAAS,IAAI,EAAE,CAAC;AACrC,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,YAAY,CAAC,GAAkB;IAC7C,OAAO;QACL,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAC9B,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YACnB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC;SAC9C,CAAC,CAAC;QACH,WAAW,EAAE,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YAChD,EAAE,EAAE,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;YACzB,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,SAAS,EAAE,UAAU,CAAC,SAAS;SAChC,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,cAAc,CAAC,UAAmC;IAChE,MAAM,KAAK,GAAiB,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC1D,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACnB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC;KAC9C,CAAC,CAAC,CAAC;IAEJ,MAAM,WAAW,GAAuB,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QAClF,EAAE,EAAE,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QACzB,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,SAAS,EAAE,UAAU,CAAC,SAAS;KAChC,CAAC,CAAC,CAAC;IAEJ,OAAO,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;AACtC,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,QAAQ,CAAC,KAAmB,EAAE,WAA+B;IAC3E,MAAM,UAAU,GAAG,IAAI,GAAG,EAAsB,CAAC;IACjD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC/C,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAA4B,CAAC;IAC7D,MAAM,cAAc,GAAG,IAAI,GAAG,EAA4B,CAAC;IAE3D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;QACxD,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;IAED,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,CAAC;QAChF,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAChD,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC;AACxF,CAAC"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { Application } from "@mahiframework/core";
|
|
2
|
+
import { type Assignee } from "./assignee.js";
|
|
3
|
+
import { type PermissionMap } from "./permission-map.js";
|
|
4
|
+
import { Permission } from "./models/permission.model.js";
|
|
5
|
+
import { Role } from "./models/role.model.js";
|
|
6
|
+
import type { ResolvedPermissionsConfig } from "./permissions-config.js";
|
|
7
|
+
/** Options every name-addressed call accepts. */
|
|
8
|
+
export interface GuardOption {
|
|
9
|
+
/** The auth guard to scope to. Defaults to config `guard`, then `auth.default`. */
|
|
10
|
+
guard?: string;
|
|
11
|
+
}
|
|
12
|
+
/** A role named by name, id, or instance. */
|
|
13
|
+
export type RoleRef = string | bigint | Role;
|
|
14
|
+
/** A permission named by name, id, or instance. */
|
|
15
|
+
export type PermissionRef = string | bigint | Permission;
|
|
16
|
+
/** Roles and permissions, their relationships, and who holds them. */
|
|
17
|
+
export declare class PermissionRegistrar {
|
|
18
|
+
private readonly app;
|
|
19
|
+
private readonly config;
|
|
20
|
+
constructor(app: Application, config: ResolvedPermissionsConfig);
|
|
21
|
+
/**
|
|
22
|
+
* The guard a call operates on.
|
|
23
|
+
*
|
|
24
|
+
* `auth.default` is the last resort rather than the ambient
|
|
25
|
+
* `currentGuard()`, which would read the guard that authenticated THIS
|
|
26
|
+
* request. That is tempting and wrong: it throws outside a request
|
|
27
|
+
* scope (`MissingAuthContextError`), so a queue job or a seeder
|
|
28
|
+
* assigning a role would crash, and it would make the same
|
|
29
|
+
* `createRole("admin")` produce a different row depending on who
|
|
30
|
+
* happened to call it.
|
|
31
|
+
*
|
|
32
|
+
* Throws rather than defaulting to `""`: `guard_name` is NOT NULL with
|
|
33
|
+
* no wildcard, so an unresolvable guard has no safe representation, and
|
|
34
|
+
* a role stamped with the empty string is a role no check will ever
|
|
35
|
+
* match.
|
|
36
|
+
*/
|
|
37
|
+
guardName(options?: GuardOption): string;
|
|
38
|
+
private defaultGuard;
|
|
39
|
+
/**
|
|
40
|
+
* The role/permission map, from cache or the database.
|
|
41
|
+
*
|
|
42
|
+
* `remember()` treats `undefined` as its miss sentinel, so the loader
|
|
43
|
+
* must never return it — this one always returns an object, even for an
|
|
44
|
+
* app with no roles at all.
|
|
45
|
+
*
|
|
46
|
+
* The map crosses the cache boundary serialised (ids as strings): see
|
|
47
|
+
* `permission-map.ts` for why a `bigint` cannot be cached.
|
|
48
|
+
*/
|
|
49
|
+
map(): Promise<PermissionMap>;
|
|
50
|
+
/** Read every role, permission, and role-permission link. Three queries, no joins. */
|
|
51
|
+
private loadMap;
|
|
52
|
+
/**
|
|
53
|
+
* Drop the cached map and every memoised assignment.
|
|
54
|
+
*
|
|
55
|
+
* Called by every write here, and by the model-event listeners for
|
|
56
|
+
* writes that bypass this class (a seeder calling `Role.create()`).
|
|
57
|
+
* Forgets exactly one key — `@mahiframework/cache` has no tags, so
|
|
58
|
+
* there is no flush-by-pattern, and `flush()` would take out the app's
|
|
59
|
+
* entire cache.
|
|
60
|
+
*/
|
|
61
|
+
forgetCache(): Promise<void>;
|
|
62
|
+
private cache;
|
|
63
|
+
/** Create a role. Throws if one already exists with this name for this guard. */
|
|
64
|
+
createRole(name: string, options?: GuardOption): Promise<Role>;
|
|
65
|
+
/** Create a permission. Throws if one already exists with this name for this guard. */
|
|
66
|
+
createPermission(name: string, options?: GuardOption): Promise<Permission>;
|
|
67
|
+
/** Create a role only if it doesn't exist, returning either way. */
|
|
68
|
+
findOrCreateRole(name: string, options?: GuardOption): Promise<Role>;
|
|
69
|
+
/** Create a permission only if it doesn't exist, returning either way. */
|
|
70
|
+
findOrCreatePermission(name: string, options?: GuardOption): Promise<Permission>;
|
|
71
|
+
/** The role with this name, or `RoleNotFoundError`. */
|
|
72
|
+
findRole(name: string, options?: GuardOption): Promise<Role>;
|
|
73
|
+
/** The permission with this name, or `PermissionNotFoundError`. */
|
|
74
|
+
findPermission(name: string, options?: GuardOption): Promise<Permission>;
|
|
75
|
+
/**
|
|
76
|
+
* Delete a role. Its assignments and permission links go with it, via
|
|
77
|
+
* the pivots' `cascadeOnDelete` foreign keys.
|
|
78
|
+
*/
|
|
79
|
+
deleteRole(role: RoleRef, options?: GuardOption): Promise<void>;
|
|
80
|
+
/** Delete a permission. Its assignments and role links cascade. */
|
|
81
|
+
deletePermission(permission: PermissionRef, options?: GuardOption): Promise<void>;
|
|
82
|
+
/** Grant permissions to a role, ignoring any it already has. */
|
|
83
|
+
givePermissionToRole(role: RoleRef, permissions: PermissionRef | PermissionRef[], options?: GuardOption): Promise<void>;
|
|
84
|
+
/** Revoke permissions from a role. */
|
|
85
|
+
revokePermissionFromRole(role: RoleRef, permissions: PermissionRef | PermissionRef[], options?: GuardOption): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Make a role's permissions exactly this list.
|
|
88
|
+
*
|
|
89
|
+
* An empty list revokes everything, which is the whole point of a sync
|
|
90
|
+
* and the one behaviour worth being explicit about: the framework's own
|
|
91
|
+
* `detach([])` is a deliberate no-op, so a caller passing
|
|
92
|
+
* `request.input("permissions")` through that API would silently keep
|
|
93
|
+
* the old set. This writes pivots directly and means what it says.
|
|
94
|
+
*/
|
|
95
|
+
syncRolePermissions(role: RoleRef, permissions: PermissionRef | PermissionRef[], options?: GuardOption): Promise<void>;
|
|
96
|
+
/** Assign roles to a subject. Already-held roles are skipped, so this is idempotent. */
|
|
97
|
+
assignRole(assignee: Assignee, roles: RoleRef | RoleRef[], options?: GuardOption): Promise<void>;
|
|
98
|
+
/** Remove roles from a subject. */
|
|
99
|
+
removeRole(assignee: Assignee, roles: RoleRef | RoleRef[], options?: GuardOption): Promise<void>;
|
|
100
|
+
/** Make a subject's roles exactly this list. An empty list removes all of them. */
|
|
101
|
+
syncRoles(assignee: Assignee, roles: RoleRef | RoleRef[], options?: GuardOption): Promise<void>;
|
|
102
|
+
/** Grant permissions to a subject directly, alongside whatever its roles grant. */
|
|
103
|
+
givePermissionTo(assignee: Assignee, permissions: PermissionRef | PermissionRef[], options?: GuardOption): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Revoke a directly-granted permission.
|
|
106
|
+
*
|
|
107
|
+
* Only touches `model_has_permissions`. A permission the subject also
|
|
108
|
+
* holds through a role survives this call, and `hasPermissionTo()` will
|
|
109
|
+
* still answer true — which is correct, and the reason
|
|
110
|
+
* `hasDirectPermission()` exists to tell the two apart.
|
|
111
|
+
*/
|
|
112
|
+
revokePermissionTo(assignee: Assignee, permissions: PermissionRef | PermissionRef[], options?: GuardOption): Promise<void>;
|
|
113
|
+
/** Make a subject's direct permissions exactly this list. An empty list removes all of them. */
|
|
114
|
+
syncPermissions(assignee: Assignee, permissions: PermissionRef | PermissionRef[], options?: GuardOption): Promise<void>;
|
|
115
|
+
/** Does this subject hold this role? */
|
|
116
|
+
hasRole(assignee: Assignee, role: string | bigint, options?: GuardOption): Promise<boolean>;
|
|
117
|
+
/** Does this subject hold at least one of these roles? */
|
|
118
|
+
hasAnyRole(assignee: Assignee, roles: Array<string | bigint>, options?: GuardOption): Promise<boolean>;
|
|
119
|
+
/** Does this subject hold every one of these roles? */
|
|
120
|
+
hasAllRoles(assignee: Assignee, roles: Array<string | bigint>, options?: GuardOption): Promise<boolean>;
|
|
121
|
+
/**
|
|
122
|
+
* Does this subject hold this permission, by any route?
|
|
123
|
+
*
|
|
124
|
+
* An unknown permission name returns false rather than throwing. This
|
|
125
|
+
* one is a check, not a write: the gate hook calls it with every
|
|
126
|
+
* ability string in the app, almost none of which are permissions, so
|
|
127
|
+
* throwing would turn `can("view-dashboard")` into a 500. The write
|
|
128
|
+
* methods still throw on an unknown name, which is where a typo
|
|
129
|
+
* actually matters.
|
|
130
|
+
*/
|
|
131
|
+
hasPermissionTo(assignee: Assignee, permission: string | bigint, options?: GuardOption): Promise<boolean>;
|
|
132
|
+
/** Does this subject hold at least one of these permissions, by any route? */
|
|
133
|
+
hasAnyPermission(assignee: Assignee, permissions: Array<string | bigint>, options?: GuardOption): Promise<boolean>;
|
|
134
|
+
/** Does this subject hold every one of these permissions, by any route? */
|
|
135
|
+
hasAllPermissions(assignee: Assignee, permissions: Array<string | bigint>, options?: GuardOption): Promise<boolean>;
|
|
136
|
+
/** Does this subject hold this permission DIRECTLY, ignoring its roles? */
|
|
137
|
+
hasDirectPermission(assignee: Assignee, permission: string | bigint, options?: GuardOption): Promise<boolean>;
|
|
138
|
+
/** This subject's role names, for the resolved guard. */
|
|
139
|
+
getRoleNames(assignee: Assignee, options?: GuardOption): Promise<Set<string>>;
|
|
140
|
+
/** Permission names this subject holds via its roles only. */
|
|
141
|
+
getPermissionsViaRoles(assignee: Assignee, options?: GuardOption): Promise<Set<string>>;
|
|
142
|
+
/** Permission names granted to this subject directly, ignoring its roles. */
|
|
143
|
+
getDirectPermissions(assignee: Assignee, options?: GuardOption): Promise<Set<string>>;
|
|
144
|
+
/** Every permission name this subject holds, by either route. */
|
|
145
|
+
getAllPermissions(assignee: Assignee, options?: GuardOption): Promise<Set<string>>;
|
|
146
|
+
/**
|
|
147
|
+
* One subject's raw assignment ids, memoised per request.
|
|
148
|
+
*
|
|
149
|
+
* Two queries, in parallel, and at most once per subject per request.
|
|
150
|
+
* The memo is what makes the gate hook affordable: it fires on every
|
|
151
|
+
* authorization check, so a controller with five `can()` calls would
|
|
152
|
+
* otherwise be ten queries against rows that cannot have changed.
|
|
153
|
+
*/
|
|
154
|
+
private assignmentsFor;
|
|
155
|
+
/** The role's name, whether it was named by name or by id. Unknown ids yield a non-matching sentinel. */
|
|
156
|
+
private roleNameOf;
|
|
157
|
+
private permissionNameOf;
|
|
158
|
+
/** Resolve a role reference to its id, throwing on an unknown name. */
|
|
159
|
+
private roleId;
|
|
160
|
+
private permissionId;
|
|
161
|
+
private roleIds;
|
|
162
|
+
private permissionIds;
|
|
163
|
+
/**
|
|
164
|
+
* Insert pivot rows, in chunks, as one multi-row statement each.
|
|
165
|
+
*
|
|
166
|
+
* Goes to raw Kysely rather than `QueryBuilder`, whose `insert()` takes
|
|
167
|
+
* a single row: a per-row loop would turn a 500-permission sync into
|
|
168
|
+
* 500 round trips. `Role.resolveConnection()` is the handle rather
|
|
169
|
+
* than a `DatabaseManager` lookup because it swaps in the active
|
|
170
|
+
* transaction when there is one, which is what makes the `sync*`
|
|
171
|
+
* methods' delete-then-insert atomic.
|
|
172
|
+
*
|
|
173
|
+
* Chunked because pivot writes in this framework do not bound their
|
|
174
|
+
* parameter count, and a multi-row insert binds one parameter per
|
|
175
|
+
* column per row — three columns here, so a sync of 12,000 would
|
|
176
|
+
* exceed SQLite's 32,766-binding ceiling (Postgres' is 65,535) and
|
|
177
|
+
* fail at the driver rather than anywhere meaningful. 1,000 rows is
|
|
178
|
+
* 3,000 bindings, well inside every engine. Sequential, since the
|
|
179
|
+
* chunks all target one table inside one transaction.
|
|
180
|
+
*/
|
|
181
|
+
private insertChunked;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=permission-registrar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-registrar.d.ts","sourceRoot":"","sources":["../src/permission-registrar.ts"],"names":[],"mappings":"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"}
|