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

acl-services.mddocs/

ACL Services

ACL Services provide the core functionality for reading and managing Access Control Lists. Spring Security ACL offers both read-only and mutable service interfaces, with production-ready JDBC implementations.

Overview

The ACL module provides two main service interfaces:

  • AclService - Read-only access to ACL data
  • MutableAclService - Full CRUD operations on ACL data

Both are backed by efficient JDBC implementations that work with any SQL database.

AclService Interface

The AclService provides read-only access to ACL data with methods optimized for different use cases:

package org.springframework.security.acls.model;

public interface AclService {
    
    // Find child object identities  
    List<ObjectIdentity> findChildren(ObjectIdentity parentIdentity);
    
    // Read single ACL (not recommended - doesn't allow SID filtering)
    Acl readAclById(ObjectIdentity object) throws NotFoundException;
    
    // Read single ACL with SID filtering (recommended)
    Acl readAclById(ObjectIdentity object, List<Sid> sids) throws NotFoundException;
    
    // Batch read ACLs
    Map<ObjectIdentity, Acl> readAclsById(List<ObjectIdentity> objects) throws NotFoundException;
    
    // Batch read ACLs with SID filtering (most efficient)
    Map<ObjectIdentity, Acl> readAclsById(List<ObjectIdentity> objects, List<Sid> sids) 
        throws NotFoundException;
}

Key Methods

Single ACL Retrieval:

@Autowired
private AclService aclService;

public boolean checkDocumentAccess(Long documentId, Authentication auth) {
    ObjectIdentity identity = new ObjectIdentityImpl(Document.class, documentId);
    List<Sid> sids = sidRetrievalStrategy.getSids(auth);
    
    try {
        // Load ACL with SID filtering for better performance
        Acl acl = aclService.readAclById(identity, sids);
        return acl.isGranted(Arrays.asList(BasePermission.READ), sids, false);
    } catch (NotFoundException e) {
        return false; // No ACL exists
    }
}

Batch ACL Retrieval:

public Map<Document, Boolean> checkMultipleDocuments(List<Document> documents, Authentication auth) {
    // Create object identities
    List<ObjectIdentity> identities = documents.stream()
        .map(doc -> new ObjectIdentityImpl(Document.class, doc.getId()))
        .collect(Collectors.toList());
    
    List<Sid> sids = sidRetrievalStrategy.getSids(auth);
    List<Permission> readPermission = Arrays.asList(BasePermission.READ);
    
    try {
        // Batch load ACLs - much more efficient than individual calls
        Map<ObjectIdentity, Acl> acls = aclService.readAclsById(identities, sids);
        
        return documents.stream()
            .collect(Collectors.toMap(
                doc -> doc,
                doc -> {
                    ObjectIdentity identity = new ObjectIdentityImpl(Document.class, doc.getId());
                    Acl acl = acls.get(identity);
                    return acl != null && acl.isGranted(readPermission, sids, false);
                }
            ));
    } catch (NotFoundException e) {
        // Return all false if any ACL is missing
        return documents.stream()
            .collect(Collectors.toMap(doc -> doc, doc -> false));
    }
}

Finding Child Objects:

public List<ObjectIdentity> getFolderContents(Long folderId) {
    ObjectIdentity folderIdentity = new ObjectIdentityImpl(Folder.class, folderId);
    return aclService.findChildren(folderIdentity);
}

MutableAclService Interface

The MutableAclService extends AclService with methods for creating, modifying, and deleting ACLs:

package org.springframework.security.acls.model;

public interface MutableAclService extends AclService {
    
    // Create new ACL
    MutableAcl createAcl(ObjectIdentity objectIdentity) throws AlreadyExistsException;
    
    // Delete ACL
    void deleteAcl(ObjectIdentity objectIdentity, boolean deleteChildren) 
        throws ChildrenExistException;
    
    // Update existing ACL
    MutableAcl updateAcl(MutableAcl acl) throws NotFoundException;
}

Creating ACLs

@Autowired
private MutableAclService mutableAclService;

public void createDocumentWithPermissions(Document document, String owner) {
    // Save document first
    document = documentRepository.save(document);
    
    // Create object identity
    ObjectIdentity identity = new ObjectIdentityImpl(Document.class, document.getId());
    
    try {
        // Create ACL
        MutableAcl acl = mutableAclService.createAcl(identity);
        
        // Set owner
        Sid ownerSid = new PrincipalSid(owner);
        acl.setOwner(ownerSid);
        
        // Grant owner full permissions
        acl.insertAce(0, BasePermission.ADMINISTRATION, ownerSid, true);
        acl.insertAce(1, BasePermission.DELETE, ownerSid, true);
        acl.insertAce(2, BasePermission.WRITE, ownerSid, true);
        acl.insertAce(3, BasePermission.READ, ownerSid, true);
        
        // Grant read permission to all authenticated users
        Sid userRole = new GrantedAuthoritySid("ROLE_USER");
        acl.insertAce(4, BasePermission.READ, userRole, true);
        
        // Save ACL
        mutableAclService.updateAcl(acl);
        
    } catch (AlreadyExistsException e) {
        throw new IllegalStateException("ACL already exists for document: " + document.getId());
    }
}

Updating ACL Permissions

public void grantUserPermission(Long documentId, String username, Permission permission) {
    ObjectIdentity identity = new ObjectIdentityImpl(Document.class, documentId);
    
    // Load existing ACL as mutable
    MutableAcl acl = (MutableAcl) mutableAclService.readAclById(identity);
    
    // Add new permission
    Sid userSid = new PrincipalSid(username);
    acl.insertAce(acl.getEntries().size(), permission, userSid, true);
    
    // Update ACL
    mutableAclService.updateAcl(acl);
}

public void revokeUserPermission(Long documentId, String username, Permission permission) {
    ObjectIdentity identity = new ObjectIdentityImpl(Document.class, documentId);
    MutableAcl acl = (MutableAcl) mutableAclService.readAclById(identity);
    
    Sid userSid = new PrincipalSid(username);
    
    // Find and remove matching ACE
    List<AccessControlEntry> entries = acl.getEntries();
    for (int i = 0; i < entries.size(); i++) {
        AccessControlEntry ace = entries.get(i);
        if (ace.getSid().equals(userSid) && ace.getPermission().equals(permission)) {
            acl.deleteAce(i);
            break;
        }
    }
    
    mutableAclService.updateAcl(acl);
}

Deleting ACLs

public void deleteDocument(Long documentId, boolean deleteChildACLs) {
    // Delete domain object first
    documentRepository.deleteById(documentId);
    
    // Delete ACL
    ObjectIdentity identity = new ObjectIdentityImpl(Document.class, documentId);
    
    try {
        mutableAclService.deleteAcl(identity, deleteChildACLs);
    } catch (ChildrenExistException e) {
        throw new IllegalStateException(
            "Cannot delete ACL - child ACLs exist. Use deleteChildACLs=true or delete children first.");
    }
}

JDBC Implementations

JdbcAclService

The production-ready read-only implementation:

@Configuration
public class AclServiceConfig {
    
    @Bean
    public AclService aclService(DataSource dataSource) {
        JdbcAclService service = new JdbcAclService(dataSource, lookupStrategy());
        return service;
    }
    
    @Bean 
    public LookupStrategy lookupStrategy() {
        return new BasicLookupStrategy(
            dataSource(),
            aclCache(),
            aclAuthorizationStrategy(), 
            auditLogger()
        );
    }
}

JdbcMutableAclService

The full-featured implementation with create/update/delete capabilities:

@Configuration
public class MutableAclServiceConfig {
    
    @Bean
    public MutableAclService mutableAclService(DataSource dataSource) {
        JdbcMutableAclService service = new JdbcMutableAclService(
            dataSource, 
            lookupStrategy(), 
            aclCache()
        );
        
        // Configure for your database
        service.setClassIdentityQuery("SELECT @@IDENTITY");     // SQL Server
        service.setSidIdentityQuery("SELECT @@IDENTITY");
        
        // For MySQL: 
        // service.setClassIdentityQuery("SELECT LAST_INSERT_ID()");
        // service.setSidIdentityQuery("SELECT LAST_INSERT_ID()");
        
        // For PostgreSQL:
        // service.setClassIdentityQuery("select currval(pg_get_serial_sequence('acl_class', 'id'))");
        // service.setSidIdentityQuery("select currval(pg_get_serial_sequence('acl_sid', 'id'))");
        
        return service;
    }
}

Database Configuration

The JDBC implementations require specific database tables:

-- ACL Class table
CREATE TABLE acl_class (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    class VARCHAR(100) NOT NULL,
    UNIQUE KEY unique_uk_2 (class)
);

-- ACL SID table  
CREATE TABLE acl_sid (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    principal BOOLEAN NOT NULL,
    sid VARCHAR(100) NOT NULL,
    UNIQUE KEY unique_uk_3 (sid, principal)
);

-- ACL Object Identity table
CREATE TABLE acl_object_identity (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    object_id_class BIGINT UNSIGNED NOT NULL,
    object_id_identity VARCHAR(36) NOT NULL,
    parent_object BIGINT UNSIGNED,
    owner_sid BIGINT UNSIGNED,
    entries_inheriting BOOLEAN NOT NULL,
    UNIQUE KEY unique_uk_4 (object_id_class, object_id_identity),
    CONSTRAINT foreign_fk_1 FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id),
    CONSTRAINT foreign_fk_2 FOREIGN KEY (object_id_class) REFERENCES acl_class (id),
    CONSTRAINT foreign_fk_3 FOREIGN KEY (owner_sid) REFERENCES acl_sid (id)
);

-- ACL Entry table
CREATE TABLE acl_entry (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    acl_object_identity BIGINT UNSIGNED NOT NULL,
    ace_order INTEGER NOT NULL,
    sid BIGINT UNSIGNED NOT NULL,
    mask INTEGER UNSIGNED NOT NULL,
    granting BOOLEAN NOT NULL,
    audit_success BOOLEAN NOT NULL,
    audit_failure BOOLEAN NOT NULL,
    UNIQUE KEY unique_uk_5 (acl_object_identity, ace_order),
    CONSTRAINT foreign_fk_4 FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id),
    CONSTRAINT foreign_fk_5 FOREIGN KEY (sid) REFERENCES acl_sid (id)
);

Advanced Features

Lookup Strategy

The LookupStrategy interface allows customization of how ACLs are retrieved:

public interface LookupStrategy {
    Map<ObjectIdentity, Acl> readAclsById(List<ObjectIdentity> objects, List<Sid> sids);
}

// Basic implementation for ANSI SQL databases
@Bean
public LookupStrategy lookupStrategy() {
    BasicLookupStrategy strategy = new BasicLookupStrategy(
        dataSource(),
        aclCache(),
        aclAuthorizationStrategy(),
        permissionGrantingStrategy()
    );
    
    // Customize SQL queries if needed
    strategy.setSelectClause("SELECT obj.object_id_identity, class.class, sid.sid, sid.principal, " +
                           "acl.entries_inheriting, acl.id as acl_id, acl.parent_object, acl.owner_sid, " +
                           "entry.id, entry.mask, entry.granting, entry.audit_success, entry.audit_failure ");
    
    return strategy;
}

Caching

ACL data is cached to improve performance:

@Bean
public AclCache aclCache() {
    // Use Spring's caching abstraction
    return new SpringCacheBasedAclCache(
        cacheManager().getCache("aclCache"),
        permissionGrantingStrategy(),
        aclAuthorizationStrategy()
    );
}

@Bean
public CacheManager cacheManager() {
    ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager("aclCache");
    return cacheManager;
}

Batch Processing

For better performance when working with many objects:

@Service
public class DocumentSecurityService {
    
    @Autowired 
    private MutableAclService aclService;
    
    @Transactional
    public void createDocumentsWithBulkPermissions(List<Document> documents, String owner) {
        // Save all documents first
        documents = documentRepository.saveAll(documents);
        
        // Create ACLs in batch
        for (Document doc : documents) {
            ObjectIdentity identity = new ObjectIdentityImpl(Document.class, doc.getId());
            
            MutableAcl acl = aclService.createAcl(identity);
            setupDefaultPermissions(acl, owner);
            aclService.updateAcl(acl);
        }
    }
    
    private void setupDefaultPermissions(MutableAcl acl, String owner) {
        Sid ownerSid = new PrincipalSid(owner);
        acl.setOwner(ownerSid);
        acl.insertAce(0, BasePermission.ADMINISTRATION, ownerSid, true);
        
        Sid userRole = new GrantedAuthoritySid("ROLE_USER"); 
        acl.insertAce(1, BasePermission.READ, userRole, true);
    }
}

Performance Optimization

1. Use SID Filtering

Always provide SID lists when possible to reduce data transfer:

// Less efficient - loads all ACL entries
Acl acl = aclService.readAclById(objectIdentity);

// More efficient - only loads relevant entries
List<Sid> sids = sidRetrievalStrategy.getSids(authentication);
Acl acl = aclService.readAclById(objectIdentity, sids);

2. Batch Operations

Load multiple ACLs in single call:

// Less efficient - multiple database queries
List<Boolean> results = new ArrayList<>();
for (ObjectIdentity identity : identities) {
    Acl acl = aclService.readAclById(identity, sids);
    results.add(acl.isGranted(permissions, sids, false));
}

// More efficient - single batch query
Map<ObjectIdentity, Acl> acls = aclService.readAclsById(identities, sids);
List<Boolean> results = identities.stream()
    .map(identity -> {
        Acl acl = acls.get(identity);
        return acl != null && acl.isGranted(permissions, sids, false);
    })
    .collect(Collectors.toList());

3. Cache Configuration

@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("aclCache");
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(Duration.ofMinutes(10))
            .recordStats());
        return cacheManager;
    }
}

4. Connection Pooling

@Configuration
public class DataSourceConfig {
    
    @Bean
    @Primary
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost/acl_db");
        config.setUsername("user");
        config.setPassword("password");
        
        // Optimize for ACL queries
        config.setMaximumPoolSize(20);
        config.setMinimumIdle(5);
        config.setConnectionTimeout(30000);
        config.setIdleTimeout(600000);
        
        return new HikariDataSource(config);
    }
}

Exception Handling

Handle ACL-specific exceptions appropriately:

@Service
public class SecureDocumentService {
    
    public Optional<Document> getDocumentIfAllowed(Long documentId, Authentication auth) {
        try {
            ObjectIdentity identity = new ObjectIdentityImpl(Document.class, documentId);
            List<Sid> sids = sidRetrievalStrategy.getSids(auth);
            
            Acl acl = aclService.readAclById(identity, sids);
            
            if (acl.isGranted(Arrays.asList(BasePermission.READ), sids, false)) {
                return documentRepository.findById(documentId);
            }
            return Optional.empty();
            
        } catch (NotFoundException e) {
            log.debug("No ACL found for document {}, denying access", documentId);
            return Optional.empty();
            
        } catch (UnloadedSidException e) {
            log.warn("ACL loaded without required SIDs for document {}", documentId);
            // Retry with full SID loading or return empty
            return Optional.empty();
        }
    }
}

Best Practices

1. Use Appropriate Service Level

// For read-only operations, inject AclService
@Autowired
private AclService aclService;

// For modifications, inject MutableAclService  
@Autowired 
private MutableAclService mutableAclService;

2. Handle ACL Lifecycle with Domain Objects

@Entity
@EntityListeners(DocumentAclListener.class)
public class Document {
    // Entity fields...
}

@Component
public class DocumentAclListener {
    
    @Autowired
    private MutableAclService aclService;
    
    @PostPersist
    public void createAcl(Document document) {
        ObjectIdentity identity = new ObjectIdentityImpl(Document.class, document.getId());
        // Create ACL with default permissions
    }
    
    @PostRemove  
    public void deleteAcl(Document document) {
        ObjectIdentity identity = new ObjectIdentityImpl(Document.class, document.getId());
        aclService.deleteAcl(identity, true);
    }
}

3. Transactional Boundaries

@Transactional
public void createDocumentWithPermissions(Document document) {
    // Both domain object and ACL operations should be in same transaction
    document = documentRepository.save(document);
    
    ObjectIdentity identity = new ObjectIdentityImpl(Document.class, document.getId());
    MutableAcl acl = mutableAclService.createAcl(identity);
    // Configure ACL...
    mutableAclService.updateAcl(acl);
}

The ACL services provide a solid foundation for managing permissions at scale. The next step is understanding how to integrate these services with Spring Security's permission evaluation for a modern annotation-based approach.

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