สมมติว่าฉันมีแอปพลิเคชันที่อนุญาตให้ผู้ใช้สร้าง มอบหมาย และแก้ไขงานได้ แอปพลิเคชันนี้สร้างขึ้นเพื่อรองรับบัญชีลูกค้าหลายบัญชี แอปพลิเคชันนี้มีบทบาทที่หลากหลาย แต่เราจะมุ่งเน้นไปที่สามบทบาทและการอนุญาตที่เกี่ยวข้องกับการดำเนินการและทรัพยากรเฉพาะ (การอัปเดตข้อความในงาน) - ผู้ดูแลระบบ (มีสิทธิ์ในการอัปเดตข้อความในงานทั้งหมดในทุกบัญชี) ผู้ดูแลบัญชี (มีสิทธิ์ในการอัปเดตข้อความในงานใด ๆ ที่เป็นของบัญชีของพวกเขา แต่อาจเป็นของหลายบัญชี) และผู้ใช้บัญชี (มีสิทธิ์ในการอัปเดตข้อความในงานเฉพาะในกรณีที่งานนั้นได้รับมอบหมายให้พวกเขา)
ตัวอย่างนี้ค่อนข้างซับซ้อนและชื่อบทบาทก็ดูกว้างเกินไปนิดหน่อย แต่ก็ต้องทนกับฉัน
เป้าหมายที่นี่คือพยายามค้นหาวิธีที่เรียบร้อยในการแยกบทบาทและการอนุญาต แต่ดูเหมือนว่าบทบาทจะเชื่อมโยงกับการอนุญาตอย่างหลีกเลี่ยงไม่ได้ (ดูโค้ดด้านล่าง)
บางทีการอนุญาตควรเป็น 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>;
}