0

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:

  1. 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...

  2. 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.

  3. 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?

2 Answers 2

1

Option 4:

The rules that users can edit their own posts and moderators can edit all posts are business logic and should be encapsulated within the domain model.

I would pass the User (I would think of a better name) to the change method, something like:

conference.changeName(newName, changeRequestedBy)

In that method you can validate the id of the creator against the id of the passed in user and also check if the passed in user has the moderator role.

5
  • Hmm I know this pattern, its kind of "domain service" right (additional business logic passed to aggregate method)? Are you sure this kind of logic should be handled in domain level? I mean this is "authorization" part which I think should not be implemented in domain logic (according to all sources I read) because it's does not even have impact on domain model consistency (in opposite to some relation between two aggregates for example). Also authorization may change over time (more checks, more rules, different user types etc) and it would be easier to not change domain model in such case?
    – user606521
    Commented Feb 18, 2021 at 9:56
  • I mean very often in the system it's relatively "easy" to define domain logic from business requirements but actual authorization rules might not be well defined in the beginning and may change quite quickly I think? I feel like it would be easier to handle authorization on different layer from domain...
    – user606521
    Commented Feb 18, 2021 at 10:00
  • I agree that general role-based authorization is usually not a domain concern. But as you noticed yourself, this scenario is different, it's conditional. You still have authorization outside of the domain, but this additional validation should be done in the domain imo.
    – Rik D
    Commented Feb 18, 2021 at 10:29
  • But does anyone do this in real world? It feels wrong to have let's say half of authorization defined in authorization/controller layer and half passed from authorization/controller layer, through application/command layer, into domain layer. Feels that code will be messy - maybe it's better to break some rules here to keep authorization in one place? Are there any cons of my example #3? The only I see is that authorization layer has access to repository and aggregate object but it can be implemented in the way that it has readonly access to aggregate object.
    – user606521
    Commented Feb 18, 2021 at 15:01
  • 1
    From my viewpoint, your option 3 is leaking domain knowledge. But if you don't consider these rules business rules then by all means do what works for you. The thing with DDD is that it requires a lot of domain knowledge and refining of the model when your knowledge increases. Don't be affraid to try something out and change if it doesn't work.
    – Rik D
    Commented Feb 18, 2021 at 15:39
1

I'll stick with the Post example because you haven't made clear exactly what your actual requirements mandate.

I don't see a useful distinction between your "authorization" layer and your "command/query" layer. They are best combined (at least logically) into an "application" layer where authorization and command/querying are each their own cohesive mechanisms. In fact, the distinction above seems to be precisely why you are here asking this question in the first place.

Take the below for example:


/** 
 * this command handler is in your application layer and 
 * can therefore be given the responsibility of authorization
 */
public async editPost(cmd: EditPost): void {
    
    let user = this.session.user; // or wherever the user comes from
    
    let post = await this.posts.findById(cmd.postId);

    // must be either user's post or a moderator
    if (post.userId !== user.id && user.role !== "moderator") {
        throw "Unauthorized";
    }

    // execute domain logic (albeit quite thin!)
    post.name = cmd.name;

    // flow of control is now back in our application layer
    this.posts.save(post);
}

We are simply carrying out the application-level logic (including both authorization and I/O) necessary to fulfill our use-case. The above follows a conventional onion (or hexagonal) architectural approach. The only oddity is that the authorization for the above editPost method requires more than just information about the logged in User.

Maybe other methods use a more conventional decorator approach:


@role("moderator") // checks user's role and throws if not moderator
public async deletePost(cmd: DeletePost): void {
    // this is a continuation for the above if it didn't throw
}

The important thing is neither of the above is doing anything different. Where the former specifically implemented its own authorization, the latter is just a wrapper doing basically the same thing.

Maybe this is closest to option 3? I can't really tell.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.