Simply put, No, an Aggregate should not depend upon some piece of state for which it is not the single, authoritative, and unambiguous representation. The real issue here is that the question you are asking is orthogonal to the problem you are trying to solve.
You may be able to reorganize your current domain in a way that might satisfy your requirements by introducing concurrent state machines where each state (ITGroupMember
, etc) is the direct entity responsible for executing your actions and each User
is given a Membership
collection or the like, but this kind of solution will undoubtedly lead to problems down the road and is, frankly, much more complex than necessary.
Generally speaking, authorization/authentication are cross-cutting concerns that don't lend themselves well to being modeled using DDD principals. Just laying out the requirements for such a system should make this clear:
In abstract terms, in order to authorize any given command in your domain, that command must be either 1) injected with or 2) wrapped by an extra piece of logic, say an AuthorizationContext
, such that Permissions
data can be made available to that command for validation. In practice, the most suitable solution is to wrap commands because it leads to lower coupling (the commands themselves needn't know anything about authorization).
In DDD terms, authorization could take either of the above forms. You could inject every entity with your AuthorizationContext
so that each entity method has access to the data necessary for authorization (which is about what you have landed on and are asking about here), or you could treat each entity as a state machine where each state represents a particular set of permissions (in this way wrapping your method calls). Neither of these solutions are particularly elegant and both introduce extra coupling as well as move the focus of your model away from executing business logic. As such, I simply cannot recommend either of the above methods.
The most traditional approach will be to just "raise" authorization up above your domain layer and authorize commands before they execute. This may take the form of thin service layer. This is BY FAR the most common case. Nearly every web framework I've encountered does authorization/authentication via some sort of middleware service that is invoked before any domain code executes. Most use some sort of data annotations put directly onto controller methods. You could simply take a page from this playbook and implement something similar (this is essentially just a global configuration mapping commands to roles).
If this must be done according to DDD, we need to have a broader discussion. A cornerstone of DDD is refactoring towards deeper insight and making the implicit explicit. Remember, the goal of DDD is to model the behavioral requirements of a system such the result is a useful abstraction of the business domain. The key is to focus on the behavior of a system, not the data. This is because the data of a given system is rarely a good starting point for modelling the functional requirements. The single biggest problem I am seeing in your domain is the inclusion of a User
entity. I would bet my first-born grandson that the only reason a User
entity exists in your domain is because you have a [user] table/document/whatever in your data store.
User
is usually a poor choice for a domain object because it denotes almost no behavior and probably encompasses too much knowledge. What do your Users
do? Comment? Commenter
. Rate? Rater
. Review? Reviewer
. You see where I'm going here? You need to slice your data vertically to group related data together according to behavior. Maybe a Commenter
has a PastCommentCollection
. Does a Rater
need a PastCommentCollection
? No. The boundaries of normalization are very unlikely to be the boundaries of all of your entities.
I know what your thinking. How does partitioning the User
entity solve my particular problem regarding authorization? And to that, I'll just leave you with a question:
What if RaterRepository.find( userId )
would throw a RaterNotFoundException
for each userId
that could not be related to the appropriate permissions necessary to rate something?