Fine-Grained Authorization with Spring Security ACL in Java

Introduction

Spring Security ACL (Access Control List) provides a powerful framework for implementing fine-grained, instance-based security in Java applications. Unlike role-based security that applies broadly, ACL allows you to control access to specific domain objects based on user permissions. This guide explores how to implement and leverage Spring Security ACL for complex authorization scenarios.


Article: Implementing Fine-Grained Security with Spring Security ACL

Spring Security ACL enables you to define permissions for specific domain object instances, allowing complex authorization rules like "User X can READ Document Y" or "User A can ADMINISTER Project B". This is essential for multi-tenant applications, document management systems, and enterprise applications requiring precise access control.

1. Spring Security ACL Architecture

Key Components:

  • ACL Entry - Individual permission (READ, WRITE, CREATE, DELETE, ADMINISTER)
  • ACL Object Identity - Links domain objects to ACL entries
  • ACL SID - Security Identity (User or Authority)
  • ACL Class - Domain object type mapping
  • ACL Cache - Performance optimization for permission checks

ACL Data Model:

acl_class (domain object types)
acl_sid (security identities) 
acl_object_identity (object instances)
acl_entry (permissions for SIDs on objects)

2. Maven Dependencies

pom.xml:

<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<spring-security.version>6.1.0</spring-security.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Spring Security ACL -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security.version}</version>
</dependency>
<!-- EhCache for ACL caching -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ehcache</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.13.0</version>
</dependency>
</dependencies>

3. Application Configuration

application.yml:

spring:
datasource:
url: jdbc:postgresql://localhost:5432/acl_demo
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:password}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
cache:
type: ehcache
ehcache:
config: classpath:ehcache.xml
# ACL Configuration
app:
acl:
cache:
enabled: true
ttl-minutes: 30
system-admin-role: ROLE_SYSTEM_ADMIN
default-owner: system
inherit-from-parent: true
# Security
security:
basic:
enabled: false

EhCache Configuration (ehcache.xml):

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
updateCheck="false">
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120" />
<!-- ACL Cache -->
<cache name="aclCache"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="600"
overflowToDisk="false"
statistics="true" />
<cache name="aclEntryCache"
maxElementsInMemory="5000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="600"
overflowToDisk="false"
statistics="true" />
<cache name="aclObjectIdentityCache"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="600"
overflowToDisk="false"
statistics="true" />
</ehcache>

4. Database Schema for ACL

PostgreSQL Schema:

-- ACL Tables Schema
CREATE TABLE acl_sid (
id BIGSERIAL PRIMARY KEY,
principal BOOLEAN NOT NULL,
sid VARCHAR(100) NOT NULL,
CONSTRAINT unique_acl_sid UNIQUE (sid, principal)
);
CREATE TABLE acl_class (
id BIGSERIAL PRIMARY KEY,
class VARCHAR(255) NOT NULL UNIQUE
);
CREATE TABLE acl_object_identity (
id BIGSERIAL PRIMARY KEY,
object_id_class BIGINT NOT NULL,
object_id_identity VARCHAR(36) NOT NULL,
parent_object BIGINT,
owner_sid BIGINT,
entries_inheriting BOOLEAN NOT NULL,
CONSTRAINT unique_acl_object_identity UNIQUE (object_id_class, object_id_identity),
CONSTRAINT fk_acl_object_identity_parent FOREIGN KEY (parent_object) 
REFERENCES acl_object_identity (id),
CONSTRAINT fk_acl_object_identity_class FOREIGN KEY (object_id_class) 
REFERENCES acl_class (id),
CONSTRAINT fk_acl_object_identity_owner FOREIGN KEY (owner_sid) 
REFERENCES acl_sid (id)
);
CREATE TABLE acl_entry (
id BIGSERIAL PRIMARY KEY,
acl_object_identity BIGINT NOT NULL,
ace_order INTEGER NOT NULL,
sid BIGINT NOT NULL,
mask INTEGER NOT NULL,
granting BOOLEAN NOT NULL,
audit_success BOOLEAN NOT NULL,
audit_failure BOOLEAN NOT NULL,
CONSTRAINT unique_acl_entry UNIQUE (acl_object_identity, ace_order),
CONSTRAINT fk_acl_entry_object FOREIGN KEY (acl_object_identity) 
REFERENCES acl_object_identity (id),
CONSTRAINT fk_acl_entry_sid FOREIGN KEY (sid) 
REFERENCES acl_sid (id)
);
-- Indexes for performance
CREATE INDEX idx_acl_sid_sid ON acl_sid(sid);
CREATE INDEX idx_acl_object_identity_object ON acl_object_identity(object_id_class, object_id_identity);
CREATE INDEX idx_acl_entry_identity ON acl_entry(acl_object_identity);
CREATE INDEX idx_acl_entry_sid ON acl_entry(sid);
-- Insert default system SID
INSERT INTO acl_sid (principal, sid) VALUES (true, 'system');

5. Spring Security ACL Configuration

ACL Configuration:

package com.myapp.acl.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.acls.AclPermissionCacheOptimizer;
import org.springframework.security.acls.AclPermissionEvaluator;
import org.springframework.security.acls.domain.*;
import org.springframework.security.acls.jdbc.BasicLookupStrategy;
import org.springframework.security.acls.jdbc.JdbcMutableAclService;
import org.springframework.security.acls.jdbc.LookupStrategy;
import org.springframework.security.acls.model.AclCache;
import org.springframework.security.acls.model.AclService;
import org.springframework.security.acls.model.PermissionGrantingStrategy;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import javax.sql.DataSource;
import java.util.Objects;
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclConfig {
@Bean
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
EhCacheManagerFactoryBean cacheManager = new EhCacheManagerFactoryBean();
cacheManager.setConfigLocation(new ClassPathResource("ehcache.xml"));
cacheManager.setShared(true);
return cacheManager;
}
@Bean
public CacheManager cacheManager() {
return new EhCacheCacheManager(Objects.requireNonNull(ehCacheManagerFactoryBean().getObject()));
}
@Bean
public AclCache aclCache() {
return new EhCacheBasedAclCache(
Objects.requireNonNull(ehCacheManagerFactoryBean().getObject()).getEhcache("aclCache"),
permissionGrantingStrategy(),
aclAuthorizationStrategy()
);
}
@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
}
@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_SYSTEM_ADMIN"));
}
@Bean
public LookupStrategy lookupStrategy() {
BasicLookupStrategy lookupStrategy = new BasicLookupStrategy(
dataSource,
aclCache(),
aclAuthorizationStrategy(),
new ConsoleAuditLogger()
);
lookupStrategy.setAclClassIdSupported(true);
return lookupStrategy;
}
@Bean
public JdbcMutableAclService aclService(DataSource dataSource) {
JdbcMutableAclService aclService = new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache());
// Custom queries for PostgreSQL
aclService.setClassIdentityQuery("SELECT currval(pg_get_serial_sequence('acl_class', 'id'))");
aclService.setSidIdentityQuery("SELECT currval(pg_get_serial_sequence('acl_sid', 'id'))");
return aclService;
}
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(AclService aclService) {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService);
expressionHandler.setPermissionEvaluator(permissionEvaluator);
expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService));
return expressionHandler;
}
@Bean
public AclPermissionEvaluator aclPermissionEvaluator(AclService aclService) {
return new AclPermissionEvaluator(aclService);
}
}

6. Domain Models with ACL Support

Secured Document Entity:

package com.myapp.acl.model;
import jakarta.persistence.*;
import org.springframework.security.core.context.SecurityContextHolder;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "documents")
public class Document {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String uuid = UUID.randomUUID().toString();
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@Enumerated(EnumType.STRING)
private DocumentType type;
@Enumerated(EnumType.STRING)
private DocumentStatus status = DocumentStatus.DRAFT;
@Column(nullable = false)
private String owner;
private String createdBy;
private String lastModifiedBy;
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime lastModifiedAt;
// ACL-specific field for object identity
@Transient
private String objectIdentity;
// Constructors
public Document() {
this.createdAt = LocalDateTime.now();
this.createdBy = getCurrentUsername();
this.owner = getCurrentUsername();
}
public Document(String title, String content, DocumentType type) {
this();
this.title = title;
this.content = content;
this.type = type;
}
// Business methods
public String getObjectIdentity() {
return this.uuid != null ? this.uuid : String.valueOf(this.id);
}
public Class<?> getAclClass() {
return Document.class;
}
private String getCurrentUsername() {
return SecurityContextHolder.getContext().getAuthentication() != null ?
SecurityContextHolder.getContext().getAuthentication().getName() : "system";
}
@PreUpdate
public void preUpdate() {
this.lastModifiedAt = LocalDateTime.now();
this.lastModifiedBy = getCurrentUsername();
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUuid() { return uuid; }
public void setUuid(String uuid) { this.uuid = uuid; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public DocumentType getType() { return type; }
public void setType(DocumentType type) { this.type = type; }
public DocumentStatus getStatus() { return status; }
public void setStatus(DocumentStatus status) { this.status = status; }
public String getOwner() { return owner; }
public void setOwner(String owner) { this.owner = owner; }
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
public String getLastModifiedBy() { return lastModifiedBy; }
public void setLastModifiedBy(String lastModifiedBy) { this.lastModifiedBy = lastModifiedBy; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getLastModifiedAt() { return lastModifiedAt; }
public void setLastModifiedAt(LocalDateTime lastModifiedAt) { this.lastModifiedAt = lastModifiedAt; }
public void setObjectIdentity(String objectIdentity) { this.objectIdentity = objectIdentity; }
}
enum DocumentType {
INTERNAL, CONFIDENTIAL, PUBLIC, PERSONAL
}
enum DocumentStatus {
DRAFT, REVIEW, APPROVED, ARCHIVED
}

Project Entity with ACL:

package com.myapp.acl.model;
import jakarta.persistence.*;
import org.springframework.security.core.context.SecurityContextHolder;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Entity
@Table(name = "projects")
public class Project {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String uuid = UUID.randomUUID().toString();
@Column(nullable = false)
private String name;
private String description;
@Enumerated(EnumType.STRING)
private ProjectStatus status = ProjectStatus.ACTIVE;
@Column(nullable = false)
private String owner;
private String createdBy;
private LocalDateTime createdAt;
@OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Document> documents = new ArrayList<>();
// ACL-specific field
@Transient
private String objectIdentity;
// Constructors
public Project() {
this.createdAt = LocalDateTime.now();
this.createdBy = getCurrentUsername();
}
public Project(String name, String description, String owner) {
this();
this.name = name;
this.description = description;
this.owner = owner;
}
// Business methods
public String getObjectIdentity() {
return this.uuid != null ? this.uuid : String.valueOf(this.id);
}
public Class<?> getAclClass() {
return Project.class;
}
private String getCurrentUsername() {
return SecurityContextHolder.getContext().getAuthentication() != null ?
SecurityContextHolder.getContext().getAuthentication().getName() : "system";
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUuid() { return uuid; }
public void setUuid(String uuid) { this.uuid = uuid; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public ProjectStatus getStatus() { return status; }
public void setStatus(ProjectStatus status) { this.status = status; }
public String getOwner() { return owner; }
public void setOwner(String owner) { this.owner = owner; }
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public List<Document> getDocuments() { return documents; }
public void setDocuments(List<Document> documents) { this.documents = documents; }
public void setObjectIdentity(String objectIdentity) { this.objectIdentity = objectIdentity; }
}
enum ProjectStatus {
ACTIVE, INACTIVE, COMPLETED, CANCELLED
}

7. Custom Permission Definitions

Custom Permissions:

package com.myapp.acl.permission;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.model.Permission;
public class CustomPermission extends BasePermission {
public static final Permission VIEW = new CustomPermission(1 << 5, 'V'); // 32
public static final Permission APPROVE = new CustomPermission(1 << 6, 'A'); // 64
public static final Permission SHARE = new CustomPermission(1 << 7, 'S'); // 128
public static final Permission EXPORT = new CustomPermission(1 << 8, 'E'); // 256
protected CustomPermission(int mask, char code) {
super(mask, code);
}
public static Permission buildFromMask(int mask) {
return BasePermission.buildFromMask(mask);
}
}

Permission Utility:

package com.myapp.acl.permission;
import org.springframework.security.acls.model.Permission;
import java.util.ArrayList;
import java.util.List;
public class PermissionUtils {
public static final int FULL_PERMISSION_MASK = 
CustomPermission.READ.getMask() |
CustomPermission.WRITE.getMask() |
CustomPermission.CREATE.getMask() |
CustomPermission.DELETE.getMask() |
CustomPermission.ADMINISTRATION.getMask() |
CustomPermission.VIEW.getMask() |
CustomPermission.APPROVE.getMask() |
CustomPermission.SHARE.getMask() |
CustomPermission.EXPORT.getMask();
public static List<Permission> getAllPermissions() {
List<Permission> permissions = new ArrayList<>();
permissions.add(CustomPermission.READ);
permissions.add(CustomPermission.WRITE);
permissions.add(CustomPermission.CREATE);
permissions.add(CustomPermission.DELETE);
permissions.add(CustomPermission.ADMINISTRATION);
permissions.add(CustomPermission.VIEW);
permissions.add(CustomPermission.APPROVE);
permissions.add(CustomPermission.SHARE);
permissions.add(CustomPermission.EXPORT);
return permissions;
}
public static String getPermissionString(int mask) {
StringBuilder sb = new StringBuilder();
if ((mask & CustomPermission.READ.getMask()) != 0) sb.append("R");
if ((mask & CustomPermission.WRITE.getMask()) != 0) sb.append("W");
if ((mask & CustomPermission.CREATE.getMask()) != 0) sb.append("C");
if ((mask & CustomPermission.DELETE.getMask()) != 0) sb.append("D");
if ((mask & CustomPermission.ADMINISTRATION.getMask()) != 0) sb.append("A");
if ((mask & CustomPermission.VIEW.getMask()) != 0) sb.append("V");
if ((mask & CustomPermission.APPROVE.getMask()) != 0) sb.append("P");
if ((mask & CustomPermission.SHARE.getMask()) != 0) sb.append("S");
if ((mask & CustomPermission.EXPORT.getMask()) != 0) sb.append("E");
return sb.toString();
}
}

8. ACL Service Layer

ACL Management Service:

package com.myapp.acl.service;
import com.myapp.acl.model.Document;
import com.myapp.acl.model.Project;
import com.myapp.acl.permission.CustomPermission;
import com.myapp.acl.permission.PermissionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.domain.GrantedAuthoritySid;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.*;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class AclManagementService {
private static final Logger logger = LoggerFactory.getLogger(AclManagementService.class);
private final MutableAclService aclService;
public AclManagementService(MutableAclService aclService) {
this.aclService = aclService;
}
// Document ACL Methods
@PreAuthorize("hasPermission(#document, 'CREATE')")
public void createDocumentWithAcl(Document document) {
// Create ACL for the document
createAcl(document, document.getOwner());
// Grant permissions to owner
grantPermission(document, document.getOwner(), 
BasePermission.READ, BasePermission.WRITE, BasePermission.DELETE, CustomPermission.VIEW);
logger.info("Created ACL for document: {} owned by: {}", 
document.getTitle(), document.getOwner());
}
@PreAuthorize("hasPermission(#document, 'ADMINISTRATION')")
public void grantDocumentPermission(Document document, String username, Permission... permissions) {
grantPermission(document, username, permissions);
logger.info("Granted permissions {} to user {} for document: {}", 
PermissionUtils.getPermissionString(getMask(permissions)), 
username, document.getTitle());
}
@PreAuthorize("hasPermission(#document, 'ADMINISTRATION')")
public void revokeDocumentPermission(Document document, String username, Permission... permissions) {
revokePermission(document, username, permissions);
logger.info("Revoked permissions {} from user {} for document: {}", 
PermissionUtils.getPermissionString(getMask(permissions)), 
username, document.getTitle());
}
@PreAuthorize("hasPermission(#document, 'ADMINISTRATION')")
public void deleteDocumentAcl(Document document) {
deleteAcl(document);
logger.info("Deleted ACL for document: {}", document.getTitle());
}
// Project ACL Methods
@PreAuthorize("hasPermission(#project, 'CREATE')")
public void createProjectWithAcl(Project project) {
// Create ACL for the project
createAcl(project, project.getOwner());
// Grant full permissions to owner
grantPermission(project, project.getOwner(), 
BasePermission.READ, BasePermission.WRITE, BasePermission.CREATE, 
BasePermission.DELETE, BasePermission.ADMINISTRATION);
logger.info("Created ACL for project: {} owned by: {}", 
project.getName(), project.getOwner());
}
@PreAuthorize("hasPermission(#project, 'ADMINISTRATION')")
public void grantProjectPermission(Project project, String username, Permission... permissions) {
grantPermission(project, username, permissions);
logger.info("Granted permissions {} to user {} for project: {}", 
PermissionUtils.getPermissionString(getMask(permissions)), 
username, project.getName());
}
@PreAuthorize("hasPermission(#project, 'ADMINISTRATION')")
public void addProjectMember(Project project, String username) {
grantPermission(project, username, 
BasePermission.READ, BasePermission.WRITE, CustomPermission.VIEW);
logger.info("Added user {} as member to project: {}", username, project.getName());
}
// Generic ACL Methods
public void createAcl(Object domainObject, String owner) {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(domainObject.getClass(), 
getObjectIdentity(domainObject));
MutableAcl acl = aclService.createAcl(objectIdentity);
// Set owner
Sid ownerSid = new PrincipalSid(owner);
acl.setOwner(ownerSid);
acl.setEntriesInheriting(true);
aclService.updateAcl(acl);
}
public void grantPermission(Object domainObject, String username, Permission... permissions) {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(domainObject.getClass(), 
getObjectIdentity(domainObject));
Sid sid = new PrincipalSid(username);
grantPermission(objectIdentity, sid, permissions);
}
public void grantAuthorityPermission(Object domainObject, String authority, Permission... permissions) {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(domainObject.getClass(), 
getObjectIdentity(domainObject));
Sid sid = new GrantedAuthoritySid(authority);
grantPermission(objectIdentity, sid, permissions);
}
private void grantPermission(ObjectIdentity objectIdentity, Sid sid, Permission... permissions) {
MutableAcl acl;
try {
acl = (MutableAcl) aclService.readAclById(objectIdentity);
} catch (NotFoundException e) {
acl = aclService.createAcl(objectIdentity);
}
for (Permission permission : permissions) {
acl.insertAce(acl.getEntries().size(), permission, sid, true);
}
aclService.updateAcl(acl);
}
public void revokePermission(Object domainObject, String username, Permission... permissions) {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(domainObject.getClass(), 
getObjectIdentity(domainObject));
try {
MutableAcl acl = (MutableAcl) aclService.readAclById(objectIdentity);
Sid sid = new PrincipalSid(username);
List<AccessControlEntry> entries = acl.getEntries();
for (int i = entries.size() - 1; i >= 0; i--) {
AccessControlEntry entry = entries.get(i);
if (entry.getSid().equals(sid) && hasPermission(entry.getPermission(), permissions)) {
acl.deleteAce(i);
}
}
aclService.updateAcl(acl);
} catch (NotFoundException e) {
logger.warn("ACL not found for object: {}", objectIdentity.getIdentifier());
}
}
public void deleteAcl(Object domainObject) {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(domainObject.getClass(), 
getObjectIdentity(domainObject));
aclService.deleteAcl(objectIdentity, true);
}
public boolean hasPermission(Object domainObject, Permission permission) {
ObjectIdentity objectIdentity = new ObjectIdentityImpl(domainObject.getClass(), 
getObjectIdentity(domainObject));
try {
Acl acl = aclService.readAclById(objectIdentity);
return acl.isGranted(List.of(permission), List.of(getCurrentUserSid()), false);
} catch (NotFoundException e) {
return false;
} catch (UnauthorizedSecurityException e) {
return false;
}
}
@PostFilter("hasPermission(filterObject, 'READ')")
public List<Document> filterReadableDocuments(List<Document> documents) {
// Spring Security will automatically filter the list
return documents;
}
@PostFilter("hasPermission(filterObject, 'READ')")
public List<Project> filterReadableProjects(List<Project> projects) {
// Spring Security will automatically filter the list
return projects;
}
// Utility Methods
private String getObjectIdentity(Object domainObject) {
if (domainObject instanceof Document) {
return ((Document) domainObject).getObjectIdentity();
} else if (domainObject instanceof Project) {
return ((Project) domainObject).getObjectIdentity();
}
return String.valueOf(System.identityHashCode(domainObject));
}
private boolean hasPermission(Permission entryPermission, Permission[] permissions) {
for (Permission permission : permissions) {
if (entryPermission.equals(permission)) {
return true;
}
}
return false;
}
private int getMask(Permission[] permissions) {
int mask = 0;
for (Permission permission : permissions) {
mask |= permission.getMask();
}
return mask;
}
private Sid getCurrentUserSid() {
return new PrincipalSid(SecurityContextHolder.getContext().getAuthentication());
}
}

9. Business Service Layer with ACL

Document Service:

package com.myapp.acl.service;
import com.myapp.acl.model.Document;
import com.myapp.acl.model.DocumentStatus;
import com.myapp.acl.permission.CustomPermission;
import com.myapp.acl.repository.DocumentRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class DocumentService {
private static final Logger logger = LoggerFactory.getLogger(DocumentService.class);
private final DocumentRepository documentRepository;
private final AclManagementService aclManagementService;
public DocumentService(DocumentRepository documentRepository, 
AclManagementService aclManagementService) {
this.documentRepository = documentRepository;
this.aclManagementService = aclManagementService;
}
@PreAuthorize("hasPermission(#document, 'CREATE')")
public Document createDocument(Document document) {
Document savedDocument = documentRepository.save(document);
aclManagementService.createDocumentWithAcl(savedDocument);
return savedDocument;
}
@PostAuthorize("hasPermission(returnObject, 'READ')")
public Optional<Document> getDocument(Long id) {
return documentRepository.findById(id);
}
@PostAuthorize("hasPermission(returnObject, 'READ')")
public Optional<Document> getDocumentByUuid(String uuid) {
return documentRepository.findByUuid(uuid);
}
@PostFilter("hasPermission(filterObject, 'READ')")
public List<Document> getAllDocuments() {
return documentRepository.findAll();
}
@PreAuthorize("hasPermission(#document, 'WRITE')")
public Document updateDocument(Document document) {
return documentRepository.save(document);
}
@PreAuthorize("hasPermission(#document, 'DELETE')")
public void deleteDocument(Document document) {
aclManagementService.deleteDocumentAcl(document);
documentRepository.delete(document);
}
@PreAuthorize("hasPermission(#document, 'ADMINISTRATION')")
public void shareDocument(Document document, String username) {
aclManagementService.grantDocumentPermission(document, username, 
CustomPermission.READ, CustomPermission.VIEW);
}
@PreAuthorize("hasPermission(#document, 'ADMINISTRATION')")
public void grantFullAccess(Document document, String username) {
aclManagementService.grantDocumentPermission(document, username, 
CustomPermission.READ, CustomPermission.WRITE, CustomPermission.DELETE);
}
@PreAuthorize("hasPermission(#document, 'APPROVE')")
public Document approveDocument(Document document) {
document.setStatus(DocumentStatus.APPROVED);
return documentRepository.save(document);
}
@PreAuthorize("hasPermission(#document, 'EXPORT')")
public String exportDocument(Document document) {
// Export logic here
logger.info("Exporting document: {}", document.getTitle());
return "Exported: " + document.getTitle();
}
public List<Document> getDocumentsByOwner(String owner) {
return documentRepository.findByOwner(owner);
}
@PostFilter("hasPermission(filterObject, 'READ')")
public List<Document> searchDocuments(String keyword) {
return documentRepository.findByTitleContainingOrContentContaining(keyword, keyword);
}
}

Project Service:

package com.myapp.acl.service;
import com.myapp.acl.model.Project;
import com.myapp.acl.repository.ProjectRepository;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class ProjectService {
private final ProjectRepository projectRepository;
private final AclManagementService aclManagementService;
public ProjectService(ProjectRepository projectRepository, 
AclManagementService aclManagementService) {
this.projectRepository = projectRepository;
this.aclManagementService = aclManagementService;
}
@PreAuthorize("hasPermission(#project, 'CREATE')")
public Project createProject(Project project) {
Project savedProject = projectRepository.save(project);
aclManagementService.createProjectWithAcl(savedProject);
return savedProject;
}
@PostAuthorize("hasPermission(returnObject, 'READ')")
public Optional<Project> getProject(Long id) {
return projectRepository.findById(id);
}
@PostFilter("hasPermission(filterObject, 'READ')")
public List<Project> getAllProjects() {
return projectRepository.findAll();
}
@PreAuthorize("hasPermission(#project, 'WRITE')")
public Project updateProject(Project project) {
return projectRepository.save(project);
}
@PreAuthorize("hasPermission(#project, 'DELETE')")
public void deleteProject(Project project) {
aclManagementService.deleteAcl(project);
projectRepository.delete(project);
}
@PreAuthorize("hasPermission(#project, 'ADMINISTRATION')")
public void addProjectMember(Project project, String username) {
aclManagementService.addProjectMember(project, username);
}
@PreAuthorize("hasPermission(#project, 'ADMINISTRATION')")
public void removeProjectMember(Project project, String username) {
aclManagementService.revokePermission(project, username, 
org.springframework.security.acls.domain.BasePermission.READ, 
org.springframework.security.acls.domain.BasePermission.WRITE);
}
public List<Project> getProjectsByOwner(String owner) {
return projectRepository.findByOwner(owner);
}
}

10. REST Controllers with ACL Security

Document Controller:

package com.myapp.acl.controller;
import com.myapp.acl.model.Document;
import com.myapp.acl.service.DocumentService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
private final DocumentService documentService;
public DocumentController(DocumentService documentService) {
this.documentService = documentService;
}
@PostMapping
public ResponseEntity<Document> createDocument(@RequestBody Document document) {
Document created = documentService.createDocument(document);
return ResponseEntity.ok(created);
}
@GetMapping("/{id}")
public ResponseEntity<Document> getDocument(@PathVariable Long id) {
Optional<Document> document = documentService.getDocument(id);
return document.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<Document>> getAllDocuments() {
List<Document> documents = documentService.getAllDocuments();
return ResponseEntity.ok(documents);
}
@PutMapping("/{id}")
public ResponseEntity<Document> updateDocument(@PathVariable Long id, 
@RequestBody Document document) {
document.setId(id);
Document updated = documentService.updateDocument(document);
return ResponseEntity.ok(updated);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteDocument(@PathVariable Long id) {
documentService.getDocument(id).ifPresent(documentService::deleteDocument);
return ResponseEntity.noContent().build();
}
@PostMapping("/{id}/share")
@PreAuthorize("@documentService.getDocument(#id).get().owner == authentication.name")
public ResponseEntity<Void> shareDocument(@PathVariable Long id, 
@RequestParam String username) {
documentService.getDocument(id).ifPresent(document -> 
documentService.shareDocument(document, username));
return ResponseEntity.ok().build();
}
@PostMapping("/{id}/approve")
@PreAuthorize("hasPermission(@documentService.getDocument(#id).get(), 'APPROVE')")
public ResponseEntity<Document> approveDocument(@PathVariable Long id) {
Optional<Document> document = documentService.getDocument(id);
if (document.isPresent()) {
Document approved = documentService.approveDocument(document.get());
return ResponseEntity.ok(approved);
}
return ResponseEntity.notFound().build();
}
@GetMapping("/{id}/export")
@PreAuthorize("hasPermission(@documentService.getDocument(#id).get(), 'EXPORT')")
public ResponseEntity<String> exportDocument(@PathVariable Long id) {
Optional<Document> document = documentService.getDocument(id);
if (document.isPresent()) {
String exportResult = documentService.exportDocument(document.get());
return ResponseEntity.ok(exportResult);
}
return ResponseEntity.notFound().build();
}
@GetMapping("/search")
public ResponseEntity<List<Document>> searchDocuments(@RequestParam String q) {
List<Document> documents = documentService.searchDocuments(q);
return ResponseEntity.ok(documents);
}
}

11. Repository Layer

Document Repository:

package com.myapp.acl.repository;
import com.myapp.acl.model.Document;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface DocumentRepository extends JpaRepository<Document, Long> {
Optional<Document> findByUuid(String uuid);
List<Document> findByOwner(String owner);
List<Document> findByStatus(String status);
@Query("SELECT d FROM Document d WHERE d.title LIKE %:keyword% OR d.content LIKE %:keyword%")
List<Document> findByTitleContainingOrContentContaining(@Param("keyword") String keyword);
@Query("SELECT d FROM Document d WHERE d.type = :type AND d.owner = :owner")
List<Document> findByTypeAndOwner(@Param("type") String type, @Param("owner") String owner);
}

Project Repository:

package com.myapp.acl.repository;
import com.myapp.acl.model.Project;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ProjectRepository extends JpaRepository<Project, Long> {
Optional<Project> findByUuid(String uuid);
List<Project> findByOwner(String owner);
List<Project> findByStatus(String status);
}

12. Security Configuration

Main Security Configuration:

package com.myapp.acl.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**", "/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(httpBasic -> {});
return http;
}
}

13. Benefits of Spring Security ACL

  1. Fine-Grained Security - Control access to individual domain objects
  2. Dynamic Permissions - Change permissions at runtime
  3. Inheritance - Support for permission inheritance
  4. Performance - Built-in caching for permission checks
  5. Integration - Seamless integration with Spring Security
  6. Flexibility - Support for custom permissions and SIDs

Conclusion

Spring Security ACL provides a robust framework for implementing fine-grained, instance-based security in Java applications. By leveraging ACL, you can create sophisticated authorization systems that control access to individual domain objects based on user permissions.

The key to successful ACL implementation is:

  • Proper database schema setup for ACL tables
  • Efficient caching configuration for performance
  • Consistent permission management across the application
  • Proper transactional boundaries for ACL operations
  • Comprehensive testing of permission scenarios

Start with basic READ/WRITE permissions and gradually add more complex permission structures as your application requirements evolve.


Call to Action: Begin by implementing basic ACL for a simple domain object like Document. Test the permission checks thoroughly, then gradually add more complex scenarios like permission inheritance, role-based ACL, and custom permissions. Monitor performance and optimize caching as needed.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper