CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-springframework-security--spring-security-acl

Spring Security ACL provides instance-based security for domain objects through a comprehensive Access Control List implementation

Pending
Overview
Eval results
Files

strategy-interfaces.mddocs/

Strategy Interfaces

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.

Overview

The ACL module uses the Strategy pattern for key extensibility points:

  • ObjectIdentityRetrievalStrategy - Extract ObjectIdentity from domain objects
  • ObjectIdentityGenerator - Create ObjectIdentity from ID and type
  • SidRetrievalStrategy - Extract security identities from Authentication
  • PermissionGrantingStrategy - Control permission evaluation logic
  • AclAuthorizationStrategy - Control ACL modification permissions
  • PermissionFactory - Create Permission instances from masks/names
  • AuditLogger - Log permission grant/deny events

ObjectIdentityRetrievalStrategy

Extracts 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);
}

Default Implementation

package org.springframework.security.acls.domain;

public class ObjectIdentityRetrievalStrategyImpl implements ObjectIdentityRetrievalStrategy {
    
    @Override
    public ObjectIdentity getObjectIdentity(Object domainObject) {
        return new ObjectIdentityImpl(domainObject);
    }
}

Custom Implementation Example

@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);
    }
}

ObjectIdentityGenerator

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);
}

Implementation Example

@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;
        }
    }
}

SidRetrievalStrategy

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);
}

Default Implementation

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;
    }
}

Custom Implementation with Group Support

@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;
    }
}

PermissionGrantingStrategy

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;
}

Default Implementation

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");
    }
}

Custom Permission Strategy

@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();
    }
}

AclAuthorizationStrategy

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;
}

Default Implementation

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));
    }
}

PermissionFactory

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);
}

Default Implementation

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());
    }
}

AuditLogger

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);
}

Console Implementation

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";
    }
}

Custom Audit Logger

@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";
    }
}

Configuration

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;
    }
}

Best Practices

1. Strategy Interface Compatibility

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");
    }
}

2. Performance Considerations

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);
    }
}

3. Testing Strategies

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

docs

acl-services.md

caching-performance.md

configuration.md

domain-model.md

index.md

permission-evaluation.md

strategy-interfaces.md

tile.json