0
# ACL Services
1
2
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.
3
4
## Overview
5
6
The ACL module provides two main service interfaces:
7
8
- **`AclService`** - Read-only access to ACL data
9
- **`MutableAclService`** - Full CRUD operations on ACL data
10
11
Both are backed by efficient JDBC implementations that work with any SQL database.
12
13
## AclService Interface
14
15
The `AclService` provides read-only access to ACL data with methods optimized for different use cases:
16
17
```java { .api }
18
package org.springframework.security.acls.model;
19
20
public interface AclService {
21
22
// Find child object identities
23
List<ObjectIdentity> findChildren(ObjectIdentity parentIdentity);
24
25
// Read single ACL (not recommended - doesn't allow SID filtering)
26
Acl readAclById(ObjectIdentity object) throws NotFoundException;
27
28
// Read single ACL with SID filtering (recommended)
29
Acl readAclById(ObjectIdentity object, List<Sid> sids) throws NotFoundException;
30
31
// Batch read ACLs
32
Map<ObjectIdentity, Acl> readAclsById(List<ObjectIdentity> objects) throws NotFoundException;
33
34
// Batch read ACLs with SID filtering (most efficient)
35
Map<ObjectIdentity, Acl> readAclsById(List<ObjectIdentity> objects, List<Sid> sids)
36
throws NotFoundException;
37
}
38
```
39
40
### Key Methods
41
42
**Single ACL Retrieval:**
43
```java { .api }
44
@Autowired
45
private AclService aclService;
46
47
public boolean checkDocumentAccess(Long documentId, Authentication auth) {
48
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, documentId);
49
List<Sid> sids = sidRetrievalStrategy.getSids(auth);
50
51
try {
52
// Load ACL with SID filtering for better performance
53
Acl acl = aclService.readAclById(identity, sids);
54
return acl.isGranted(Arrays.asList(BasePermission.READ), sids, false);
55
} catch (NotFoundException e) {
56
return false; // No ACL exists
57
}
58
}
59
```
60
61
**Batch ACL Retrieval:**
62
```java { .api }
63
public Map<Document, Boolean> checkMultipleDocuments(List<Document> documents, Authentication auth) {
64
// Create object identities
65
List<ObjectIdentity> identities = documents.stream()
66
.map(doc -> new ObjectIdentityImpl(Document.class, doc.getId()))
67
.collect(Collectors.toList());
68
69
List<Sid> sids = sidRetrievalStrategy.getSids(auth);
70
List<Permission> readPermission = Arrays.asList(BasePermission.READ);
71
72
try {
73
// Batch load ACLs - much more efficient than individual calls
74
Map<ObjectIdentity, Acl> acls = aclService.readAclsById(identities, sids);
75
76
return documents.stream()
77
.collect(Collectors.toMap(
78
doc -> doc,
79
doc -> {
80
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, doc.getId());
81
Acl acl = acls.get(identity);
82
return acl != null && acl.isGranted(readPermission, sids, false);
83
}
84
));
85
} catch (NotFoundException e) {
86
// Return all false if any ACL is missing
87
return documents.stream()
88
.collect(Collectors.toMap(doc -> doc, doc -> false));
89
}
90
}
91
```
92
93
**Finding Child Objects:**
94
```java { .api }
95
public List<ObjectIdentity> getFolderContents(Long folderId) {
96
ObjectIdentity folderIdentity = new ObjectIdentityImpl(Folder.class, folderId);
97
return aclService.findChildren(folderIdentity);
98
}
99
```
100
101
## MutableAclService Interface
102
103
The `MutableAclService` extends `AclService` with methods for creating, modifying, and deleting ACLs:
104
105
```java { .api }
106
package org.springframework.security.acls.model;
107
108
public interface MutableAclService extends AclService {
109
110
// Create new ACL
111
MutableAcl createAcl(ObjectIdentity objectIdentity) throws AlreadyExistsException;
112
113
// Delete ACL
114
void deleteAcl(ObjectIdentity objectIdentity, boolean deleteChildren)
115
throws ChildrenExistException;
116
117
// Update existing ACL
118
MutableAcl updateAcl(MutableAcl acl) throws NotFoundException;
119
}
120
```
121
122
### Creating ACLs
123
124
```java { .api }
125
@Autowired
126
private MutableAclService mutableAclService;
127
128
public void createDocumentWithPermissions(Document document, String owner) {
129
// Save document first
130
document = documentRepository.save(document);
131
132
// Create object identity
133
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, document.getId());
134
135
try {
136
// Create ACL
137
MutableAcl acl = mutableAclService.createAcl(identity);
138
139
// Set owner
140
Sid ownerSid = new PrincipalSid(owner);
141
acl.setOwner(ownerSid);
142
143
// Grant owner full permissions
144
acl.insertAce(0, BasePermission.ADMINISTRATION, ownerSid, true);
145
acl.insertAce(1, BasePermission.DELETE, ownerSid, true);
146
acl.insertAce(2, BasePermission.WRITE, ownerSid, true);
147
acl.insertAce(3, BasePermission.READ, ownerSid, true);
148
149
// Grant read permission to all authenticated users
150
Sid userRole = new GrantedAuthoritySid("ROLE_USER");
151
acl.insertAce(4, BasePermission.READ, userRole, true);
152
153
// Save ACL
154
mutableAclService.updateAcl(acl);
155
156
} catch (AlreadyExistsException e) {
157
throw new IllegalStateException("ACL already exists for document: " + document.getId());
158
}
159
}
160
```
161
162
### Updating ACL Permissions
163
164
```java { .api }
165
public void grantUserPermission(Long documentId, String username, Permission permission) {
166
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, documentId);
167
168
// Load existing ACL as mutable
169
MutableAcl acl = (MutableAcl) mutableAclService.readAclById(identity);
170
171
// Add new permission
172
Sid userSid = new PrincipalSid(username);
173
acl.insertAce(acl.getEntries().size(), permission, userSid, true);
174
175
// Update ACL
176
mutableAclService.updateAcl(acl);
177
}
178
179
public void revokeUserPermission(Long documentId, String username, Permission permission) {
180
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, documentId);
181
MutableAcl acl = (MutableAcl) mutableAclService.readAclById(identity);
182
183
Sid userSid = new PrincipalSid(username);
184
185
// Find and remove matching ACE
186
List<AccessControlEntry> entries = acl.getEntries();
187
for (int i = 0; i < entries.size(); i++) {
188
AccessControlEntry ace = entries.get(i);
189
if (ace.getSid().equals(userSid) && ace.getPermission().equals(permission)) {
190
acl.deleteAce(i);
191
break;
192
}
193
}
194
195
mutableAclService.updateAcl(acl);
196
}
197
```
198
199
### Deleting ACLs
200
201
```java { .api }
202
public void deleteDocument(Long documentId, boolean deleteChildACLs) {
203
// Delete domain object first
204
documentRepository.deleteById(documentId);
205
206
// Delete ACL
207
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, documentId);
208
209
try {
210
mutableAclService.deleteAcl(identity, deleteChildACLs);
211
} catch (ChildrenExistException e) {
212
throw new IllegalStateException(
213
"Cannot delete ACL - child ACLs exist. Use deleteChildACLs=true or delete children first.");
214
}
215
}
216
```
217
218
## JDBC Implementations
219
220
### JdbcAclService
221
222
The production-ready read-only implementation:
223
224
```java { .api }
225
@Configuration
226
public class AclServiceConfig {
227
228
@Bean
229
public AclService aclService(DataSource dataSource) {
230
JdbcAclService service = new JdbcAclService(dataSource, lookupStrategy());
231
return service;
232
}
233
234
@Bean
235
public LookupStrategy lookupStrategy() {
236
return new BasicLookupStrategy(
237
dataSource(),
238
aclCache(),
239
aclAuthorizationStrategy(),
240
auditLogger()
241
);
242
}
243
}
244
```
245
246
### JdbcMutableAclService
247
248
The full-featured implementation with create/update/delete capabilities:
249
250
```java { .api }
251
@Configuration
252
public class MutableAclServiceConfig {
253
254
@Bean
255
public MutableAclService mutableAclService(DataSource dataSource) {
256
JdbcMutableAclService service = new JdbcMutableAclService(
257
dataSource,
258
lookupStrategy(),
259
aclCache()
260
);
261
262
// Configure for your database
263
service.setClassIdentityQuery("SELECT @@IDENTITY"); // SQL Server
264
service.setSidIdentityQuery("SELECT @@IDENTITY");
265
266
// For MySQL:
267
// service.setClassIdentityQuery("SELECT LAST_INSERT_ID()");
268
// service.setSidIdentityQuery("SELECT LAST_INSERT_ID()");
269
270
// For PostgreSQL:
271
// service.setClassIdentityQuery("select currval(pg_get_serial_sequence('acl_class', 'id'))");
272
// service.setSidIdentityQuery("select currval(pg_get_serial_sequence('acl_sid', 'id'))");
273
274
return service;
275
}
276
}
277
```
278
279
### Database Configuration
280
281
The JDBC implementations require specific database tables:
282
283
```sql { .api }
284
-- ACL Class table
285
CREATE TABLE acl_class (
286
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
287
class VARCHAR(100) NOT NULL,
288
UNIQUE KEY unique_uk_2 (class)
289
);
290
291
-- ACL SID table
292
CREATE TABLE acl_sid (
293
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
294
principal BOOLEAN NOT NULL,
295
sid VARCHAR(100) NOT NULL,
296
UNIQUE KEY unique_uk_3 (sid, principal)
297
);
298
299
-- ACL Object Identity table
300
CREATE TABLE acl_object_identity (
301
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
302
object_id_class BIGINT UNSIGNED NOT NULL,
303
object_id_identity VARCHAR(36) NOT NULL,
304
parent_object BIGINT UNSIGNED,
305
owner_sid BIGINT UNSIGNED,
306
entries_inheriting BOOLEAN NOT NULL,
307
UNIQUE KEY unique_uk_4 (object_id_class, object_id_identity),
308
CONSTRAINT foreign_fk_1 FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id),
309
CONSTRAINT foreign_fk_2 FOREIGN KEY (object_id_class) REFERENCES acl_class (id),
310
CONSTRAINT foreign_fk_3 FOREIGN KEY (owner_sid) REFERENCES acl_sid (id)
311
);
312
313
-- ACL Entry table
314
CREATE TABLE acl_entry (
315
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
316
acl_object_identity BIGINT UNSIGNED NOT NULL,
317
ace_order INTEGER NOT NULL,
318
sid BIGINT UNSIGNED NOT NULL,
319
mask INTEGER UNSIGNED NOT NULL,
320
granting BOOLEAN NOT NULL,
321
audit_success BOOLEAN NOT NULL,
322
audit_failure BOOLEAN NOT NULL,
323
UNIQUE KEY unique_uk_5 (acl_object_identity, ace_order),
324
CONSTRAINT foreign_fk_4 FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id),
325
CONSTRAINT foreign_fk_5 FOREIGN KEY (sid) REFERENCES acl_sid (id)
326
);
327
```
328
329
## Advanced Features
330
331
### Lookup Strategy
332
333
The `LookupStrategy` interface allows customization of how ACLs are retrieved:
334
335
```java { .api }
336
public interface LookupStrategy {
337
Map<ObjectIdentity, Acl> readAclsById(List<ObjectIdentity> objects, List<Sid> sids);
338
}
339
340
// Basic implementation for ANSI SQL databases
341
@Bean
342
public LookupStrategy lookupStrategy() {
343
BasicLookupStrategy strategy = new BasicLookupStrategy(
344
dataSource(),
345
aclCache(),
346
aclAuthorizationStrategy(),
347
permissionGrantingStrategy()
348
);
349
350
// Customize SQL queries if needed
351
strategy.setSelectClause("SELECT obj.object_id_identity, class.class, sid.sid, sid.principal, " +
352
"acl.entries_inheriting, acl.id as acl_id, acl.parent_object, acl.owner_sid, " +
353
"entry.id, entry.mask, entry.granting, entry.audit_success, entry.audit_failure ");
354
355
return strategy;
356
}
357
```
358
359
### Caching
360
361
ACL data is cached to improve performance:
362
363
```java { .api }
364
@Bean
365
public AclCache aclCache() {
366
// Use Spring's caching abstraction
367
return new SpringCacheBasedAclCache(
368
cacheManager().getCache("aclCache"),
369
permissionGrantingStrategy(),
370
aclAuthorizationStrategy()
371
);
372
}
373
374
@Bean
375
public CacheManager cacheManager() {
376
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager("aclCache");
377
return cacheManager;
378
}
379
```
380
381
### Batch Processing
382
383
For better performance when working with many objects:
384
385
```java { .api }
386
@Service
387
public class DocumentSecurityService {
388
389
@Autowired
390
private MutableAclService aclService;
391
392
@Transactional
393
public void createDocumentsWithBulkPermissions(List<Document> documents, String owner) {
394
// Save all documents first
395
documents = documentRepository.saveAll(documents);
396
397
// Create ACLs in batch
398
for (Document doc : documents) {
399
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, doc.getId());
400
401
MutableAcl acl = aclService.createAcl(identity);
402
setupDefaultPermissions(acl, owner);
403
aclService.updateAcl(acl);
404
}
405
}
406
407
private void setupDefaultPermissions(MutableAcl acl, String owner) {
408
Sid ownerSid = new PrincipalSid(owner);
409
acl.setOwner(ownerSid);
410
acl.insertAce(0, BasePermission.ADMINISTRATION, ownerSid, true);
411
412
Sid userRole = new GrantedAuthoritySid("ROLE_USER");
413
acl.insertAce(1, BasePermission.READ, userRole, true);
414
}
415
}
416
```
417
418
## Performance Optimization
419
420
### 1. Use SID Filtering
421
422
Always provide SID lists when possible to reduce data transfer:
423
424
```java { .api }
425
// Less efficient - loads all ACL entries
426
Acl acl = aclService.readAclById(objectIdentity);
427
428
// More efficient - only loads relevant entries
429
List<Sid> sids = sidRetrievalStrategy.getSids(authentication);
430
Acl acl = aclService.readAclById(objectIdentity, sids);
431
```
432
433
### 2. Batch Operations
434
435
Load multiple ACLs in single call:
436
437
```java { .api }
438
// Less efficient - multiple database queries
439
List<Boolean> results = new ArrayList<>();
440
for (ObjectIdentity identity : identities) {
441
Acl acl = aclService.readAclById(identity, sids);
442
results.add(acl.isGranted(permissions, sids, false));
443
}
444
445
// More efficient - single batch query
446
Map<ObjectIdentity, Acl> acls = aclService.readAclsById(identities, sids);
447
List<Boolean> results = identities.stream()
448
.map(identity -> {
449
Acl acl = acls.get(identity);
450
return acl != null && acl.isGranted(permissions, sids, false);
451
})
452
.collect(Collectors.toList());
453
```
454
455
### 3. Cache Configuration
456
457
```java { .api }
458
@Configuration
459
@EnableCaching
460
public class CacheConfig {
461
462
@Bean
463
public CacheManager cacheManager() {
464
CaffeineCacheManager cacheManager = new CaffeineCacheManager("aclCache");
465
cacheManager.setCaffeine(Caffeine.newBuilder()
466
.maximumSize(10000)
467
.expireAfterWrite(Duration.ofMinutes(10))
468
.recordStats());
469
return cacheManager;
470
}
471
}
472
```
473
474
### 4. Connection Pooling
475
476
```java { .api }
477
@Configuration
478
public class DataSourceConfig {
479
480
@Bean
481
@Primary
482
public DataSource dataSource() {
483
HikariConfig config = new HikariConfig();
484
config.setJdbcUrl("jdbc:mysql://localhost/acl_db");
485
config.setUsername("user");
486
config.setPassword("password");
487
488
// Optimize for ACL queries
489
config.setMaximumPoolSize(20);
490
config.setMinimumIdle(5);
491
config.setConnectionTimeout(30000);
492
config.setIdleTimeout(600000);
493
494
return new HikariDataSource(config);
495
}
496
}
497
```
498
499
## Exception Handling
500
501
Handle ACL-specific exceptions appropriately:
502
503
```java { .api }
504
@Service
505
public class SecureDocumentService {
506
507
public Optional<Document> getDocumentIfAllowed(Long documentId, Authentication auth) {
508
try {
509
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, documentId);
510
List<Sid> sids = sidRetrievalStrategy.getSids(auth);
511
512
Acl acl = aclService.readAclById(identity, sids);
513
514
if (acl.isGranted(Arrays.asList(BasePermission.READ), sids, false)) {
515
return documentRepository.findById(documentId);
516
}
517
return Optional.empty();
518
519
} catch (NotFoundException e) {
520
log.debug("No ACL found for document {}, denying access", documentId);
521
return Optional.empty();
522
523
} catch (UnloadedSidException e) {
524
log.warn("ACL loaded without required SIDs for document {}", documentId);
525
// Retry with full SID loading or return empty
526
return Optional.empty();
527
}
528
}
529
}
530
```
531
532
## Best Practices
533
534
### 1. Use Appropriate Service Level
535
```java { .api }
536
// For read-only operations, inject AclService
537
@Autowired
538
private AclService aclService;
539
540
// For modifications, inject MutableAclService
541
@Autowired
542
private MutableAclService mutableAclService;
543
```
544
545
### 2. Handle ACL Lifecycle with Domain Objects
546
```java { .api }
547
@Entity
548
@EntityListeners(DocumentAclListener.class)
549
public class Document {
550
// Entity fields...
551
}
552
553
@Component
554
public class DocumentAclListener {
555
556
@Autowired
557
private MutableAclService aclService;
558
559
@PostPersist
560
public void createAcl(Document document) {
561
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, document.getId());
562
// Create ACL with default permissions
563
}
564
565
@PostRemove
566
public void deleteAcl(Document document) {
567
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, document.getId());
568
aclService.deleteAcl(identity, true);
569
}
570
}
571
```
572
573
### 3. Transactional Boundaries
574
```java { .api }
575
@Transactional
576
public void createDocumentWithPermissions(Document document) {
577
// Both domain object and ACL operations should be in same transaction
578
document = documentRepository.save(document);
579
580
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, document.getId());
581
MutableAcl acl = mutableAclService.createAcl(identity);
582
// Configure ACL...
583
mutableAclService.updateAcl(acl);
584
}
585
```
586
587
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](permission-evaluation.md) for a modern annotation-based approach.