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

permission-evaluation.mddocs/

Permission Evaluation

Spring Security ACL integrates seamlessly with Spring Security's expression-based access control through the AclPermissionEvaluator. This modern annotation-based approach provides clean, declarative security that's easy to understand and maintain.

Overview

Permission evaluation in Spring Security ACL works through:

  • @PreAuthorize - Check permissions before method execution
  • @PostAuthorize - Check permissions after method execution
  • @PostFilter - Filter collections based on permissions
  • @PreFilter - Filter input collections based on permissions
  • AclPermissionEvaluator - Core integration point with ACL system
  • AclPermissionCacheOptimizer - Batch loading for performance

AclPermissionEvaluator

The AclPermissionEvaluator implements Spring Security's PermissionEvaluator interface to bridge ACL data with expression evaluation:

package org.springframework.security.acls;

public class AclPermissionEvaluator implements PermissionEvaluator {
    
    private final AclService aclService;
    
    public AclPermissionEvaluator(AclService aclService) {
        this.aclService = aclService;
    }
    
    // Check permission on domain object instance
    @Override
    public boolean hasPermission(Authentication authentication, Object domainObject, Object permission) {
        // Implementation details...
    }
    
    // Check permission by object ID and type
    @Override  
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        // Implementation details...
    }
    
    // Configuration methods
    public void setObjectIdentityRetrievalStrategy(ObjectIdentityRetrievalStrategy strategy);
    public void setObjectIdentityGenerator(ObjectIdentityGenerator generator);
    public void setSidRetrievalStrategy(SidRetrievalStrategy strategy);
    public void setPermissionFactory(PermissionFactory factory);
}

Basic Configuration

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
    
    @Bean
    public MethodSecurityExpressionHandler methodSecurityExpressionHandler(AclService aclService) {
        DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
        handler.setPermissionEvaluator(new AclPermissionEvaluator(aclService));
        return handler;
    }
}

Advanced Configuration

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class AdvancedMethodSecurityConfig {
    
    @Bean
    public AclPermissionEvaluator aclPermissionEvaluator(AclService aclService) {
        AclPermissionEvaluator evaluator = new AclPermissionEvaluator(aclService);
        
        // Custom strategies
        evaluator.setObjectIdentityRetrievalStrategy(objectIdentityRetrievalStrategy());
        evaluator.setSidRetrievalStrategy(sidRetrievalStrategy());
        evaluator.setPermissionFactory(permissionFactory());
        
        return evaluator;
    }
    
    @Bean
    public MethodSecurityExpressionHandler methodSecurityExpressionHandler(
            AclPermissionEvaluator permissionEvaluator,
            AclPermissionCacheOptimizer cacheOptimizer) {
        
        DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
        handler.setPermissionEvaluator(permissionEvaluator);
        handler.setPermissionCacheOptimizer(cacheOptimizer);
        return handler;
    }
    
    @Bean
    public AclPermissionCacheOptimizer permissionCacheOptimizer(AclService aclService) {
        return new AclPermissionCacheOptimizer(aclService);
    }
}

Pre-Authorization with @PreAuthorize

Use @PreAuthorize to check permissions before method execution:

Basic Usage

@Service
public class DocumentService {
    
    // Check READ permission on document object
    @PreAuthorize("hasPermission(#document, 'READ')")
    public Document viewDocument(Document document) {
        return document;
    }
    
    // Check WRITE permission using object parameter
    @PreAuthorize("hasPermission(#document, 'WRITE')")  
    public Document updateDocument(Document document, String content) {
        document.setContent(content);
        return documentRepository.save(document);
    }
    
    // Check permission by ID and type
    @PreAuthorize("hasPermission(#documentId, 'com.example.Document', 'DELETE')")
    public void deleteDocument(Long documentId) {
        documentRepository.deleteById(documentId);
    }
    
    // Multiple permission checks
    @PreAuthorize("hasPermission(#document, 'READ') and hasPermission(#document, 'WRITE')")
    public Document editDocument(Document document) {
        // User needs both READ and WRITE permissions
        return document;
    }
}

Advanced Pre-Authorization

@Service
public class FolderService {
    
    // Check permission with custom logic
    @PreAuthorize("hasPermission(#folder, 'ADMINISTRATION') or " +
                  "(hasPermission(#folder, 'WRITE') and #folder.owner == authentication.name)")
    public void changeFolderSettings(Folder folder, Map<String, Object> settings) {
        // Only folder admins or owners can change settings
    }
    
    // Permission with role fallback
    @PreAuthorize("hasRole('ADMIN') or hasPermission(#folder, 'CREATE')")  
    public Document createDocumentInFolder(Folder folder, Document document) {
        document.setFolder(folder);
        return documentRepository.save(document);
    }
    
    // Complex permission logic
    @PreAuthorize("@folderSecurityService.canAccess(#folderId, authentication)")
    public List<Document> getFolderContents(Long folderId) {
        return documentRepository.findByFolderId(folderId);
    }
}

@Service
public class FolderSecurityService {
    
    @Autowired
    private AclPermissionEvaluator permissionEvaluator;
    
    public boolean canAccess(Long folderId, Authentication authentication) {
        return permissionEvaluator.hasPermission(
            authentication, folderId, "com.example.Folder", "READ"
        ) || hasInheritedAccess(folderId, authentication);
    }
}

Post-Authorization with @PostAuthorize

Use @PostAuthorize to check permissions on method return values:

@Service
public class DocumentService {
    
    // Check permission on returned object
    @PostAuthorize("hasPermission(returnObject, 'READ')")
    public Document findDocument(Long id) {
        return documentRepository.findById(id).orElse(null);
    }
    
    // Multiple checks on return value
    @PostAuthorize("returnObject != null and hasPermission(returnObject, 'READ')")
    public Document getDocumentByTitle(String title) {
        return documentRepository.findByTitle(title);
    }
    
    // Check permissions on nested objects
    @PostAuthorize("returnObject.folder == null or hasPermission(returnObject.folder, 'READ')")
    public Document getDocumentWithFolder(Long id) {
        return documentRepository.findByIdWithFolder(id);
    }
    
    // Custom permission validation  
    @PostAuthorize("@documentSecurityService.canView(returnObject, authentication)")
    public Document getDocument(Long id) {
        return documentRepository.findById(id).orElse(null);
    }
}

Collection Filtering with @PostFilter

Filter returned collections based on permissions:

@Service
public class DocumentService {
    
    // Filter list - only return documents user can read
    @PostFilter("hasPermission(filterObject, 'READ')")
    public List<Document> getAllDocuments() {
        return documentRepository.findAll();
    }
    
    // Filter with additional criteria
    @PostFilter("hasPermission(filterObject, 'READ') and filterObject.published == true")
    public List<Document> getPublishedDocuments() {
        return documentRepository.findAll();
    }
    
    // Filter complex objects
    @PostFilter("hasPermission(filterObject.document, 'READ')")
    public List<DocumentWithMetadata> getDocumentsWithMetadata() {
        return documentRepository.findAllWithMetadata();
    }
    
    // Filter based on different permission levels
    @PostFilter("hasPermission(filterObject, 'READ') or " +
                "hasPermission(filterObject, 'WRITE') or " +  
                "hasPermission(filterObject, 'ADMINISTRATION')")
    public List<Document> getAccessibleDocuments() {
        return documentRepository.findAll();
    }
}

Input Filtering with @PreFilter

Filter input collections before method execution:

@Service  
public class DocumentService {
    
    // Filter input list - only process documents user can write to
    @PreFilter("hasPermission(filterObject, 'WRITE')")
    public List<Document> updateMultipleDocuments(List<Document> documents) {
        // Only documents with WRITE permission will be processed
        return documents.stream()
                .map(doc -> {
                    doc.setLastModified(new Date());
                    return documentRepository.save(doc);
                })
                .collect(Collectors.toList());
    }
    
    // Filter with permission and business logic
    @PreFilter("hasPermission(filterObject, 'DELETE') and filterObject.status != 'PUBLISHED'")
    public void deleteMultipleDocuments(List<Document> documents) {
        documents.forEach(doc -> documentRepository.delete(doc));
    }
    
    // Filter by ID list
    @PreFilter("hasPermission(filterObject, 'com.example.Document', 'READ')")
    public List<Document> getDocumentsByIds(List<Long> documentIds) {
        return documentRepository.findAllById(documentIds);
    }
}

Permission Types

The ACL system supports multiple ways to specify permissions:

String Permissions

@PreAuthorize("hasPermission(#document, 'READ')")
@PreAuthorize("hasPermission(#document, 'WRITE')")  
@PreAuthorize("hasPermission(#document, 'DELETE')")
@PreAuthorize("hasPermission(#document, 'ADMINISTRATION')")

// Custom permissions (must be registered in PermissionFactory)
@PreAuthorize("hasPermission(#document, 'APPROVE')")
@PreAuthorize("hasPermission(#document, 'PUBLISH')")

Integer Mask Permissions

// Using BasePermission mask values
@PreAuthorize("hasPermission(#document, 1)")    // READ
@PreAuthorize("hasPermission(#document, 2)")    // WRITE  
@PreAuthorize("hasPermission(#document, 4)")    // CREATE
@PreAuthorize("hasPermission(#document, 8)")    // DELETE
@PreAuthorize("hasPermission(#document, 16)")   // ADMINISTRATION

// Combined permissions using bitwise OR
@PreAuthorize("hasPermission(#document, 3)")    // READ + WRITE (1 | 2)

Permission Objects

@Service
public class DocumentService {
    
    // Inject permission constants  
    @PreAuthorize("hasPermission(#document, T(org.springframework.security.acls.domain.BasePermission).READ)")
    public Document viewDocument(Document document) {
        return document;
    }
    
    // Custom permission objects
    @PreAuthorize("hasPermission(#document, T(com.example.CustomPermission).APPROVE)")
    public void approveDocument(Document document) {
        document.setApproved(true);
        documentRepository.save(document);
    }
}

Performance Optimization

Batch Loading with AclPermissionCacheOptimizer

The cache optimizer pre-loads ACL data for collections to avoid N+1 queries:

@Configuration
public class PerformanceConfig {
    
    @Bean
    public AclPermissionCacheOptimizer aclPermissionCacheOptimizer(AclService aclService) {
        AclPermissionCacheOptimizer optimizer = new AclPermissionCacheOptimizer(aclService);
        
        // Configure custom strategies if needed
        optimizer.setObjectIdentityRetrievalStrategy(objectIdentityRetrievalStrategy());
        optimizer.setSidRetrievalStrategy(sidRetrievalStrategy());
        
        return optimizer;
    }
}

The optimizer automatically detects collection parameters and return values:

@Service
public class OptimizedDocumentService {
    
    // Optimizer will batch-load ACLs for all documents before filtering
    @PostFilter("hasPermission(filterObject, 'READ')")
    public List<Document> getAllDocuments() {
        return documentRepository.findAll(); // Single query + batch ACL load
    }
    
    // Optimizer detects collection parameter  
    @PreFilter("hasPermission(filterObject, 'WRITE')")
    public void updateDocuments(List<Document> documents) {
        // ACLs loaded in batch before filtering
        documents.forEach(documentRepository::save);
    }
}

Custom Permission Caching

Implement custom caching for complex permission logic:

@Service
@CacheConfig(cacheNames = "permissions")
public class CachedPermissionService {
    
    @Cacheable(key = "#objectId + ':' + #targetType + ':' + #permission + ':' + #authentication.name")
    public boolean hasPermission(Long objectId, String targetType, String permission, Authentication authentication) {
        return aclPermissionEvaluator.hasPermission(authentication, objectId, targetType, permission);
    }
    
    @CacheEvict(key = "#objectId + ':*'", allEntries = true)
    public void clearPermissionCache(Long objectId) {
        // Called when object permissions change
    }
}

Error Handling

Handle permission evaluation errors gracefully:

@Service
public class RobustDocumentService {
    
    @PreAuthorize("@permissionChecker.canRead(#documentId, authentication)")
    public Document getDocument(Long documentId) {
        return documentRepository.findById(documentId).orElse(null);
    }
}

@Component
public class PermissionChecker {
    
    @Autowired
    private AclPermissionEvaluator permissionEvaluator;
    
    public boolean canRead(Long documentId, Authentication authentication) {
        try {
            return permissionEvaluator.hasPermission(
                authentication, documentId, "com.example.Document", "READ"
            );
        } catch (Exception e) {
            log.warn("Permission check failed for document {}: {}", documentId, e.getMessage());
            return false; // Fail securely
        }
    }
}

Global Exception Handling

@ControllerAdvice
public class SecurityExceptionHandler {
    
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException e) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body(new ErrorResponse("Access denied: " + e.getMessage()));
    }
    
    @ExceptionHandler(NotFoundException.class)
    public ResponseEntity<ErrorResponse> handleAclNotFound(NotFoundException e) {
        // ACL not found - might be legitimate (new object) or security issue
        log.debug("ACL not found: {}", e.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("Resource not found"));
    }
}

Testing Permission Logic

Unit Testing

@ExtendWith(MockitoExtension.class)
class DocumentServiceTest {
    
    @Mock
    private AclPermissionEvaluator permissionEvaluator;
    
    @Mock
    private DocumentRepository documentRepository;
    
    @InjectMocks
    private DocumentService documentService;
    
    @Test
    void testPreAuthorizeWithPermission() {
        // Given
        Document document = new Document(1L, "Test");
        Authentication auth = createAuthentication("user1");
        
        when(permissionEvaluator.hasPermission(auth, document, "READ"))
            .thenReturn(true);
        
        // When/Then - method should execute without exception
        assertThat(documentService.viewDocument(document)).isEqualTo(document);
    }
    
    @Test
    void testPreAuthorizeWithoutPermission() {
        // Given  
        Document document = new Document(1L, "Test");
        Authentication auth = createAuthentication("user1");
        
        when(permissionEvaluator.hasPermission(auth, document, "READ"))
            .thenReturn(false);
        
        // When/Then - should throw AccessDeniedException
        assertThatThrownBy(() -> documentService.viewDocument(document))
            .isInstanceOf(AccessDeniedException.class);
    }
}

Integration Testing

@SpringBootTest
@Transactional
class DocumentServiceIntegrationTest {
    
    @Autowired
    private DocumentService documentService;
    
    @Autowired
    private MutableAclService aclService;
    
    @Test
    @WithMockUser(username = "user1", roles = "USER")
    void testDocumentAccessWithRealAcl() {
        // Given - create document with ACL
        Document document = documentRepository.save(new Document("Test"));
        
        ObjectIdentity identity = new ObjectIdentityImpl(Document.class, document.getId());
        MutableAcl acl = aclService.createAcl(identity);
        
        Sid userSid = new PrincipalSid("user1");
        acl.insertAce(0, BasePermission.READ, userSid, true);
        aclService.updateAcl(acl);
        
        // When/Then - user should be able to access document
        Document result = documentService.viewDocument(document);
        assertThat(result).isEqualTo(document);
    }
}

Testing with Security Context

@TestConfiguration
static class TestSecurityConfig {
    
    @Bean
    @Primary 
    public AclPermissionEvaluator testPermissionEvaluator() {
        return new AclPermissionEvaluator(aclService()) {
            @Override
            public boolean hasPermission(Authentication auth, Object domainObject, Object permission) {
                // Mock implementation for testing
                return "user1".equals(auth.getName()) && "READ".equals(permission);
            }
        };
    }
}

Best Practices

1. Use Method-Level Security Consistently

// Good - consistent permission checking
@Service
public class DocumentService {
    
    @PreAuthorize("hasPermission(#document, 'READ')")
    public Document getDocument(Document document) { /* */ }
    
    @PreAuthorize("hasPermission(#document, 'WRITE')")  
    public Document updateDocument(Document document) { /* */ }
    
    @PreAuthorize("hasPermission(#documentId, 'com.example.Document', 'DELETE')")
    public void deleteDocument(Long documentId) { /* */ }
}

// Avoid - mixing security approaches
@Service  
public class MixedSecurityService {
    
    @PreAuthorize("hasPermission(#document, 'READ')")
    public Document getDocument(Document document) { /* */ }
    
    // Inconsistent - manual permission checking
    public Document updateDocument(Document document) {
        if (!hasWritePermission(document)) {
            throw new AccessDeniedException("Access denied");
        }
        // Update logic...
    }
}

2. Handle Null Objects Gracefully

@Service
public class SafeDocumentService {
    
    // Good - null check built into expression
    @PostAuthorize("returnObject == null or hasPermission(returnObject, 'READ')")
    public Document findDocument(Long id) {
        return documentRepository.findById(id).orElse(null);
    }
    
    // Alternative - separate null handling
    public Document getDocument(Long id) {
        Document doc = documentRepository.findById(id).orElse(null);
        if (doc != null && !hasReadPermission(doc)) {
            throw new AccessDeniedException("Access denied");
        }
        return doc;
    }
}

3. Optimize Collection Operations

// Good - use @PostFilter for automatic optimization
@PostFilter("hasPermission(filterObject, 'READ')")
public List<Document> getUserDocuments() {
    return documentRepository.findAll();
}

// Better - pre-filter at database level when possible
public List<Document> getUserDocuments(Authentication authentication) {
    List<Sid> sids = sidRetrievalStrategy.getSids(authentication);
    return documentRepository.findDocumentsAccessibleToSids(sids);
}

4. Use Meaningful Permission Names

// Good - clear business intent
@PreAuthorize("hasPermission(#document, 'APPROVE')")  
@PreAuthorize("hasPermission(#document, 'PUBLISH')")
@PreAuthorize("hasPermission(#document, 'ARCHIVE')")

// Avoid - technical permission names without business context
@PreAuthorize("hasPermission(#document, 'PERM_001')")
@PreAuthorize("hasPermission(#document, 'FLAG_A')")

The permission evaluation system provides a powerful, declarative way to implement fine-grained security. Combined with proper configuration and setup, it enables maintainable, high-performance access control for complex applications.

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