@application-kit/authorization
v0.5.0
Published
Авторизация на CASL для NestJS и браузера: реестры subject'ов и ролей, ability из грантов модулей, глобальный guard политик, гвард маршрутов Vue
Maintainers
Readme
@application-kit/authorization
Авторизация на CASL для NestJS-приложений с браузерным клиентом. Пакет не знает ни одной роли, subject'а и правила: словарь и гранты объявляют модули приложения, механизм собирает из них ability и проверяет политики. Одна и та же карта грантов строит ability и на сервере (проверка доступа), и в браузере (скрытие недоступного UI).
Установка
yarn add @application-kit/authorizationPeer-зависимости: @casl/ability, @nestjs/common, @nestjs/core, nestjs-pino — обязательные; @casl/vue, vue,
vue-router — опциональные, нужны только входу /vue.
@nestjs/graphql пакету не нужен: guard узнаёт GraphQL-запрос по типу контекста и работает и в приложении без GraphQL.
Входы
| Вход | Для чего | Тянет |
|------|----------|-------|
| @application-kit/authorization | сервер и браузер: реестры, buildAbilityFor, composeAbilityGrants | только @casl/ability |
| @application-kit/authorization/nest | сервер: guard, декораторы, фабрика, логгер | @nestjs/*, nestjs-pino |
| @application-kit/authorization/vue | клиент и SSR: ability на приложение, <Can>, гвард маршрутов по meta.can | vue, vue-router, @casl/vue |
| @application-kit/authorization/testing | спека продукта: гейт реестра abilities | node:fs, @casl/ability |
Корневой вход не импортирует Nest — его можно собирать в браузерный бандл.
Словарь приложения
Роли, subject'ы и их action'ы объявляются расширением реестров пакета (module augmentation):
declare module '@application-kit/authorization' {
interface RoleRegistryInterface { EDITOR: true, VIEWER: true }
interface SubjectRegistryInterface { Article: 'publish' | 'read' }
}Пока реестр ролей пуст, тип роли — never: грант нельзя выдать роли, которой приложение не объявило.
Гранты
Модуль объявляет фрагмент карты «роль → грант» рядом со своим subject'ом; фрагменты складываются в одну карту. Гранты одной роли из разных модулей складываются, а не перезаписывают друг друга. Роль без гранта не получает ни одного правила (deny-by-default).
const grants = composeAbilityGrants(['EDITOR', 'VIEWER'], articleGrants, commentGrants)
const ability = buildAbilityFor({ role: 'EDITOR' }, grants)Vue
installAuthorization(app, options) ставит ability (свой экземпляр на приложение, поэтому и на запрос SSR), <Can>,
useAbility() и гвард: маршрут с meta.can без нужного права уводит на accessDenied. Решение только о показе
интерфейса: источник истины — бэкенд. RouteMeta.can объявляет сам вход.
installAuthorization(app, {
accessDenied: { name: 'ACCESS_DENIED' }, // сам не несёт meta.can: отказ на нём зациклился бы
grants,
resolveActor: async () => meRepository.loadMe(), // null — гость; исполняется в контексте приложения
router,
})
const routes = [{ path: '/console', component: Console, meta: { can: { action: 'read', subject: 'Console' } } }]Актор запрашивается лениво, на первом маршруте с meta.can: маршруты без него запроса не ждут. Ошибка resolveActor
читается как гость, и следующий такой переход спросит снова — это и покрывает вход без перезагрузки страницы. Актор,
приехавший снапшотом SSR, resolveActor возвращает сам, без запроса. canAccessRoute(ability, route) — то же решение
отдельно от гварда.
Nest
PoliciesGuard— глобальный guard (APP_GUARD): обработчик с@Public()пропускается, с@CheckPolicies(...)— проверяется конъюнкцией политик над ability актора изrequest.user. GraphQL-резолвер без обоих декораторов запрещён (fail-closed) с предупреждением в лог, REST-обработчик — пропускается: его авторизация на маршруте.CheckPolicies(...handlers),PolicyHandler,permitAuthenticated— политики обработчика.Public(),IS_PUBLIC_KEY— открытый обработчик.withPublicRoutes(AuthGuard('jwt'))— guard аутентификации приложения поверх его базы: открытый обработчик пропускает мимо базы, а запрос ей отдаёт и на HTTP, и в GraphQL.@nestjs/passportпакету не нужен.CurrentActor()— параметр обработчика сrequest.user; на открытом обработчике актора может не быть.resolveExecutionRequest(context)— запрос без развилки транспорта.@nestjs/graphqlпакету не нужен.AbilityFactory— ability актора по карте грантов.
Собирает механизм AuthorizationModule.forRoot({ grants }): фабрика ability, логгер и глобальный guard (APP_GUARD)
провайдятся пакетом, приложение приносит только карту грантов. Наружу модуль отдаёт ABILITY_FACTORY, ABILITY_GRANTS
и AbilityFactory — для кода, который строит ability актора сам (например, резолвер, отдающий её клиенту).
@Module({
imports: [AuthorizationModule.forRoot({ grants })],
})
export class AppModule {}AuthorizationLogger — pino-логгер пакета с контекстом AuthorizationLogger; собирается из глобального
LoggerModule nestjs-pino.
Гейт реестра
Реестр subject'ов живёт только в типах: забытая регистрация не падает в рантайме. assertAbilityRegistryConsistent из
входа @application-kit/authorization/testing читает блоки declare module файлов грантов и падает, если запись не
выдана грантом продукта или её subject не проверяется в prod-коде:
await assertAbilityRegistryConsistent({
grants: abilityGrants,
grantsFiles: 'modules/*/module.grants.ts',
roles: Object.values(ROLE_VALUE),
sourceRoot: path.resolve(import.meta.dirname, '..'),
})