Формирование доступов
Permission - это доступ к функционалу, обусловленный бизнес требованиями.
Как выделять permission
Требование является permission, если:
- Требование указывает на то, что функционал приложения должен быть ограничен на основе данных об аккаунте (роль, организация, оплата...)
- Описанное ограничение может быть снято при изменении данных пользователя
Permission не является:
- Временный Feature Toggle на функционал
- Перманентное условие на блокировку функционала, без возможности открытия доступа (даже при изменении роли пользователя)
- Условия (if) в коде, реализующие рядовые бизнес требования
Примеры требований
Кнопка "Создать документ" доступна только администратору
- это permission потому,
что ограничение основывается на данных пользователя и доступ к функционалу можно открыть, если изменить роль пользователя.
Кнопка "Реда ктировать документ" доступна только для пользователей с платным аккаунтом и добавленной организацией
- это permission потому,
что ограничение основывается на данных пользователя и доступ к функционалу можно открыть, если оплатить аккаунт и добавить организацию.
Кнопка "Создать документ" заблокирована до 20.05.2024
- это не permission,
а feature toggle. Требование не зависит от данных пользователя.
При этом:
Кнопка "Создать документ" для пользователей с тарифом "Базовый" заблокирована до 20.05.2024
- это уже permission потому, что основывается на данных пользователя.
Кнопка "Отправить" заблокирована, если чекбокс "Ознакомлен с требованиями" не активен
- это не permission. Это требование для формы.
Создание permissions посредством @astral/permissions
Permissions создаются только внутри policy с помощью метода policy.createPermission
:
modules/permissions/domain/stores/PermissionsStore/policies/AdministrationPolicyStore
import { makeAutoObservable } from 'mobx';
import type { UserRepository } from '@example/data';
import { PermissionDenialReason } from '../../../../enums';
// @astral/permissions в реальном коде должен реэкспортироваться через shared
import { PolicyManagerStore, Policy } from '@astral/permissions';
export class AdministrationPolicyStore {
private readonly policy: Policy;
constructor(
private readonly policyManager: PolicyManagerStore,
private readonly userRepo: UserRepository,
) {
makeAutoObservable(this, {}, { autoBind: true });
this.policy = this.policyManager.createPolicy({
name: 'administration',
prepareData: async (): Promise<void> => {
await Promise.all([this.userRepo.getRolesQuery().async()]);
},
});
}
/**
* Доступ к действиям администратора
*/
public get administrationActions() {
return this.policy.createPermission((allow, deny) => {
if (this.userRepo.getRolesQuery().data?.isAdmin) {
// разрешает доступ
return allow();
}
// запрещает доступ с конкретной причиной
deny(PermissionDenialReason.NoAdmin);
});
}
}
API Permission
createPermission
возвращает объект вида:
type Permission = {
isAllowed: boolean;
/**
* Причина отказа в доступе
*/
reason?: string;
/**
* @example permission.hasReason(DenialReason.NoAdmin)
*/
hasReason: (reason: string) => boolean;
};
О причинах отказа читайте далее.
Пример реализации и использования permission
Требования
Кнопка "Создать книгу" в Sidebar отображается только если пользователь является администратором.
Решение
modules/permissions/domain/stores/PermissionsStore/policies/AdministrationPolicyStore
class AdministrationPolicyStore {
constructor(
private readonly policyManager: PolicyManagerStore,
private readonly userRepo: UserRepository,
) {
makeAutoObservable(this, {}, { autoBind: true });
this.policyManager.createPolicy({
name: 'administration',
prepareData: async (): Promise<void> => {
await Promise.all([this.userRepo.getRolesQuery().async()]);
},
});
}
/**
* Доступ к действиям администратора
*/
public get administrationActions() {
return this.policyManager.createPermission((allow, deny) => {
if (this.userRepo.getRolesQuery().data?.isAdmin) {
return allow();
}
deny(PermissionDenialReason.NoAdmin);
});
}
}
В features необходимо избегать разрешения доступов через абстрактные компоненты вида:
import { observer } from 'mobx-react-lite';
import { permissionsStore } from '@example/modules/permissions';
export const Sidebar = observer(() => {
return (
<Sidebar>
<PermissionsGateway
permission={permissionsStore.administration.administrationActions}
allow={
<RouterLink to={APP_ROUTES.createBook.getRedirectPath()}>
Создать книгу
</RouterLink>
}
/>
</Sidebar>
);
});
Использование компонентов вроде PermissionsGateway
переносит логику доступов для фичи в UI слой, что нарушает архитектурную концепцию.
Разрешение доступов должно происходить в UIStore
:
modules/layout/features/MainLayout/Sidebar/UIStore
export class UIStore {
constructor(private readonly permissions: PermissionsStore) {
makeAutoObservable(this, {}, { autoBind: true });
}
public get isAllowedBookCreation() {
return this.permissions.administration.administrationActions.isAllowed;
}
}
modules/layout/features/MainLayout/Sidebar/Sidebar.tsx
export const Sidebar = observer(() => {
const [{ isAllowedBookCreation }] = useState(createUIStore);
return (
<Sidebar>
<SidebarItem>
{isAllowedBookCreation && (
<RouterLink to={APP_ROUTES.createBook.getRedirectPath()}>
Создать книгу
</RouterLink>
)}
</SidebarItem>
</Sidebar>
);
});
Permission не должен зависеть от UI
Permission не должен напрямую зависеть и указывать на UI, который блокируется. Зависимость от UI приведет к взрывному росту permissions и в последствии к сложной поддержке кода.
Пример
Кнопка "Создать документ" отображается только если пользователь является администратором
- в данном требовании присутствует указать на конкретную кнопку.
Неправильное решение
Создать permission showCreationDocButton
:
class AdministrationPolicyStore {
...
public get showCreationDocButton() {
return this.policyManager.createPermission((allow, deny) => {
if (this.userRepo.getRolesQuery().data?.isAdmin) {
return allow();
}
deny(PermissionDenialReason.NoAdmin);
});
}
public get allowAdministrationRoute() {
return this.policyManager.createPermission((allow, deny) => {
if (this.userRepo.getRolesQuery().data?.isAdmin) {
return allow();
}
deny(PermissionDenialReason.NoAdmin);
});
}
public get showEditingDocModal() {
return this.policyManager.createPermission((allow, deny) => {
if (this.userRepo.getRolesQuery().data?.isAdmin) {
return allow();
}
deny(PermissionDenialReason.NoAdmin);
});
}
}
Как видно из примера, при связывании permission и UI происходит взрывной рост одинаковых доступов:
- Доступ к кнопке
- Доступ к руту
- Доступ к модалке редактирования
Правильное решение
Создать абстрактный permission administrationActions
, который будет закрывать доступ к действиям администратора:
class AdministrationPolicyStore {
...
public get administrationActions() {
return this.policyManager.createPermission((allow, deny) => {
if (this.userRepo.getRolesQuery().data?.isAdmin) {
return allow();
}
deny(PermissionDenialReason.NoAdmin);
});
}
}
Теперь на уровне features необходимо проверять administrationActions
и на основе его выполнять необходимые действия.
Нейминг
Название permission должно отвечать на вопрос: "Доступ открыт/закрыт для чего/к чему?".
Примеры
- Доступ закрыт к действиям администратора -
administrationActions
- Доступ закрыт к чтению книги онлайн -
readingBook
- Доступ закрыт к управлению организацией -
organizationManagement
✅ Valid
public get administrationActions() { ... }
public get addingToShelf() { ... }
public get readingBook() { ... }
public get organizationManagement() { ... }
❌ Invalid
public get canReadingBook() { ... }
public get isAddToShelf() { ... }
Нейминг методов для вычисления доступов
Для методов, которые вычисляют доступы, добавляется префикс calc
.
✅ Valid
public calcReadingBook = (bookId: string) => { ... }
❌ Invalid
public checkReadingBook = (bookId: string) => { ... }