Допустим, у меня есть приложение, которое позволяет пользователям создавать, назначать и редактировать задачи. Приложение создано для поддержки нескольких учетных записей клиентов. Это приложение имеет различные роли, но мы сосредоточимся на трех и их разрешениях, относящихся к конкретному действию и ресурсу (обновление текста в задаче) — администраторы (имеют разрешения на обновление текста во всех задачах во всех учетных записях), администраторы учетных записей (имеют разрешения на обновление текста в любой задаче, принадлежащей их учетным записям, но они могут принадлежать нескольким учетным записям) и пользователи учетной записи (имеют разрешения на обновление текста в задаче, только если эта задача назначена им).
Пример немного надуманный, а имена ролей слишком общие, но потерпите меня.
Цель здесь — попытаться найти аккуратный способ разделения ролей и разрешений, но кажется, что роли неизбежно привязаны к разрешениям (см. код ниже).
Возможно, разрешение должно быть просто task:updateText
, но как тогда проверить роли? Могу ли я переместить свой блок switch (actor.type)
в модели домена в службу домена и проверить там, связан ли пользователь с администратором, администратором учетной записи или пользователем учетной записи в этой конкретной учетной записи? Данные могут быть кэшированы, но администраторы учетных записей (и, возможно, другие пользователи) могут быть связаны с несколькими учетными записями, что означает, что предварительная загрузка этих данных может потребовать слишком много данных в контексте и может быть проблематичной, поскольку эти данные передаются между службами.
Проверки владения/назначения выполняются как часть предметной области, поскольку они зависят от текущего состояния модели. Здесь не рассматривается, но используется простой механизм управления версиями, гарантирующий, что модель не изменится между моментом ее извлечения и моментом применения обновлений. Кажется, что политики могут, по крайней мере, сделать эту логику чище, но если бы я переместил эту логику в политику, я не уверен, как я буду продолжать гарантировать это, если бы политика и метод обслуживания не имели способа гарантировать, что они используют одну и ту же версию. ресурса.
Каковы мои варианты здесь? Любое руководство будет высоко оценено.
class TaskApplicationService {
constructor(private taskRepository: TaskRepository) { }
async updateText({ taskId, text, accountId, context }: { taskId: string, text: string, accountId?: string, context: Context }) {
let actor: Actor;
const userId = context.user.id;
// permissions follow pattern resource:action:qualifier
if (await hasPermission('task:updateText:all')) {
actor = await anAdmin({ userId });
} else if (await hasPermission('task:updateText:account')) {
actor = await anAccountAdmin({ accountId, userId });
} else if (await hasPermission('task.updateText:assigned')) {
actor = await anAccountUser({ accountId, userId });
} else {
throw new Error('not authorized');
}
const task = await this.taskRepository.findOne({ taskId });
task.updateText({ text, actor });
await this.taskRepository.save(task);
// return TaskMapper.toDto(task);
}
}
class TaskDomainModel {
private props: {
text: string,
accountId: string,
assignedAccountUserId: string;
};
get text(): string {
return this.props.text;
}
updateText({ text, actor }: { text: string, actor: Actor }) {
switch (actor.type) {
case ActorType.ADMIN:
break;
case ActorType.ACCOUNT_ADMIN:
assert(this.props.accountId === actor.tenantId);
break;
case ActorType.ACCOUNT_USER:
assert(this.props.accountId === actor.tenantId);
assert(this.props.assignedAccountUserId === actor.tenantUserId);
break;
default:
// note assertions and throwing errors are here for brevity,
// but normally would use something similar to this:
// https://khalilstemmler.com/articles/enterprise-typescript-nodejs/handling-errors-result-class/
throw new Error('unknown actor type');
}
this.props.text = text;
}
}
// supporting cast
interface User {
id: string;
}
interface Context {
user: User;
}
enum ActorType {
ADMIN,
ACCOUNT_ADMIN,
ACCOUNT_USER
}
interface Admin {
type: ActorType.ADMIN,
userId: string
}
interface AccountAdmin {
type: ActorType.ACCOUNT_ADMIN,
tenantId: string,
userId: string
}
interface AccountUser {
type: ActorType.ACCOUNT_USER,
tenantUserId: string,
tenantId: string,
userId: string
}
async function anAdmin({ userId }: { userId: string }): Promise<Admin> {
// gets an admin
}
async function anAccountAdmin({ accountId, userId }: { accountId: string, userId: string }): Promise<AccountAdmin> {
// gets an account admin
}
async function anAccountUser({ accountId, userId }: { accountId: string, userId: string }): Promise<AccountUser> {
// gets an account user
}
async function hasPermission(permission: string) {
// checks permissions in cache or calls to external service
}
type Actor = Admin | AccountAdmin | AccountUser;
interface TaskRepository {
findOne({ taskId }: { taskId: string }): Promise<TaskModel>;
save(task: TaskModel): Promise<TaskModel>;
}