This answer pointed out that Spring compares for an exact match between the ACE mask and the permission we're checking. As the OP says:
Now i have to create single entries for the "READ" and "Write"
permission, that's not pretty handy.
-Yes. This seems like the default behaviour. As we can see from any of the DB schemas presented here, we are allowed to add multiple entries for a given object. So if we want to grant Alice read and write access, we'd add an entry for read, maybe with order 1, and then an entry for write, with order 2 or something.
Although the default behaviour seems to be as above, the mention of "bitwise" in the documentation is probably confusing newcomers. However, I'd imagine that for backwards compatibility of a framework that's been around for so long and relied upon by so many, it's probably too late to change the default behaviour. The Spring developers seem to be well aware of this conundrum, and so they have provided a simple enough way to overcome it. As this answer points out, the key is to provide a custom PermissionGrantingStrategy
. The linked answer shows how to do hook in the class via XML. I am more comfortable with code based configuration though, so I followed this link which explains how to do set up Spring ACL in code.
Now, all you have to do that's different to what's described in that link, is to replace this Bean definition:
@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
return new DefaultPermissionGrantingStrategy(
new ConsoleAuditLogger());
}
And instead return a permission granting strategy of your own making. Don't worry though! You don't have to write your own class completely from scratch. Your custom permission granting strategy can then just subtype the class DefaultPermissionGrantingStrategy
and override only the method isGranted
. The default implementation indeed just does an exact match:
protected boolean isGranted(AccessControlEntry ace, Permission p) {
return ace.getPermission().getMask() == p.getMask();
}
But the author seems well aware of the need for an override, based on this snippet from the method's doc:
By default, we compare the Permission masks for exact match.
Subclasses of this strategy can override this behavior and implement
more sophisticated comparisons, e.g. a bitwise comparison for ACEs
that grant access.
The documentation goes even further to actually give you the code to do the override. I haven't tested this, but I believe it's just:
protected boolean isGranted(AccessControlEntry ace, Permission p) {
if (ace.isGranting() && p.getMask() != 0) {
return (ace.getPermission().getMask() & p.getMask()) != 0;
} else {
return ace.getPermission().getMask() == p.getMask();
}
}
Should you Use Bitwise ACL?
Before overriding Spring ACL's default to use bitwise storage, you should first ask yourself is it worth the extra effort and the risk of bugs by deviating from OOTB behaviour.
But also, you should be aware that you will lose some behaviour that is available in non-bitwise Spring ACL: namely the ability to delete ACEs. I'll demonstrate with an example. Suppose there is a Company
object called C
and a Department
object called D
which is nested underneath C
in the Spring ACL hierarchy. And suppose D
inherits its permissions from C
.
Now suppose Alice has READ access to the entire company C
. From this, she inherits READ access to all departments underneath C
, including D
. One day, Bob decides he wants to grant Alice WRITE access to the department D
only, but not any other departments. With bitwise logic, what ACE gets added for D
? Since the single ACE must include all permissions READ, WRITE etc. encoded into the various bits, the READ bit must be either 0 or 1. Both are potentially problematic:
- Suppose the READ bit on the new ACE is 0. It would be crazy to assume Alice is now denied READ access to
D
, so we'd have to assume that a 0 bit doesn't mean "denied" but the weaker meaning "not granted". Meaning the ACL logic will still look to parent objects to grant the permission. This means that we lose the ability to DENY-override permissions that were granted at a higher level in the hierarchy.
- On the other hand, we could implement a more complex logic which detects that Alice has READ access via the parent object
C
and so sets the READ bit to 1 on the new ACE. But then what happens if we remove Alice's READ access to C
? There is no way to know that the READ access to D
should also be removed, unless we store that info somewhere, and doing so would defeat the whole point of bitwise ACLs which is to compress storage.
One way around this would be to use two bits for every permission, instead of 1. For example, you could interpret as follows: 00 means not granted or denied - look to parents for a value, 01 means denied, 10 means granted. 11 would be free in that case, but you might use it for something else later, for example. However, doing it with 2 bits would be more complex because the OOTB permissions are only 1 bit, so you'd have to write your own. And for all that effort, you'd only get half the storage compression at the end of it.
return ((permissionMask & permissionsRequired) == permissionMask);
– Subarctic