@application-kit/authorization 0.4.0 → 0.5.1
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/CHANGELOG.md +14 -0
- package/README.md +16 -0
- package/dist/cjs/testing/assert-ability-registry-consistent.d.ts +9 -0
- package/dist/cjs/testing/assert-ability-registry-consistent.js +64 -0
- package/dist/cjs/testing/index.d.ts +1 -0
- package/dist/cjs/testing/index.js +17 -0
- package/dist/esm/testing/assert-ability-registry-consistent.d.ts +9 -0
- package/dist/esm/testing/assert-ability-registry-consistent.js +57 -0
- package/dist/esm/testing/index.d.ts +1 -0
- package/dist/esm/testing/index.js +1 -0
- package/package.json +20 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @application-kit/authorization
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8e10f98: Peer-зависимости принимают Nest 12, `@nestjs/graphql` и `@nestjs/mercurius` 14, `@nestjs/cqrs` 12 и `nestjs-pino` 5. `PinoLoggerConfig` задаёт `forRoutes` явно: с маршрутом nestjs-pino 5 по умолчанию запросы на Fastify не логировались.
|
|
8
|
+
|
|
9
|
+
## 0.5.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- e9411eb: Спеки-гейты продукта переехали в пакеты: спека теперь — вызов помощника с параметрами продукта, а нарушение приходит ошибкой со списком.
|
|
14
|
+
|
|
15
|
+
`ui/testing`: `assertTokensDeclared`. `kafka/testing` (новый вход): `assertConsumerTopicsConsistent`. `authorization/testing` (новый вход): `assertAbilityRegistryConsistent`. `nestjs-vite/static-assets-gate` (новый вход, отдельно от `/testing`, чтобы спеки с фейком рендерера не грузили `vite`): `assertStaticAssetsLayout`. `lint-config` разрешает импорт нового входа `nestjs-vite/static-assets-gate`.
|
|
16
|
+
|
|
3
17
|
## 0.4.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ Peer-зависимости: `@casl/ability`, `@nestjs/common`, `@nestjs/core`,
|
|
|
21
21
|
| `@application-kit/authorization` | сервер и браузер: реестры, `buildAbilityFor`, `composeAbilityGrants` | только `@casl/ability` |
|
|
22
22
|
| `@application-kit/authorization/nest` | сервер: guard, декораторы, фабрика, логгер | `@nestjs/*`, `nestjs-pino` |
|
|
23
23
|
| `@application-kit/authorization/vue` | клиент и SSR: ability на приложение, `<Can>`, гвард маршрутов по `meta.can` | `vue`, `vue-router`, `@casl/vue` |
|
|
24
|
+
| `@application-kit/authorization/testing` | спека продукта: гейт реестра abilities | `node:fs`, `@casl/ability` |
|
|
24
25
|
|
|
25
26
|
Корневой вход не импортирует Nest — его можно собирать в браузерный бандл.
|
|
26
27
|
|
|
@@ -96,3 +97,18 @@ export class AppModule {}
|
|
|
96
97
|
|
|
97
98
|
`AuthorizationLogger` — pino-логгер пакета с контекстом `AuthorizationLogger`; собирается из глобального
|
|
98
99
|
`LoggerModule` `nestjs-pino`.
|
|
100
|
+
|
|
101
|
+
## Гейт реестра
|
|
102
|
+
|
|
103
|
+
Реестр subject'ов живёт только в типах: забытая регистрация не падает в рантайме. `assertAbilityRegistryConsistent` из
|
|
104
|
+
входа `@application-kit/authorization/testing` читает блоки `declare module` файлов грантов и падает, если запись не
|
|
105
|
+
выдана грантом продукта или её subject не проверяется в prod-коде:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
await assertAbilityRegistryConsistent({
|
|
109
|
+
grants: abilityGrants,
|
|
110
|
+
grantsFiles: 'modules/*/module.grants.ts',
|
|
111
|
+
roles: Object.values(ROLE_VALUE),
|
|
112
|
+
sourceRoot: path.resolve(import.meta.dirname, '..'),
|
|
113
|
+
})
|
|
114
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AbilityGrantMap } from '../types/ability-grant.js';
|
|
2
|
+
import type { Role } from '../types/roles.js';
|
|
3
|
+
export interface AbilityRegistryOptionsInterface {
|
|
4
|
+
readonly grants: AbilityGrantMap;
|
|
5
|
+
readonly grantsFiles: string;
|
|
6
|
+
readonly roles: readonly Role[];
|
|
7
|
+
readonly sourceRoot: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const assertAbilityRegistryConsistent: (options: AbilityRegistryOptionsInterface) => Promise<void>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.assertAbilityRegistryConsistent = void 0;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const build_ability_js_1 = require("../build-ability.js");
|
|
10
|
+
const REGISTRY_MARKER = 'interface SubjectRegistryInterface {';
|
|
11
|
+
const PRODUCTION_EXTENSIONS = new Set(['.ts', '.vue']);
|
|
12
|
+
const actionsOf = (declaration) => declaration
|
|
13
|
+
.split('|')
|
|
14
|
+
.map((action) => action.replaceAll(/[^\w-]/gu, ''))
|
|
15
|
+
.filter((action) => action.length > 0 && action !== 'never');
|
|
16
|
+
const withoutComments = (source) => source
|
|
17
|
+
.split('\n')
|
|
18
|
+
.filter((line) => !(/^\s*(?:\/\/|\*|\/\*)/u).test(line))
|
|
19
|
+
.join('\n');
|
|
20
|
+
const registryEntries = (code) => {
|
|
21
|
+
const start = code.indexOf(REGISTRY_MARKER) + REGISTRY_MARKER.length;
|
|
22
|
+
return code.slice(start, code.indexOf('}', start))
|
|
23
|
+
.split('\n')
|
|
24
|
+
.map((line) => line.trim())
|
|
25
|
+
.filter((line) => line.includes(':'))
|
|
26
|
+
.map((line) => [line.slice(0, line.indexOf(':')).trim(), actionsOf(line.slice(line.indexOf(':') + 1))]);
|
|
27
|
+
};
|
|
28
|
+
const grantedPairs = (roles, grants) => {
|
|
29
|
+
const rules = roles.flatMap((role) => (0, build_ability_js_1.buildAbilityFor)({ role }, grants).rules);
|
|
30
|
+
return new Set(rules.flatMap((rule) => [rule.action].flat().flatMap((action) => [rule.subject].flat().map((subject) => `${action} ${subject}`))));
|
|
31
|
+
};
|
|
32
|
+
const productionSources = async (sourceRoot, producers) => {
|
|
33
|
+
const files = await (0, promises_1.readdir)(sourceRoot, { recursive: true });
|
|
34
|
+
const sources = files.filter((file) => PRODUCTION_EXTENSIONS.has(node_path_1.default.extname(file))
|
|
35
|
+
&& !file.endsWith('.spec.ts')
|
|
36
|
+
&& !file.split(node_path_1.default.sep).includes('tests')
|
|
37
|
+
&& !producers.includes(file));
|
|
38
|
+
return Promise.all(sources.map(async (file) => (0, promises_1.readFile)(node_path_1.default.join(sourceRoot, file), 'utf8')));
|
|
39
|
+
};
|
|
40
|
+
const assertAbilityRegistryConsistent = async (options) => {
|
|
41
|
+
const { grants, grantsFiles, roles, sourceRoot } = options;
|
|
42
|
+
const producers = await Array.fromAsync((0, promises_1.glob)(grantsFiles, { cwd: sourceRoot }));
|
|
43
|
+
const [codes, sources] = await Promise.all([
|
|
44
|
+
Promise.all(producers.map(async (file) => withoutComments(await (0, promises_1.readFile)(node_path_1.default.join(sourceRoot, file), 'utf8')))),
|
|
45
|
+
productionSources(sourceRoot, producers),
|
|
46
|
+
]);
|
|
47
|
+
const entries = codes.filter((code) => code.includes(REGISTRY_MARKER)).flatMap((code) => registryEntries(code));
|
|
48
|
+
const granted = grantedPairs(roles, grants);
|
|
49
|
+
const violations = [
|
|
50
|
+
...entries.length === 0 ? [`шаблон ${grantsFiles} не нашёл записей реестра`] : [],
|
|
51
|
+
...entries.flatMap(([subject, actions]) => actions
|
|
52
|
+
.map((action) => `${action} ${subject}`)
|
|
53
|
+
.filter((pair) => !granted.has(pair))
|
|
54
|
+
.map((pair) => `запись реестра «${pair}» не выдана ни одним грантом`)),
|
|
55
|
+
...entries
|
|
56
|
+
.map(([subject]) => subject)
|
|
57
|
+
.filter((subject) => sources.every((source) => !source.includes(`'${subject}'`)))
|
|
58
|
+
.map((subject) => `subject ${subject} не проверяется в prod-коде`),
|
|
59
|
+
];
|
|
60
|
+
if (violations.length > 0) {
|
|
61
|
+
throw new Error(`Гейт реестра abilities:\n${violations.join('\n')}`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
exports.assertAbilityRegistryConsistent = assertAbilityRegistryConsistent;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './assert-ability-registry-consistent.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./assert-ability-registry-consistent.js"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AbilityGrantMap } from '../types/ability-grant.js';
|
|
2
|
+
import type { Role } from '../types/roles.js';
|
|
3
|
+
export interface AbilityRegistryOptionsInterface {
|
|
4
|
+
readonly grants: AbilityGrantMap;
|
|
5
|
+
readonly grantsFiles: string;
|
|
6
|
+
readonly roles: readonly Role[];
|
|
7
|
+
readonly sourceRoot: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const assertAbilityRegistryConsistent: (options: AbilityRegistryOptionsInterface) => Promise<void>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { glob, readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { buildAbilityFor } from '../build-ability.js';
|
|
4
|
+
const REGISTRY_MARKER = 'interface SubjectRegistryInterface {';
|
|
5
|
+
const PRODUCTION_EXTENSIONS = new Set(['.ts', '.vue']);
|
|
6
|
+
const actionsOf = (declaration) => declaration
|
|
7
|
+
.split('|')
|
|
8
|
+
.map((action) => action.replaceAll(/[^\w-]/gu, ''))
|
|
9
|
+
.filter((action) => action.length > 0 && action !== 'never');
|
|
10
|
+
const withoutComments = (source) => source
|
|
11
|
+
.split('\n')
|
|
12
|
+
.filter((line) => !(/^\s*(?:\/\/|\*|\/\*)/u).test(line))
|
|
13
|
+
.join('\n');
|
|
14
|
+
const registryEntries = (code) => {
|
|
15
|
+
const start = code.indexOf(REGISTRY_MARKER) + REGISTRY_MARKER.length;
|
|
16
|
+
return code.slice(start, code.indexOf('}', start))
|
|
17
|
+
.split('\n')
|
|
18
|
+
.map((line) => line.trim())
|
|
19
|
+
.filter((line) => line.includes(':'))
|
|
20
|
+
.map((line) => [line.slice(0, line.indexOf(':')).trim(), actionsOf(line.slice(line.indexOf(':') + 1))]);
|
|
21
|
+
};
|
|
22
|
+
const grantedPairs = (roles, grants) => {
|
|
23
|
+
const rules = roles.flatMap((role) => buildAbilityFor({ role }, grants).rules);
|
|
24
|
+
return new Set(rules.flatMap((rule) => [rule.action].flat().flatMap((action) => [rule.subject].flat().map((subject) => `${action} ${subject}`))));
|
|
25
|
+
};
|
|
26
|
+
const productionSources = async (sourceRoot, producers) => {
|
|
27
|
+
const files = await readdir(sourceRoot, { recursive: true });
|
|
28
|
+
const sources = files.filter((file) => PRODUCTION_EXTENSIONS.has(path.extname(file))
|
|
29
|
+
&& !file.endsWith('.spec.ts')
|
|
30
|
+
&& !file.split(path.sep).includes('tests')
|
|
31
|
+
&& !producers.includes(file));
|
|
32
|
+
return Promise.all(sources.map(async (file) => readFile(path.join(sourceRoot, file), 'utf8')));
|
|
33
|
+
};
|
|
34
|
+
export const assertAbilityRegistryConsistent = async (options) => {
|
|
35
|
+
const { grants, grantsFiles, roles, sourceRoot } = options;
|
|
36
|
+
const producers = await Array.fromAsync(glob(grantsFiles, { cwd: sourceRoot }));
|
|
37
|
+
const [codes, sources] = await Promise.all([
|
|
38
|
+
Promise.all(producers.map(async (file) => withoutComments(await readFile(path.join(sourceRoot, file), 'utf8')))),
|
|
39
|
+
productionSources(sourceRoot, producers),
|
|
40
|
+
]);
|
|
41
|
+
const entries = codes.filter((code) => code.includes(REGISTRY_MARKER)).flatMap((code) => registryEntries(code));
|
|
42
|
+
const granted = grantedPairs(roles, grants);
|
|
43
|
+
const violations = [
|
|
44
|
+
...entries.length === 0 ? [`шаблон ${grantsFiles} не нашёл записей реестра`] : [],
|
|
45
|
+
...entries.flatMap(([subject, actions]) => actions
|
|
46
|
+
.map((action) => `${action} ${subject}`)
|
|
47
|
+
.filter((pair) => !granted.has(pair))
|
|
48
|
+
.map((pair) => `запись реестра «${pair}» не выдана ни одним грантом`)),
|
|
49
|
+
...entries
|
|
50
|
+
.map(([subject]) => subject)
|
|
51
|
+
.filter((subject) => sources.every((source) => !source.includes(`'${subject}'`)))
|
|
52
|
+
.map((subject) => `subject ${subject} не проверяется в prod-коде`),
|
|
53
|
+
];
|
|
54
|
+
if (violations.length > 0) {
|
|
55
|
+
throw new Error(`Гейт реестра abilities:\n${violations.join('\n')}`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './assert-ability-registry-consistent.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './assert-ability-registry-consistent.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@application-kit/authorization",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Авторизация на CASL для NestJS и браузера: реестры subject'ов и ролей, ability из грантов модулей, глобальный guard политик, гвард маршрутов Vue",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authorization",
|
|
@@ -44,6 +44,16 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"./package.json": "./package.json",
|
|
47
|
+
"./testing": {
|
|
48
|
+
"import": {
|
|
49
|
+
"types": "./dist/esm/testing/index.d.ts",
|
|
50
|
+
"default": "./dist/esm/testing/index.js"
|
|
51
|
+
},
|
|
52
|
+
"require": {
|
|
53
|
+
"types": "./dist/cjs/testing/index.d.ts",
|
|
54
|
+
"default": "./dist/cjs/testing/index.js"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
47
57
|
"./vue": {
|
|
48
58
|
"import": {
|
|
49
59
|
"types": "./dist/esm/vue/index.d.ts",
|
|
@@ -62,7 +72,7 @@
|
|
|
62
72
|
"dist"
|
|
63
73
|
],
|
|
64
74
|
"scripts": {
|
|
65
|
-
"build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src --entry-only=nest,internal,vue && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
|
|
75
|
+
"build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src --entry-only=nest,internal,testing,vue && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
|
|
66
76
|
"lint:tsc": "tsc --noEmit --pretty --project .",
|
|
67
77
|
"test:mutation": "stryker run",
|
|
68
78
|
"test:unit": "vitest run"
|
|
@@ -70,28 +80,28 @@
|
|
|
70
80
|
"devDependencies": {
|
|
71
81
|
"@casl/ability": "^7.0.1",
|
|
72
82
|
"@casl/vue": "^3.0.1",
|
|
73
|
-
"@nestjs/common": "^
|
|
74
|
-
"@nestjs/core": "^
|
|
75
|
-
"@nestjs/testing": "^
|
|
83
|
+
"@nestjs/common": "^12.0.0",
|
|
84
|
+
"@nestjs/core": "^12.0.0",
|
|
85
|
+
"@nestjs/testing": "^12.0.0",
|
|
76
86
|
"@stryker-mutator/core": "^10.0.0",
|
|
77
87
|
"@stryker-mutator/vitest-runner": "^10.0.0",
|
|
78
88
|
"@types/node": "^26.2.0",
|
|
79
|
-
"nestjs-pino": "^
|
|
89
|
+
"nestjs-pino": "^5.0.0",
|
|
80
90
|
"pino": "^10.3.1",
|
|
81
91
|
"pino-http": "^11.0.0",
|
|
82
92
|
"reflect-metadata": "^0.2.2",
|
|
83
93
|
"rxjs": "^7.8.2",
|
|
84
94
|
"typescript": "^6.0.3",
|
|
85
|
-
"vitest": "^
|
|
95
|
+
"vitest": "^5.0.0",
|
|
86
96
|
"vue": "^3.5.41",
|
|
87
97
|
"vue-router": "^5.2.0"
|
|
88
98
|
},
|
|
89
99
|
"peerDependencies": {
|
|
90
100
|
"@casl/ability": "^7.0.0",
|
|
91
101
|
"@casl/vue": "^3.0.0",
|
|
92
|
-
"@nestjs/common": "^11.0.0",
|
|
93
|
-
"@nestjs/core": "^11.0.0",
|
|
94
|
-
"nestjs-pino": "^4.6.1",
|
|
102
|
+
"@nestjs/common": "^11.0.0 || ^12.0.0",
|
|
103
|
+
"@nestjs/core": "^11.0.0 || ^12.0.0",
|
|
104
|
+
"nestjs-pino": "^4.6.1 || ^5.0.0",
|
|
95
105
|
"vue": "^3.5.0",
|
|
96
106
|
"vue-router": "^5.0.0"
|
|
97
107
|
},
|