【发布时间】:2020-03-26 11:25:25
【问题描述】:
假设我有一个允许用户创建、分配和编辑任务的应用程序。该应用程序旨在支持多个客户帐户。此应用程序具有各种角色,但我们将重点关注三个及其与特定操作和资源相关的权限(更新任务的文本) - 管理员(有权更新所有帐户的所有任务的文本),帐户管理员(有权更新属于其帐户的任何任务的文本,但他们可以属于多个帐户)和帐户用户(只有在任务分配给他们时才有权更新该任务的文本)。
这个例子有点做作,角色名称有点过于笼统,但请耐心等待。
这里的目标是尝试找到一种巧妙的方法来分离角色和权限,但似乎角色不可避免地与权限相关联(参见下面的代码)。
也许权限应该只是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>;
}
【问题讨论】:
-
让你的问题更简洁,或许早点得到答案。
标签: typescript security permissions authorization domain-driven-design