Generally most of the authorization in the system is done in authorization layer which then calls commands/queries from application layer, which call methods on domain aggregates. However there are several cases when authorization has to be done in same transaction where aggregate method is executed and this authorization requires some info/fields from aggregate that is going to be modified (for example user can edit only his own posts - we know if post belongs to user only after reading it from storage/db).
API -> controller (authorization) -> command/query -> aggregate.method()
Here is how example command looks like:
import _ from 'lodash';
import * as di from '@/framework/di';
import * as app from '@/framework/application';
import { ConferenceRepositoryImplMemory, Conference } from './../../../infrastructure/repositories/conference-repository-impl-memory';
interface UpdateConferencePayload {
conference_guid: string;
name: string;
short_name: string;
website_url: string;
}
@di.perResolutionChain()
export class UpdateConferenceCommand extends app.Command<UpdateConferencePayload, void> {
constructor(
private readonly conferenceRepository: ConferenceRepositoryImplMemory,
) {
super();
}
public async execute(payload: UpdateConferencePayload) {
const conference = await this.conferenceRepository.getByGuid(payload.conference_guid);
conference.changeName(_.pick(payload, ['name', 'short_name']));
conference.changeWebsite(_.pick(payload, ['website_url']));
this.conferenceRepository.update(conference);
await this.conferenceRepository.saveChanges();
}
}
I think it can be done in several ways:
make command/query to take extra argument in addition to payload for example "checkIfPostBelongToUser(postOwnerGuid)" and then call it (with proper aggregate field) just after reading aggregate from db but before invoking aggregate method. However I think this is bad for testing the command because you have to test authorization in addition to command "behaviour". Also it's possible that same command could be called by moderator for example who can edit others posts or by internal system which does not have to be authorized at all. It would be possible to make this extra authorization command param optional though...
another option is to instead of one command "editPost()" make two commands "editOwnPost(..., userGuid)", "editPost()". Former one would throw exception if post aggregate read from storage has incorrect ownerGuid. In this case authorization could be done in authorization layer (if user is moderator then editPost().execute() else editOwnPost().execute. However I am not sure it this is a way to go because it's defining something between authorization and business logic on command level... also commands don't match domain logic and it feels like it should be done on domain level instead but again putting authorization to domain level is not a good thing AFAIK.
the last option I can think of is to start transaction in which command runs before command is executed, read aggregate, check it's fields (owner for example) and then if it's ok execute command. Thanks to dependency injection it would not require duplicated reads from storage etc, and command would be completely separated from authorization. In such case command controller would look like this:
import _ from 'lodash';
import * as di from '@/framework/di';
import * as app from '@/framework/application';
import { ConferenceRepositoryImplMemory, Conference } from './../../../infrastructure/repositories/conference-repository-impl-memory';
interface UpdateConferencePayload {
conference_guid: string;
name: string;
short_name: string;
website_url: string;
}
@di.perResolutionChain()
export class UpdateConferenceCommandController {
constructor(
private readonly conferenceRepository: ConferenceRepositoryImplMemory,
private readonly updateConferenceCommand: UpdateConferenceCommand,
) {
super();
}
public async execute(payload: UpdateConferencePayload) {
const conference = await this.conferenceRepository.getByGuid(payload.conference_guid);
if (conference.something) {
throw AuthorizationError();
}
await this.updateConferenceCommand.execute(payload);
}
}
I like last option the most because it makes authorization and application layers separate, however it also requires authorization level code to actually init transaction and read aggregate before the command itself is executed.
Which option (if any) will be best, I mean according to some DDD/other desing principles?