Spring Security ACL provides instance-based security for domain objects through a comprehensive Access Control List implementation
—
Spring Security ACL provides several strategy interfaces that allow customization of core behaviors. These interfaces enable you to adapt the ACL system to your specific domain model, security requirements, and infrastructure.
The ACL module uses the Strategy pattern for key extensibility points:
ObjectIdentityRetrievalStrategy - Extract ObjectIdentity from domain objectsObjectIdentityGenerator - Create ObjectIdentity from ID and typeSidRetrievalStrategy - Extract security identities from AuthenticationPermissionGrantingStrategy - Control permission evaluation logicAclAuthorizationStrategy - Control ACL modification permissionsPermissionFactory - Create Permission instances from masks/namesAuditLogger - Log permission grant/deny eventsExtracts ObjectIdentity from domain objects:
package org.springframework.security.acls.model;
public interface ObjectIdentityRetrievalStrategy {
/**
* Obtain the ObjectIdentity from a domain object
* @param domainObject the domain object
* @return ObjectIdentity representing the domain object
*/
ObjectIdentity getObjectIdentity(Object domainObject);
}package org.springframework.security.acls.domain;
public class ObjectIdentityRetrievalStrategyImpl implements ObjectIdentityRetrievalStrategy {
@Override
public ObjectIdentity getObjectIdentity(Object domainObject) {
return new ObjectIdentityImpl(domainObject);
}
}@Component
public class CustomObjectIdentityRetrievalStrategy implements ObjectIdentityRetrievalStrategy {
@Override
public ObjectIdentity getObjectIdentity(Object domainObject) {
if (domainObject instanceof Document) {
Document doc = (Document) domainObject;
// Use custom identifier format
return new ObjectIdentityImpl(Document.class, doc.getUuid());
}
if (domainObject instanceof Folder) {
Folder folder = (Folder) domainObject;
// Use hierarchical identifier
return new ObjectIdentityImpl("com.example.Folder",
folder.getPath() + "/" + folder.getId());
}
// Fallback to default behavior
return new ObjectIdentityImpl(domainObject);
}
}Creates ObjectIdentity from identifier and type:
package org.springframework.security.acls.model;
public interface ObjectIdentityGenerator {
/**
* Create ObjectIdentity from identifier and type
* @param id the object identifier
* @param type the object type
* @return ObjectIdentity for the specified object
*/
ObjectIdentity createObjectIdentity(Serializable id, String type);
}@Component
public class UuidObjectIdentityGenerator implements ObjectIdentityGenerator {
@Override
public ObjectIdentity createObjectIdentity(Serializable id, String type) {
// Validate UUID format for certain types
if ("com.example.Document".equals(type) && id instanceof String) {
String uuid = (String) id;
if (!isValidUuid(uuid)) {
throw new IllegalArgumentException("Invalid UUID format: " + uuid);
}
}
return new ObjectIdentityImpl(type, id);
}
private boolean isValidUuid(String uuid) {
try {
UUID.fromString(uuid);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}Extracts security identities from Spring Security's Authentication:
package org.springframework.security.acls.model;
public interface SidRetrievalStrategy {
/**
* Obtain security identities from Authentication
* @param authentication current authentication
* @return list of security identities
*/
List<Sid> getSids(Authentication authentication);
}package org.springframework.security.acls.domain;
public class SidRetrievalStrategyImpl implements SidRetrievalStrategy {
private boolean roleHierarchySupported = false;
private RoleHierarchy roleHierarchy;
@Override
public List<Sid> getSids(Authentication authentication) {
List<Sid> sids = new ArrayList<>();
// Add principal SID
sids.add(new PrincipalSid(authentication));
// Add authority SIDs
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (roleHierarchySupported && roleHierarchy != null) {
authorities = roleHierarchy.getReachableGrantedAuthorities(authorities);
}
for (GrantedAuthority authority : authorities) {
sids.add(new GrantedAuthoritySid(authority));
}
return sids;
}
public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
this.roleHierarchySupported = true;
}
}@Component
public class GroupAwareSidRetrievalStrategy implements SidRetrievalStrategy {
@Autowired
private GroupService groupService;
@Override
public List<Sid> getSids(Authentication authentication) {
List<Sid> sids = new ArrayList<>();
// Add principal SID
sids.add(new PrincipalSid(authentication));
// Add direct authorities
for (GrantedAuthority authority : authentication.getAuthorities()) {
sids.add(new GrantedAuthoritySid(authority));
}
// Add group memberships
String username = authentication.getName();
List<String> groups = groupService.getUserGroups(username);
for (String group : groups) {
sids.add(new GrantedAuthoritySid("GROUP_" + group));
}
// Add organizational hierarchy
String department = getUserDepartment(authentication);
if (department != null) {
sids.add(new GrantedAuthoritySid("DEPT_" + department));
}
return sids;
}
private String getUserDepartment(Authentication authentication) {
// Extract department from user details or external service
if (authentication.getPrincipal() instanceof UserDetails) {
return ((CustomUserDetails) authentication.getPrincipal()).getDepartment();
}
return null;
}
}Controls the core permission evaluation logic:
package org.springframework.security.acls.model;
public interface PermissionGrantingStrategy {
/**
* Evaluate if permissions are granted
* @param acl the ACL containing entries to evaluate
* @param permission required permissions
* @param sids security identities to check
* @param administrativeMode if true, skip auditing
* @return true if permission is granted
*/
boolean isGranted(Acl acl, List<Permission> permission, List<Sid> sids,
boolean administrativeMode) throws NotFoundException, UnloadedSidException;
}package org.springframework.security.acls.domain;
public class DefaultPermissionGrantingStrategy implements PermissionGrantingStrategy {
private final AuditLogger auditLogger;
public DefaultPermissionGrantingStrategy(AuditLogger auditLogger) {
this.auditLogger = auditLogger;
}
@Override
public boolean isGranted(Acl acl, List<Permission> permission, List<Sid> sids,
boolean administrativeMode) throws NotFoundException, UnloadedSidException {
List<AccessControlEntry> aces = acl.getEntries();
for (Permission p : permission) {
// Check each permission against ACL entries
for (AccessControlEntry ace : aces) {
if (ace.getPermission().getMask() == p.getMask()) {
if (sids.contains(ace.getSid())) {
// Log audit event
if (!administrativeMode) {
auditLogger.logIfNeeded(true, ace);
}
return ace.isGranting();
}
}
}
}
// Check parent ACL if inheritance is enabled
if (acl.isEntriesInheriting() && acl.getParentAcl() != null) {
return isGranted(acl.getParentAcl(), permission, sids, administrativeMode);
}
throw new NotFoundException("Unable to locate a matching ACE for passed permissions and SIDs");
}
}@Component
public class CustomPermissionGrantingStrategy implements PermissionGrantingStrategy {
private final AuditLogger auditLogger;
@Override
public boolean isGranted(Acl acl, List<Permission> permission, List<Sid> sids,
boolean administrativeMode) throws NotFoundException, UnloadedSidException {
List<AccessControlEntry> aces = acl.getEntries();
// Custom logic: check deny entries first (explicit deny takes precedence)
for (AccessControlEntry ace : aces) {
if (!ace.isGranting() && sids.contains(ace.getSid())) {
for (Permission p : permission) {
if (hasPermission(ace.getPermission(), p)) {
if (!administrativeMode) {
auditLogger.logIfNeeded(false, ace);
}
return false; // Explicit deny
}
}
}
}
// Then check grant entries
for (AccessControlEntry ace : aces) {
if (ace.isGranting() && sids.contains(ace.getSid())) {
for (Permission p : permission) {
if (hasPermission(ace.getPermission(), p)) {
if (!administrativeMode) {
auditLogger.logIfNeeded(true, ace);
}
return true;
}
}
}
}
// Check inheritance
if (acl.isEntriesInheriting() && acl.getParentAcl() != null) {
return isGranted(acl.getParentAcl(), permission, sids, administrativeMode);
}
return false; // Default deny
}
private boolean hasPermission(Permission acePermission, Permission requiredPermission) {
// Custom permission matching logic (e.g., bitwise operations)
return (acePermission.getMask() & requiredPermission.getMask()) == requiredPermission.getMask();
}
}Controls who can modify ACL entries:
package org.springframework.security.acls.domain;
public interface AclAuthorizationStrategy {
/**
* Check if current user can change ACL ownership
*/
void securityCheck(Acl acl, int changeType);
// Change type constants
int CHANGE_OWNERSHIP = 0;
int CHANGE_AUDITING = 1;
int CHANGE_GENERAL = 2;
}public class AclAuthorizationStrategyImpl implements AclAuthorizationStrategy {
private final GrantedAuthority gaGeneralChanges;
private final GrantedAuthority gaModifyAuditing;
private final GrantedAuthority gaTakeOwnership;
public AclAuthorizationStrategyImpl(GrantedAuthority... auths) {
// Configure required authorities for different operations
this.gaTakeOwnership = auths[0];
this.gaModifyAuditing = auths.length > 1 ? auths[1] : auths[0];
this.gaGeneralChanges = auths.length > 2 ? auths[2] : auths[0];
}
@Override
public void securityCheck(Acl acl, int changeType) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
switch (changeType) {
case CHANGE_OWNERSHIP:
if (!hasAuthority(auth, gaTakeOwnership) && !isOwner(auth, acl)) {
throw new AccessDeniedException("Access denied");
}
break;
case CHANGE_AUDITING:
if (!hasAuthority(auth, gaModifyAuditing)) {
throw new AccessDeniedException("Access denied");
}
break;
case CHANGE_GENERAL:
if (!hasAuthority(auth, gaGeneralChanges) && !isOwner(auth, acl)) {
throw new AccessDeniedException("Access denied");
}
break;
}
}
private boolean hasAuthority(Authentication auth, GrantedAuthority required) {
return auth.getAuthorities().contains(required);
}
private boolean isOwner(Authentication auth, Acl acl) {
Sid owner = acl.getOwner();
return owner != null && owner.equals(new PrincipalSid(auth));
}
}Creates Permission instances from masks or names:
package org.springframework.security.acls.domain;
public interface PermissionFactory {
/**
* Create permission from bitmask
*/
Permission buildFromMask(int mask);
/**
* Create permission from name
*/
Permission buildFromName(String name);
/**
* Create multiple permissions from names
*/
List<Permission> buildFromNames(List<String> names);
}public class DefaultPermissionFactory implements PermissionFactory {
private final Map<Integer, Permission> registeredPermissions = new HashMap<>();
private final Map<String, Permission> namedPermissions = new HashMap<>();
public DefaultPermissionFactory() {
// Register built-in permissions
registerPublicPermissions(BasePermission.class);
}
public void registerPublicPermissions(Class<?> clazz) {
Field[] fields = clazz.getFields();
for (Field field : fields) {
if (Permission.class.isAssignableFrom(field.getType())) {
try {
Permission permission = (Permission) field.get(null);
registeredPermissions.put(permission.getMask(), permission);
namedPermissions.put(field.getName(), permission);
} catch (IllegalAccessException e) {
// Skip inaccessible fields
}
}
}
}
@Override
public Permission buildFromMask(int mask) {
Permission permission = registeredPermissions.get(mask);
if (permission == null) {
throw new IllegalArgumentException("Unknown permission mask: " + mask);
}
return permission;
}
@Override
public Permission buildFromName(String name) {
Permission permission = namedPermissions.get(name.toUpperCase());
if (permission == null) {
throw new IllegalArgumentException("Unknown permission name: " + name);
}
return permission;
}
@Override
public List<Permission> buildFromNames(List<String> names) {
return names.stream()
.map(this::buildFromName)
.collect(Collectors.toList());
}
}Logs permission evaluation events:
package org.springframework.security.acls.domain;
public interface AuditLogger {
/**
* Log access attempt if auditing is enabled for the ACE
* @param granted whether access was granted
* @param ace the access control entry being evaluated
*/
void logIfNeeded(boolean granted, AccessControlEntry ace);
}public class ConsoleAuditLogger implements AuditLogger {
@Override
public void logIfNeeded(boolean granted, AccessControlEntry ace) {
if (ace instanceof AuditableAccessControlEntry) {
AuditableAccessControlEntry auditableAce = (AuditableAccessControlEntry) ace;
if ((granted && auditableAce.isAuditSuccess()) ||
(!granted && auditableAce.isAuditFailure())) {
System.out.println("GRANTED: " + granted +
", ACE: " + ace +
", Principal: " + getCurrentUsername());
}
}
}
private String getCurrentUsername() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null ? auth.getName() : "anonymous";
}
}@Component
public class DatabaseAuditLogger implements AuditLogger {
@Autowired
private AuditEventRepository auditRepository;
@Override
public void logIfNeeded(boolean granted, AccessControlEntry ace) {
if (ace instanceof AuditableAccessControlEntry) {
AuditableAccessControlEntry auditableAce = (AuditableAccessControlEntry) ace;
boolean shouldLog = (granted && auditableAce.isAuditSuccess()) ||
(!granted && auditableAce.isAuditFailure());
if (shouldLog) {
AuditEvent event = new AuditEvent();
event.setTimestamp(Instant.now());
event.setPrincipal(getCurrentUsername());
event.setObjectIdentity(ace.getAcl().getObjectIdentity().toString());
event.setPermission(ace.getPermission().getPattern());
event.setSid(ace.getSid().toString());
event.setGranted(granted);
event.setAceId(ace.getId());
auditRepository.save(event);
}
}
}
private String getCurrentUsername() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null ? auth.getName() : "anonymous";
}
}Configure strategy implementations in your Spring configuration:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class AclStrategyConfig {
@Bean
public ObjectIdentityRetrievalStrategy objectIdentityRetrievalStrategy() {
return new CustomObjectIdentityRetrievalStrategy();
}
@Bean
public ObjectIdentityGenerator objectIdentityGenerator() {
return new UuidObjectIdentityGenerator();
}
@Bean
public SidRetrievalStrategy sidRetrievalStrategy() {
return new GroupAwareSidRetrievalStrategy();
}
@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
return new CustomPermissionGrantingStrategy(auditLogger());
}
@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(
new SimpleGrantedAuthority("ROLE_ADMIN"),
new SimpleGrantedAuthority("ROLE_AUDITOR"),
new SimpleGrantedAuthority("ROLE_ACL_ADMIN")
);
}
@Bean
public PermissionFactory permissionFactory() {
DefaultPermissionFactory factory = new DefaultPermissionFactory();
factory.registerPublicPermissions(CustomPermission.class);
return factory;
}
@Bean
public AuditLogger auditLogger() {
return new DatabaseAuditLogger();
}
@Bean
public AclPermissionEvaluator permissionEvaluator(AclService aclService) {
AclPermissionEvaluator evaluator = new AclPermissionEvaluator(aclService);
evaluator.setObjectIdentityRetrievalStrategy(objectIdentityRetrievalStrategy());
evaluator.setObjectIdentityGenerator(objectIdentityGenerator());
evaluator.setSidRetrievalStrategy(sidRetrievalStrategy());
evaluator.setPermissionFactory(permissionFactory());
return evaluator;
}
}Ensure your custom strategies work well together:
@Component
public class CompatibleStrategies {
@Autowired
private ObjectIdentityRetrievalStrategy objectIdentityRetrievalStrategy;
@Autowired
private SidRetrievalStrategy sidRetrievalStrategy;
@PostConstruct
public void validateCompatibility() {
// Test with sample objects to ensure strategies work together
Document sampleDoc = new Document();
sampleDoc.setId(1L);
ObjectIdentity identity = objectIdentityRetrievalStrategy.getObjectIdentity(sampleDoc);
MockAuthentication auth = new MockAuthentication("testuser");
List<Sid> sids = sidRetrievalStrategy.getSids(auth);
Assert.notNull(identity, "ObjectIdentity strategy failed");
Assert.notEmpty(sids, "SID retrieval strategy failed");
}
}Optimize strategy implementations for your use case:
@Component
public class CachingObjectIdentityRetrievalStrategy implements ObjectIdentityRetrievalStrategy {
private final Cache<Object, ObjectIdentity> cache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(Duration.ofMinutes(30))
.build();
@Override
public ObjectIdentity getObjectIdentity(Object domainObject) {
return cache.get(domainObject, this::createObjectIdentity);
}
private ObjectIdentity createObjectIdentity(Object domainObject) {
return new ObjectIdentityImpl(domainObject);
}
}Thoroughly test custom strategy implementations:
@ExtendWith(SpringExtension.class)
class CustomStrategiesTest {
@Mock
private GroupService groupService;
@InjectMocks
private GroupAwareSidRetrievalStrategy sidStrategy;
@Test
void shouldIncludeGroupSids() {
when(groupService.getUserGroups("testuser"))
.thenReturn(Arrays.asList("developers", "managers"));
Authentication auth = new UsernamePasswordAuthenticationToken(
"testuser", "password", Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
List<Sid> sids = sidStrategy.getSids(auth);
assertThat(sids).hasSize(4); // principal + role + 2 groups
assertThat(sids).contains(new GrantedAuthoritySid("GROUP_developers"));
assertThat(sids).contains(new GrantedAuthoritySid("GROUP_managers"));
}
}Strategy interfaces provide powerful customization capabilities for the ACL system. The next step is understanding how these strategies integrate with permission evaluation in your application.
Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-security--spring-security-acl