Access Control in Domain Driven Design
Asked Answered
U

2

90

I read about DDD and Access Control, and I found some contradiction between the following two opinions:

  • "security concerns should be handled outside the domain"
  • "access control requirements are domain specific"

I am looking for a best practice about this. So where should I put the access control logic by domain driven design, and how should I implement it?

(To be more specific by DDD + CQRS + ES.)

I think it should be somewhere near to the business logic, for example a user story could be something like this:

The user can edit his profile by sending a user name, a list of hobbies, cv, etc...

Based on the user story we implement the domain model and the services, for example:

UserService
    editProfile(EditUserProfileCommand command)
        User user = userRepository.getOneById(command.id)
        user.changeName(command.name)
        user.changeHobbies(command.hobbies)
        user.changeCV(command.cv)

UserRepository
    User getOneById(id)
        
User
    changeName(String name)
    changeHobbies(String[] hobbies)
    changeCV(String cv)
    

This is okay, but where is the HIS profile part of the story?

This is obviously attribute based access control, because we should write a rule something like this:

deny all, but if subject.id = resource.owner.id then grant access

But where should we enforce this rule, and how should we implement it?

Utricle answered 5/5, 2014 at 4:16 Comment(2)
Including user id in command (command.id) introduces ambiguity. Better remove user id from command and pass user taken from authorization context alongside with command.Gelid
@Gelid Thanks for your input. To be honest I have no idea why this question and answer gets that high points. Maybe somebody should write an article or example application about how this should be done properly. I started to read about DDD again recently and it is really difficult to find good articles. Some of them have obvious design issues. I guess that happens when everybody can write in a topic even people like me, who don't fully understand it.Utricle
U
61

So where should I put the access control logic?

According to this: https://softwareengineering.stackexchange.com/a/71883/65755 the policy enforcement point should be right before the call of the UserService.editProfile().

I came to the same conclusion: it cannot be in the UI because by multiple UIs we would have code repetition. It should be before the creation of domain events, because they indicated that we have already done something in the system. So we can restrict the access to domain objects or to services which use those domain objects. By CQRS we don't necessary have domain objects by the read model, just services, so we have to restrict access to the services if we want a general solution. We could put the access decisions at the beginning of every service operation, but that would be grant all, deny x security anti pattern.

How should I implement it?

This depends on which access control model fits to the domain, so it depends on the user story. By an access decision we usually send an access request and wait a permission in return. The access request usually has the following parts: subject, resource, operation, environment. So the subject requires permission to perform an operation on the resource in an environment. First we identify the subject, then we authenticate it, and after that comes the authorization, where we check whether the access request fits to our access policy. Every access control model works in a similar way. Ofc. they can lack of some of these steps, but that does not matter...

I created a short list of access control models. I put the rules, policies into annotations, but normally we should store them in a database probably in XACML format if we want to have a well maintainable system...

  • By identity based access control (IBAC) we have an identity - permission storage (access control list, capability list, access control matrix). So for example by an access control list, we store the list of the users or groups whose can have permissions.

    UserService
        @AccessControlList[inf3rno]
        editProfile(EditUserProfileCommand command)
    
  • By lattice based access control (LBAC) the subject has a clearance level, the resource has a required clearance level, and we check which level is higher...

    @posseses[level=5]
    inf3rno
    
    UserService
        @requires(level>=3)
        editProfile(EditUserProfileCommand command)
    
  • By role based access control (RBAC) we define subject roles and we grant permissions to subjects whose act the actual role.

    @roles[admin]
    inf3rno
    
    UserService
        @requires(role=admin)
        editProfile(EditUserProfileCommand command)
    
  • By attribute based access control (ABAC) we define subject, resource and environment attributes and we write our policies based on them.

    @attributes[roles=[admin]]
    inf3rno
    
    UserService
        @policy(subject.role=admin or resource.owner.id = subject.id)
        editProfile(EditUserProfileCommand command)
        @attribute(owner)
        Subject getOwner(EditUserProfileCommand command)
    
  • By policy based access control (PBAC) we don't assign our policies to anything else, they are standalone.

    @attributes[roles=[admin]]
    inf3rno
    
    UserService
        editProfile(EditUserProfileCommand command)
        deleteProfile(DeleteUserProfileCommand command)
        @attribute(owner)
        Subject getOwner(EditUserProfileCommand command)
    
    @permission(UserService.editProfile, UserService.deleteProfile)
    @criteria(subject.role=admin or resource.owner.id = subject.id)
    WriteUserServicePolicy
    
  • By risk-adaptive access control (RAdAC) we base our decision on the relative risk profile of the subject and the risk level of the operation. This cannot be described with rules I think. I am unsure of the implementation, maybe this is what stackoverflow uses by its point system.

  • By authorization based access control (ZBAC) we don't do identification and authentication, instead we assign permissions to identification factors. For example if somebody sends a token, then she can have access to a service. Everything else is similar to the previous solutions. For example with ABAC:

    @attributes[roles=[editor]]
    token:2683fraicfv8a2zuisbkcaac
    
    ArticleService
        @policy(subject.role=editor)
        editArticle(EditArticleCommand command)
    

    So everybody who knows the 2683fraicfv8a2zuisbkcaac token can use the service.

and so on...

There are many other models, and the best fit always depends on the needs of your customer.

So to summarize

- "security concerns should be handled outside the domain"
- "access control requirements are domain specific"

both can be right, because security is not part of the domain model, but its implementation depends on the domain model and the application logic.

edit after 2 years 2016-09-05

Since I answered my own question as a DDD newbie, I have read Implementing Domain-Driven Design from Vaughn Vernon. It was an interesting book in the topic. Here is a quote from it:

This constitutes a new Bounded Context - the Identity and Access Context - and will be used by other Bounded Contexts through standard DDD integration techniques. To the consuming contexts the Identity and Access Context is a Generic Subdomain. The product will be named IdOvation.

So according to Vernon probably the best solution to move the access control to a generic subdomain.

Utricle answered 6/5, 2014 at 2:43 Comment(16)
About the edit, it's easy to move role-based, ACL or permission-based to a generic BC, but not so much for rule-based authorization.Wendiwendie
One thing I don't understand about these ABAC systems that have checks like @policy(subject.role=admin or resource.owner.id = subject.id) is how versioning is handled because the code in the policy is sandbox from the code in the service. The owner may have changed by the time the code in the actual service method requests the resource, but maybe there's some magic I'm missing?Comment
@Comment Feel free to edit or change it the way you want. As far as I remember I read a week about ABAC, but I never used it. As far as I remember it is some sort of policy or rule based access control, though it is called attribute based. It is possible that I completely misunderstood something when I wrote that answer 5 years ago. I should have left only the "edit after 2 years part", but people already upvoted my answers, I think that's why I left it there. It would not be the first time here that people upvoted a wrong answer though...Utricle
@Utricle it’s a very thorough and well thought out answer and it does generally look like similar examples I’ve seen. I’m mostly just curious if there’s a canonical way of handling versioning one ABAC systems.Comment
@Comment Sorry, I don't know.Utricle
@Comment Is it actually important? Would it really matter if a permission is revoked 1ms before a user performs an action.Wendiwendie
@Wendiwendie Yes there are many cases where you’d be violating an invariant and could end up in an invalid/unexpected state.Comment
@Comment I don't think I've ever encountered such as case. Log entries or the order of events could look odd e.g. PermissionRevoked, then ActionPerformed, but ultimately if it doesn't occur often most businesses would be fine with that IMO. You could always have eventual consistency that checks the event log to find such scenarios and issue compensating actions?Wendiwendie
@Wendiwendie I guess I'd have to think about that some more. I'm generally comfortable with the idea of eventual consistency, but I'm usually using an optimistic concurrency model or at least validating that the object is in a valid state for the action/behavior in question. Your point seems reasonable, but it goes against my instinct :) You're probably right that this is true in most cases, but I'm sure there are some domains where it would be unacceptable.Comment
@Comment Yes for sure, but ultimately when you start thinking about it it often doesn't matter. For instance, management is in a meeting, they just took the decision to fire an employee and access rights must be revoked. The decision is taken already, but no one actioned it in the system yet. There's a real world race condition already. Most of the time it doesn't matter who would have clicked first when enforcing authorization rules. Access rights are often even cached in many systems for a certain period of time.Wendiwendie
You'd have to be careful not to make business state decisions based off non-strongly-consistent access rights though, such as "if only the owner of an entity can modify it, then it's certain that if we are in that operation the current user owns the entity". That's conflating the business model state with the access model state.Wendiwendie
@Comment Well you could do something similar mongodb does. You start with a resource having version number v1. You do your changes and send a query to update the resource and ofc. you attach v1 to the query. Meanwhile somebody updated the resource which got the v2 version number. So when mongodb gets your query with v1 it will reject it, because the resource is in v2. In your case you can attach a version to the command before the first security check and when the version changes before processing the command and creating domain events, then you can check it again.Utricle
@Comment The granularity is an interesting topic here. Looks like I added the access control to application services, so I guess each application service should have its own version. Another option that each release gets a policy version. So by version update it is possible to stop processing commands and try again with the new policies. Maybe this is a partial solution. It does not solve the scenarios where the policy is the same, but for example the owner is changed while processing a command from the previous owner. I think for those scenarios you need eventual consistency as plalx wrote.Utricle
But again, I am not an expert. :PUtricle
Hi @inf3rno, I have a question about how one would use the identity and access management (IAM) subdomain if separated into its own generic subdomain. Can you clarify if the following call-path represents the recommended use of the IAM subdomain that you provided? user makes request -> http request handler (app controller) converts request to DTO object and calls useCase -> use case handles domain logic, checks usersRepository.exists(user), if exists, checks permissions via IAMrepository.getPermission(user) (a read model), and only then performs the unit of work if the permissions are valid?Plaque
@KyleFong Sounds ok, but I am not an expert of the topic. I don't even understand how I got so many upvotes, probably nobody else is an expert either. :DUtricle
F
1

But where should we enforce this rule, and how should we implement it?

Consider use case, username can only be changed by Administrator, thus Authorization should be passed to each entities and domain services behaviors/methods (including factory method).

In following example, Application service is a service that reflects use case of the application/API. It dependent on domain's repositories and authorization service. Injects all required dependencies into Application Service(s).

Based on your example, we can modify it as following:

UserRepository interface
    User getOneById(id)

AuthorizationService interface
    ActiveUser getActiveUser(String token)

ActiveUser interface
    Role getRole()
    Id getId()

ApplicationService

    //dependencies
    UserRepository userRepository
    AuthorizationService authorizationService
    ...

    editProfile(RequestContext context, EditUserProfileCommand command)
        activeUser = authorizationService.getActiveUser(context.getAccessToken)
        User user = userRepository.getOneById(command.id)
        user.changeName(activeUser, command.name)
        user.changeHobbies(activeUser, command.hobbies)
        user.changeCV(activeUser, command.cv)
        userRepository.save(user)

===
User
    changeName(ActiveUser activeUser, String name)
    changeHobbies(ActiveUser activeUser, String[] hobbies)
    changeCV(ActiveUser activeUser, String cv)

//example how to handle in entity behavior
changeName(ActiveUser activeUser, String newName)
    if activeUser.getRole() != UserRole.ADMINISTRATOR
        throw UnauthorizedException()
    this.name = newName

Finger answered 2/1, 2022 at 7:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.