Reputation: 193
I'm trying to follow DDD principles to create a model for determining whether or not an Identity has access to an Action belonging to a Resource.
A Resource (e.g. a webservice) is something that holds a number of Actions (e.g. methods), which can be accessed or not. An Identity is something that wants to access one or more Actions on a Resource. For example, someone uses an api-key in a call to a webservice method, and it must be determined whether or not access is allowed.
As I currently see it, Identity and Resource are aggregate roots, and Action is an entity belonging to Resource. It doesn't seem to make sense for an Action to live on its own; it will always belong to one Resource. An Identity needs to know to which Resource Actions it has access. This seems to suggest the following model.
However, as I understand it, this violates the principle that something outside an aggregate cannot reference an entity within the aggregate. It must go through the root. Then I'm thinking, what if Action was the aggregate root and Resource an entity? But that doesn't seem very logical to me. I've also been thinking of merging Resource and Action into one entity, which would then be an aggregate root, but that also seems wrong to me.
So it leaves me kind of stuck on how to model this correctly using DDD principles. Anyone have a good idea on how to model this?
Update: The model I'm trying to create is the identity model for defining which resource actions an Identity is allowed to access. It is not a model for the actual implementation of resources and actions.
Update 2 - invariants: Id of all objects is given at birth, is unique, and doesn't change. ApiKey of Identity must be unique across all Identities. Name of Action must be unique within aggregate, but two different Resources can have Actions with same names, e.g. Resource "R1" can have an Action "A1" and Resource "R2" can also have an Action "A1", but the two "A1"s are not the same.
Upvotes: 3
Views: 1210
Reputation: 13246
I will also expand on the bit identified by @VoiceOfUnreason:
For example, someone uses an api-key in a call to a webservice method, and it must be determined whether or not access is allowed.
How would the particular bit of exposed functionality know what security is applied to it? The answer is provided by @Chris Simon: Permission
.
I have a common implementation that I use that has not been distilled into an Identity & Access
BC of its own but follows closely with what you are attempting --- I hope :)
A Session
has a list of Permission
strings. Typically I use a uri
to represent a permission since it is quite readable. Something like my-system:\\users\list
. Anyway, how the user is assigned these permissions could be anything. There may very well be a Role
containing permissions and a user is assigned to one or more roles; or the user may even have a custom list of permissions.
When a SessionToken
is requested (via authentication) the server retrieves the permissions for the relevant user and creates a session with the relevant permissions assigned to it. This results in a read-side token/permission
.
Each exposed bit of functionality (such as a rest endpoint) is assigned a permission. In c# web-api it is simply an attribute on the method:
[RequiredPermission("my-system:\\order\create")]
My session token is passed in the header and a quick check determines whether the session has expired and whether the session (assigned to the user) has access to the resource.
Using your design the Action
may very well carry the required Permission
. The User
would still require a list of either roles or UserAction
entries that contain, perhaps, the ResourceId
and ActionId
. When the use logs in the read-optimized session structures are created using that structure.
If there is an arbitrary Action
list that can be assigned to any Resource
then both Resource
and Action
are probably aggregates. Then you would need a ResourceAction
as mentioned by @Chris Simon. The ResourceAction
would then contain the Permission
.
That's my take on it...
Upvotes: 0
Reputation: 6505
The domain model in terms of aggregates and entities has it's purpose in DDD in order to simplify expression and enforcement of the invariants - as write operations are applied to the model.
As mentioned in @VoiceOfUnreason's answer, the question 'Can this user do action A on resource R' is a question that doesn't necessarily need to flow through the domain model - it can be answered with a query against either a pre-projected read-only model, or standard SQL querying against the tables that make up the write model persistence (depend on your needs).
However, your question, whilst mostly about how to identify if an identity is allowed to carry out an action, is implicitly seeking a simpler model for the updating of resources, actions and permissions. So to explore that idea... there are implicitly two types of write operations:
It's possible that the model for these two types of operations might by simplified if they were split into different bounded contexts.
In the first, you'd model as you have done, an Aggregate with Resource as the aggregate root and Action as a contained entity. This permits enforcing the invariant that the action name must be unique within a resource.
As changes are made in this context, you publish events e.g. ActionAddedToResource
, ActionRemovedFromResource
.
In the second context, you'd have three aggregates:
ResourceAction instances would be updated based events published from the first context - created on ActionAddedToResource, removed on ActionRemovedFromResource. If there is a resource with no actions, there is no ResourceAction at all.
Permission would contain two identity references - IdentityId
and ResourceActionId
This way when carrying out the operation "Permit this user to do this action on this resource" the operation is just to create a new Permission instance - reducing the set of operations that affect the Identity aggregate's consistency boundary - assuming there are no invariants that require the concept of a 'permission' to be enforced within an Identity aggregate?
This also simplifies the query side of things, as you just need to search for a Permission entry with matching identityId
, resourceName
and actionName
after joining Permissions
to ResourceActions
.
The DDD Book in the section on Strategic Design refers to organising your contexts according to responsibility layers. To use the terms from the book, the above suggestion is based on the idea of a 'capability' responsibility layer (defining resources and actions) and an 'operational' responsibility layer (defining identity permissions and checking identity permissions).
Upvotes: 1
Reputation: 2369
Based on the invariants you mention, Identity can contain a Resources dictionary/map where resourceId is the key and the value is a set of unique action names/ids. This gives you uniqueness of action names for each resource per identity:
Map<Resource, Set<Action>>
Alternatively, you could have a set/list of Resources and they have a collection of Actions on them. Uniqueness can be enforced by the collection types available in the language you're coding in:
Set<Resource> Resources
class Resource {
Set<Action> Actions
}
Even simpler, just create a Resource-Action key by combining the two ids and store it in a set or something to give you uniqueness:
Resource1-Action1
Resource1-Action2
Resource2-Action1
...etc
You can then have a method on Identity to add a new Resource-Action combination.
I don't see anything in your description to warrant Actions being Entities as they appear to have no identity of their own.
This is really simple though, so I am presuming you've simplified the domain considerably.
Upvotes: 0
Reputation: 57194
For example, someone uses an api-key in a call to a webservice method, and it must be determined whether or not access is allowed.
That's a query. Fundamentally, there's nothing wrong with answering a query by joining two read-only copies of entities that belong to different aggregates.
You do need to be aware that, because the aggregates can change independently of each other, and because they can change independently of your query, the answer you get when you do the join may be stale, and not entirely consistent.
For example, you may be joining a copy of an Identity written 100ms ago to a copy of an Action written 200ms ago. Either of the aggregates could be changing while you are running the query.
Upvotes: 0