Spring Security ACL provides instance-based security for domain objects through a comprehensive Access Control List implementation
—
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.
The ACL module provides two main service interfaces:
AclService - Read-only access to ACL dataMutableAclService - Full CRUD operations on ACL dataBoth are backed by efficient JDBC implementations that work with any SQL database.
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;
}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);
}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;
}@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());
}
}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);
}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.");
}
}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()
);
}
}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;
}
}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)
);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;
}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;
}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);
}
}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);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());@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;
}
}@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);
}
}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();
}
}
}// For read-only operations, inject AclService
@Autowired
private AclService aclService;
// For modifications, inject MutableAclService
@Autowired
private MutableAclService mutableAclService;@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);
}
}@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