Concept Deep-Dive: Custom Roles
To understand how to create your custom roles and privileges, let's deep dive into the permission concept of Neos and Flow.
Neos and Flow feature quite a sophisticated permission framework, which allows to model many kinds of permission restrictions – however, it comes with a learning curve. On this page, we'll explain all the core concepts in full detail.
#What are Privilege Targets?
That were a lot of concepts to grasp so far. Let's focus a bit more in detail on a crucial one - the Privilege Target. This will help you to understand the way permissions are applied in Neos (and Flow) in better detail.
Cornerstone: Declarative privilege enforcement
In Neos and Flow, we want to ensure that permissions are enforced everywhere and consistently.
Of course, you are free to do explicit access checks in your code by calling $securityContext->hasRole('Your:Role.Here')
or $privilegeManager->isPrivilegeTargetGranted('Your:PrivilegeTarget.Here')
.
However, you would manually need to ensure that you place this code in all possible code-paths the user can trigger. If you forget this at a single code path, this will lead to a security issue.
To ensure that permissions are enforced across all possible code paths, we do two things:
- We specify the access check declaratively in Policy.yaml (by using Privilege Targets).
- We enforce all privilege targets in the system on a low level by using Aspect-Oriented Programming (AOP) or Doctrine Query Rewriting.
How does this look in practice? Let's check this out with an example.
#Specifying a MethodPrivilege
As an example, let's specify a MethodPrivilege to target all set*()
methods on the Invoice
model class:
privilegeTargets:
// the first level here is the TYPE of the privilege
'Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege':
// the second level is our desired NAME of the privilegeTarget
// (must be unique in the whole system)
'Your.Package:ModifyInvoice':
matcher: 'method(Your\Package\Domain\Model\Invoice->set.*())'
By specifying this privilege target, the system will automatically instrument your code, and place an access check in front of every set*()
method in the Invoice class.
This is done using AOP (Aspect-Oriented Programming) – so the matcher for the MethodPrivilege is using the AOP Pointcut Expression syntax.
Access to Privilege Targets is DENIED by default
Defining a privilegeTarget means that access to the target needs to be explicitely GRANTED, otherwise the user is forbidden to e.g. call the method.
This means, effectively the default behavior is switched by specifying a privilegeTarget:
Without a privilegeTarget, a method call is allowed. Conversely, by targeting a method with a privilegeTarget, the method call is DENIED unless explicitely GRANTED.
#Referencing the Privilege in a role
In order to grant access to our just specified privilege target Your.Package:ModifyInvoice
, we need to add a rule to the roles we want to allow access:
privilegeTargets:
...
roles:
'Neos.Flow:AuthenticatedUser':
privileges:
-
privilegeTarget: 'Your.Package:ModifyInvoice'
permission: GRANT
Now, we created our desired behavior:
- a logged in user to the system is allowed to modify invoices
- all other users are not allowed to modify invoices.
#Different types of PrivilegeTargets
So far, we have seen the MethodPrivilege in action. Now, let's check out some privilege types on nodes:
privilegeTargets:
'Neos\ContentRepository\Security\Authorization\Privilege\Node\EditNodePrivilege':
'YourSite:EditWebsitePart':
matcher: 'isDescendantNodeOf("c1e528e2-b495-0622-e71c-f826614ef287")'
Let's again reconfirm our understanding about what is changing in the system with the above privilege:
- All nodes below the specified one are now switched to being non-editable.
- We now have to explicitely grant the
YourSite:EditWebsitePart
privilege to again have the behavior as before - where you simply could edit all nodes.
We see that this privilege target has a different kind of matcher
expression – one which is used to select nodes. This is because we need a different way to target nodes, compared to targeting methods.
Every Privilege Target type can have a different kind of Matcher expression. The table below summarizes them.
#Overview of PrivilegeTargets and their match expressions
Privilege Target type | expression language in matcher |
---|---|
MethodPrivilege | AOP Pointcut Expressions - see the Flow manual about AOP for details. |
EntityPrivilege | Eel expressions with isType , property - see the Flow manual for details |
Node Privileges | |
ReadNodePrivilege | Eel expressions using Reference is on List of Node Privilege Matchers. |
EditNodePrivilege | |
NodeTreePrivilege | |
CreateNodePrivilege | Eel expressions using the list above, plus createdNodeIsOfType . |
ReadNodePropertyPrivilege | Eel expressions using the list above, plus nodePropertyIsIn . |
EditNodePropertyPrivilege | |
Asset Privileges | |
ReadAssetPrivilege | Eel expressions using Reference is on the List of Asset Privileges. |
ReadAssetCollectionPrivilege | Eel expressions using Reference is on the List of Asset Privileges. |
ReadTagPrivilege | Eel expressions using Reference is on the List of Asset Privileges. |
Neos Privileges | |
ModulePrivilege | Strings which are module paths (i.e. URLs of the module) |
#GRANT and DENY explained
If a resource (i.e. a method for MethodPrivilege, or a Backend Module for ModulePrivilege) is covered by a privilege target, it needs at least one role which GRANTS access to the resource, and no role which DENIES access. In case no role (for the current account) specifies GRANT or DENY, access is denied by default.
This means if a role DENIES access, this overrules all GRANT statements; no matter if they are in the same role or in other roles which are currently active for the current account.
Suggestion: Do not use DENY
As long as you do not use DENY statements in your roles, the permission logic is purely additive: This means that adding an additional role will never decrease the effective permissions of the user.
This leads to a really predictable permission system – so we suggest you try to avoid DENY statements.
When you start using DENY, you can "cut" something out of the set of allowed Privilege Targets that should always be forbidden.
As soon as we use DENY, the policy is no longer composable, because a DENY always "wins", no matter what GRANTs are still existing for the privilege target..
DENY is meant as an escape hatch (last resort), because sometimes you need it to implement very complex rules.
#Limitations
Except for the assignment of roles to users there is no UI for editing security related configuration. Any changes needed have to be made to the policies in Policy.yaml
.
#Closing Thoughts
We hope you now have a detailed understanding of how the security framework works. We suggest that you check out the Real-World Examples next.
The links below contain some more information from the community:
Everything written on this page applies to Flow
Because the Security framework is a Flow concept, everything written here on this section can be used for plain Flow applications as well – you just have a few less privilege target types.