typescript - DDD hvordan man riktig skiller mellom tillatelser og roller
La oss si at jeg har en applikasjon som lar brukere opprette, tildele og redigere oppgaver. Applikasjonen er bygget for å støtte flere kundekontoer. Denne applikasjonen har ulike roller, men vi vil fokusere på tre og deres tillatelser knyttet til en spesifikk handling og ressurs (oppdatering av teksten på en oppgave) - administratorer (har tillatelser til å oppdatere teksten på alle oppgaver på tvers av alle kontoer), kontoadministratorer (har tillatelser til å oppdatere teksten på hvilken som helst oppgave som tilhører deres kontoer, men de kan tilhøre flere kontoer) og kontobrukere (har tillatelser til å oppdatere teksten på en oppgave bare hvis den oppgaven er tildelt dem).
Eksemplet er litt påtatt, og rollenavnene er litt overgeneralisert, men hold ut med meg.
Målet her er å prøve å finne en ryddig måte å skille roller og tillatelser på, men det ser ut til at rollene uunngåelig er knyttet til tillatelsene (se koden nedenfor).
Kanskje bør tillatelsen bare være task:updateText, men hvordan sjekker jeg rollene da? Ville jeg flyttet min switch (actor.type)-blokk i domenemodellen til en domenetjeneste og sjekket der om brukeren er tilknyttet som admin, kontoadministrator eller konto-bruker på den aktuelle kontoen? Data kan caches, men kontoadministratorer (og potensielt andre brukere) kan være tilknyttet flere kontoer, noe som betyr at forhåndslasting av disse dataene kan kreve for mye data i konteksten og kan være problematisk når disse dataene sendes mellom tjenester.
Eierskaps-/tildelingskontroller utføres som en del av domenet siden de avhenger av modellens nåværende tilstand. Ikke dekket her, men en enkel versjonsmekanisme brukes for å sikre at en modell ikke endres mellom det tidspunktet den hentes og når oppdateringer er anvendt. Det ser ut til at retningslinjer i det minste kan gjøre denne logikken renere, men hvis jeg skulle flytte denne logikken ut i en retningslinje, er jeg usikker på hvordan jeg fortsatt kunne garantere det med mindre retningslinjen og tjenestemetoden hadde en måte å sikre at de delte samme versjon av ressursen.
Hva er alternativene mine her? Enhver veiledning ville være veldig verdsatt.
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>;
}
Svar #1
Jeg tror det ikke er nødvendig å koble roller til tillatelser så strengt. I stedet kan du ha en rolleklasse som inneholder en liste over generiske tillatelser som nedenfor.
class Permission {
name: String;
}
class Role {
name: String;
permissions: Permission[];
}
class User {
id: String;
role: Role;
}
På denne måten kan du endre hvilke tillatelser roller faktisk har.
En annen ting som bekymrer meg er at logikken for tilgangssjekk er spredt overalt, noe som svekker kodevedlikeholdbarheten. I stedet kan du trekke det ut til en enkelt tjeneste. Her er et grovt utkast.
class PermissionService {
userHasRightToUpdateText = (task : Task, user: User) => {
return (user.role.permissions.some(p => p.name == 'task:updateText:all')
|| (user.role.permissions.some(p => p.name == 'task:updateText:account') && task.accountId == user.id));
//or whatever you auth logic is
}
}
class TaskApplicationService {
constructor(private taskRepository: TaskRepository,
private userRepository: UserRepository,
private permissionService : PermissionService) { }
async updateText({ taskId, text, accountId, context }: { taskId: string, text: string, accountId?: string, context: Context }) {
const userId = context.user.id;
const user = await this.userRepository.findOne({ userId });
const task = await this.taskRepository.findOne({ taskId });
if (this.permissionService.userHasRightToUpdateText(task, user)) {
task.updateText({ text });
await this.taskRepository.save(task);
}
else {
throw new Error('not authorized');
}
// return TaskMapper.toDto(task);
}
}
Håper dette hjelper.
