This package implements dynamic Access Control Lists for Neos Roles.
The development of this package was sponsored by ujamii and queo.
Main features:
RestrictedEditor
to an allowlist-only permission approach. By installing this package, the RestrictedEditor
is
no longer allowed to change any content.composer require sandstorm/neosacl
./flow doctrine:migrate
Initial (Package) Setup
Sandstorm.NeosAcl
in the DistributionPackages of a Neos 4.3 or later installationcomposer.json
as "sandstorm/neosacl": "*"
composer update
Initial React Setup
cd Resources/Private/react-acl-editor
yarn
yarn dev
Then, log into the backend of Neos, and visit the module "Dynamic Roles".
The basic idea was the following: Hook into PolicyService::emitConfigurationLoaded
, and modify the $configuration
array (introduce new roles
and privilegeTargets). This basically works at runtime - however there is a problem with dynamic MethodPrivilege enforcement, which is
explained below and by the following diagram:
PointcutFilterInterface
can - during compile time of Flow - decide which classes
and methods match for a certain aspect.
PolicyEnforcementAspect
(which is the central point for enforcing MethodPrivileges).MethodPrivilegePointcutFilter
is referenced.MethodPrivilegePointcutFilter
asks the PolicyService
for all configured MethodPrivilege
s - and ensures
AOP proxies are built for these methods.MethodPrivilegePointcutFilter
additionally builds up
a data structure methodPermissions
- which remembers which MethodPrivileges
are registered for which method.
Flow_Security_Authorization_Privilege_Method
cache.PolicyEnforcementAspect
, all configured MethodPrivilege
s are
invoked - and they have to quickly decide if they match this particular call-site.methodPermissions
data structure from the Flow_Security_Authorization_Privilege_Method
cache.MethodPrivilege
is defined dynamically at runtime, then the methodPermissions
data structure is missing
the information that this new privilege should be invoked for certain methods.MethodPrivileges
for call-sites which are already instrumented by AOP;
because otherwise the code will never get invoked (because of missing proxies).We are mostly working with EditNodePrivilege
etc. - so why does this apply there?
EditNodePrivilege
has an internal MethodPrivilege
which takes care of the method call enforcement part;
i.e. preventing you to call e.g. NodeInterface::setProperty()
if you do not have the permission to do so.Furthermore, to make this idea work, the Policy.yaml
of this package defines a catch-all Sandstorm.NeosAcl:EditAllNodes
PrivilegeTarget - so AOP will instrument the corresponding methods of NodeInterface
. This catch-all makes sense
in any case, because this switches the security framework to an allowlist-only approach
In order to make the dynamic policy enforcement work, we need to add custom stuff to the methodPermissions
- for
the dynamically added roles.
The post-processing of the methodPermissions
is done using a custom cache frontend (SecurityAuthorizationPrivilegeMethodCacheFrontend
).
Method privileges internally can use dynamic AOP Runtime Expressions (in case you check for method parameters). Especially
the MethodPrivilege
- which is attached to the RemoveNodePrivilege
- uses the following expression code:
return 'within(' . NodeInterface::class . ') && method(.*->setRemoved(removed == true))';
The removed == true
part is a so-called AOP Runtime Expression.
This is internally implemented using the Flow_Aop_RuntimeExpressions
"cache", which is pre-filled again during the compile
time (which is a nasty side-effect).
Thus, in our case we need to again implement a custom cache frontend (AopRuntimeExpressionsCacheFrontend
),
using the runtime expressions of the base configuration, which exists properly.