0
# Permission Evaluation
1
2
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.
3
4
## Overview
5
6
Permission evaluation in Spring Security ACL works through:
7
8
- **`@PreAuthorize`** - Check permissions before method execution
9
- **`@PostAuthorize`** - Check permissions after method execution
10
- **`@PostFilter`** - Filter collections based on permissions
11
- **`@PreFilter`** - Filter input collections based on permissions
12
- **`AclPermissionEvaluator`** - Core integration point with ACL system
13
- **`AclPermissionCacheOptimizer`** - Batch loading for performance
14
15
## AclPermissionEvaluator
16
17
The `AclPermissionEvaluator` implements Spring Security's `PermissionEvaluator` interface to bridge ACL data with expression evaluation:
18
19
```java { .api }
20
package org.springframework.security.acls;
21
22
public class AclPermissionEvaluator implements PermissionEvaluator {
23
24
private final AclService aclService;
25
26
public AclPermissionEvaluator(AclService aclService) {
27
this.aclService = aclService;
28
}
29
30
// Check permission on domain object instance
31
@Override
32
public boolean hasPermission(Authentication authentication, Object domainObject, Object permission) {
33
// Implementation details...
34
}
35
36
// Check permission by object ID and type
37
@Override
38
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
39
// Implementation details...
40
}
41
42
// Configuration methods
43
public void setObjectIdentityRetrievalStrategy(ObjectIdentityRetrievalStrategy strategy);
44
public void setObjectIdentityGenerator(ObjectIdentityGenerator generator);
45
public void setSidRetrievalStrategy(SidRetrievalStrategy strategy);
46
public void setPermissionFactory(PermissionFactory factory);
47
}
48
```
49
50
### Basic Configuration
51
52
```java { .api }
53
@Configuration
54
@EnableGlobalMethodSecurity(prePostEnabled = true)
55
public class MethodSecurityConfig {
56
57
@Bean
58
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(AclService aclService) {
59
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
60
handler.setPermissionEvaluator(new AclPermissionEvaluator(aclService));
61
return handler;
62
}
63
}
64
```
65
66
### Advanced Configuration
67
68
```java { .api }
69
@Configuration
70
@EnableGlobalMethodSecurity(prePostEnabled = true)
71
public class AdvancedMethodSecurityConfig {
72
73
@Bean
74
public AclPermissionEvaluator aclPermissionEvaluator(AclService aclService) {
75
AclPermissionEvaluator evaluator = new AclPermissionEvaluator(aclService);
76
77
// Custom strategies
78
evaluator.setObjectIdentityRetrievalStrategy(objectIdentityRetrievalStrategy());
79
evaluator.setSidRetrievalStrategy(sidRetrievalStrategy());
80
evaluator.setPermissionFactory(permissionFactory());
81
82
return evaluator;
83
}
84
85
@Bean
86
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(
87
AclPermissionEvaluator permissionEvaluator,
88
AclPermissionCacheOptimizer cacheOptimizer) {
89
90
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
91
handler.setPermissionEvaluator(permissionEvaluator);
92
handler.setPermissionCacheOptimizer(cacheOptimizer);
93
return handler;
94
}
95
96
@Bean
97
public AclPermissionCacheOptimizer permissionCacheOptimizer(AclService aclService) {
98
return new AclPermissionCacheOptimizer(aclService);
99
}
100
}
101
```
102
103
## Pre-Authorization with @PreAuthorize
104
105
Use `@PreAuthorize` to check permissions before method execution:
106
107
### Basic Usage
108
109
```java { .api }
110
@Service
111
public class DocumentService {
112
113
// Check READ permission on document object
114
@PreAuthorize("hasPermission(#document, 'READ')")
115
public Document viewDocument(Document document) {
116
return document;
117
}
118
119
// Check WRITE permission using object parameter
120
@PreAuthorize("hasPermission(#document, 'WRITE')")
121
public Document updateDocument(Document document, String content) {
122
document.setContent(content);
123
return documentRepository.save(document);
124
}
125
126
// Check permission by ID and type
127
@PreAuthorize("hasPermission(#documentId, 'com.example.Document', 'DELETE')")
128
public void deleteDocument(Long documentId) {
129
documentRepository.deleteById(documentId);
130
}
131
132
// Multiple permission checks
133
@PreAuthorize("hasPermission(#document, 'READ') and hasPermission(#document, 'WRITE')")
134
public Document editDocument(Document document) {
135
// User needs both READ and WRITE permissions
136
return document;
137
}
138
}
139
```
140
141
### Advanced Pre-Authorization
142
143
```java { .api }
144
@Service
145
public class FolderService {
146
147
// Check permission with custom logic
148
@PreAuthorize("hasPermission(#folder, 'ADMINISTRATION') or " +
149
"(hasPermission(#folder, 'WRITE') and #folder.owner == authentication.name)")
150
public void changeFolderSettings(Folder folder, Map<String, Object> settings) {
151
// Only folder admins or owners can change settings
152
}
153
154
// Permission with role fallback
155
@PreAuthorize("hasRole('ADMIN') or hasPermission(#folder, 'CREATE')")
156
public Document createDocumentInFolder(Folder folder, Document document) {
157
document.setFolder(folder);
158
return documentRepository.save(document);
159
}
160
161
// Complex permission logic
162
@PreAuthorize("@folderSecurityService.canAccess(#folderId, authentication)")
163
public List<Document> getFolderContents(Long folderId) {
164
return documentRepository.findByFolderId(folderId);
165
}
166
}
167
168
@Service
169
public class FolderSecurityService {
170
171
@Autowired
172
private AclPermissionEvaluator permissionEvaluator;
173
174
public boolean canAccess(Long folderId, Authentication authentication) {
175
return permissionEvaluator.hasPermission(
176
authentication, folderId, "com.example.Folder", "READ"
177
) || hasInheritedAccess(folderId, authentication);
178
}
179
}
180
```
181
182
## Post-Authorization with @PostAuthorize
183
184
Use `@PostAuthorize` to check permissions on method return values:
185
186
```java { .api }
187
@Service
188
public class DocumentService {
189
190
// Check permission on returned object
191
@PostAuthorize("hasPermission(returnObject, 'READ')")
192
public Document findDocument(Long id) {
193
return documentRepository.findById(id).orElse(null);
194
}
195
196
// Multiple checks on return value
197
@PostAuthorize("returnObject != null and hasPermission(returnObject, 'READ')")
198
public Document getDocumentByTitle(String title) {
199
return documentRepository.findByTitle(title);
200
}
201
202
// Check permissions on nested objects
203
@PostAuthorize("returnObject.folder == null or hasPermission(returnObject.folder, 'READ')")
204
public Document getDocumentWithFolder(Long id) {
205
return documentRepository.findByIdWithFolder(id);
206
}
207
208
// Custom permission validation
209
@PostAuthorize("@documentSecurityService.canView(returnObject, authentication)")
210
public Document getDocument(Long id) {
211
return documentRepository.findById(id).orElse(null);
212
}
213
}
214
```
215
216
## Collection Filtering with @PostFilter
217
218
Filter returned collections based on permissions:
219
220
```java { .api }
221
@Service
222
public class DocumentService {
223
224
// Filter list - only return documents user can read
225
@PostFilter("hasPermission(filterObject, 'READ')")
226
public List<Document> getAllDocuments() {
227
return documentRepository.findAll();
228
}
229
230
// Filter with additional criteria
231
@PostFilter("hasPermission(filterObject, 'READ') and filterObject.published == true")
232
public List<Document> getPublishedDocuments() {
233
return documentRepository.findAll();
234
}
235
236
// Filter complex objects
237
@PostFilter("hasPermission(filterObject.document, 'READ')")
238
public List<DocumentWithMetadata> getDocumentsWithMetadata() {
239
return documentRepository.findAllWithMetadata();
240
}
241
242
// Filter based on different permission levels
243
@PostFilter("hasPermission(filterObject, 'READ') or " +
244
"hasPermission(filterObject, 'WRITE') or " +
245
"hasPermission(filterObject, 'ADMINISTRATION')")
246
public List<Document> getAccessibleDocuments() {
247
return documentRepository.findAll();
248
}
249
}
250
```
251
252
## Input Filtering with @PreFilter
253
254
Filter input collections before method execution:
255
256
```java { .api }
257
@Service
258
public class DocumentService {
259
260
// Filter input list - only process documents user can write to
261
@PreFilter("hasPermission(filterObject, 'WRITE')")
262
public List<Document> updateMultipleDocuments(List<Document> documents) {
263
// Only documents with WRITE permission will be processed
264
return documents.stream()
265
.map(doc -> {
266
doc.setLastModified(new Date());
267
return documentRepository.save(doc);
268
})
269
.collect(Collectors.toList());
270
}
271
272
// Filter with permission and business logic
273
@PreFilter("hasPermission(filterObject, 'DELETE') and filterObject.status != 'PUBLISHED'")
274
public void deleteMultipleDocuments(List<Document> documents) {
275
documents.forEach(doc -> documentRepository.delete(doc));
276
}
277
278
// Filter by ID list
279
@PreFilter("hasPermission(filterObject, 'com.example.Document', 'READ')")
280
public List<Document> getDocumentsByIds(List<Long> documentIds) {
281
return documentRepository.findAllById(documentIds);
282
}
283
}
284
```
285
286
## Permission Types
287
288
The ACL system supports multiple ways to specify permissions:
289
290
### String Permissions
291
292
```java { .api }
293
@PreAuthorize("hasPermission(#document, 'READ')")
294
@PreAuthorize("hasPermission(#document, 'WRITE')")
295
@PreAuthorize("hasPermission(#document, 'DELETE')")
296
@PreAuthorize("hasPermission(#document, 'ADMINISTRATION')")
297
298
// Custom permissions (must be registered in PermissionFactory)
299
@PreAuthorize("hasPermission(#document, 'APPROVE')")
300
@PreAuthorize("hasPermission(#document, 'PUBLISH')")
301
```
302
303
### Integer Mask Permissions
304
305
```java { .api }
306
// Using BasePermission mask values
307
@PreAuthorize("hasPermission(#document, 1)") // READ
308
@PreAuthorize("hasPermission(#document, 2)") // WRITE
309
@PreAuthorize("hasPermission(#document, 4)") // CREATE
310
@PreAuthorize("hasPermission(#document, 8)") // DELETE
311
@PreAuthorize("hasPermission(#document, 16)") // ADMINISTRATION
312
313
// Combined permissions using bitwise OR
314
@PreAuthorize("hasPermission(#document, 3)") // READ + WRITE (1 | 2)
315
```
316
317
### Permission Objects
318
319
```java { .api }
320
@Service
321
public class DocumentService {
322
323
// Inject permission constants
324
@PreAuthorize("hasPermission(#document, T(org.springframework.security.acls.domain.BasePermission).READ)")
325
public Document viewDocument(Document document) {
326
return document;
327
}
328
329
// Custom permission objects
330
@PreAuthorize("hasPermission(#document, T(com.example.CustomPermission).APPROVE)")
331
public void approveDocument(Document document) {
332
document.setApproved(true);
333
documentRepository.save(document);
334
}
335
}
336
```
337
338
## Performance Optimization
339
340
### Batch Loading with AclPermissionCacheOptimizer
341
342
The cache optimizer pre-loads ACL data for collections to avoid N+1 queries:
343
344
```java { .api }
345
@Configuration
346
public class PerformanceConfig {
347
348
@Bean
349
public AclPermissionCacheOptimizer aclPermissionCacheOptimizer(AclService aclService) {
350
AclPermissionCacheOptimizer optimizer = new AclPermissionCacheOptimizer(aclService);
351
352
// Configure custom strategies if needed
353
optimizer.setObjectIdentityRetrievalStrategy(objectIdentityRetrievalStrategy());
354
optimizer.setSidRetrievalStrategy(sidRetrievalStrategy());
355
356
return optimizer;
357
}
358
}
359
```
360
361
The optimizer automatically detects collection parameters and return values:
362
363
```java { .api }
364
@Service
365
public class OptimizedDocumentService {
366
367
// Optimizer will batch-load ACLs for all documents before filtering
368
@PostFilter("hasPermission(filterObject, 'READ')")
369
public List<Document> getAllDocuments() {
370
return documentRepository.findAll(); // Single query + batch ACL load
371
}
372
373
// Optimizer detects collection parameter
374
@PreFilter("hasPermission(filterObject, 'WRITE')")
375
public void updateDocuments(List<Document> documents) {
376
// ACLs loaded in batch before filtering
377
documents.forEach(documentRepository::save);
378
}
379
}
380
```
381
382
### Custom Permission Caching
383
384
Implement custom caching for complex permission logic:
385
386
```java { .api }
387
@Service
388
@CacheConfig(cacheNames = "permissions")
389
public class CachedPermissionService {
390
391
@Cacheable(key = "#objectId + ':' + #targetType + ':' + #permission + ':' + #authentication.name")
392
public boolean hasPermission(Long objectId, String targetType, String permission, Authentication authentication) {
393
return aclPermissionEvaluator.hasPermission(authentication, objectId, targetType, permission);
394
}
395
396
@CacheEvict(key = "#objectId + ':*'", allEntries = true)
397
public void clearPermissionCache(Long objectId) {
398
// Called when object permissions change
399
}
400
}
401
```
402
403
## Error Handling
404
405
Handle permission evaluation errors gracefully:
406
407
```java { .api }
408
@Service
409
public class RobustDocumentService {
410
411
@PreAuthorize("@permissionChecker.canRead(#documentId, authentication)")
412
public Document getDocument(Long documentId) {
413
return documentRepository.findById(documentId).orElse(null);
414
}
415
}
416
417
@Component
418
public class PermissionChecker {
419
420
@Autowired
421
private AclPermissionEvaluator permissionEvaluator;
422
423
public boolean canRead(Long documentId, Authentication authentication) {
424
try {
425
return permissionEvaluator.hasPermission(
426
authentication, documentId, "com.example.Document", "READ"
427
);
428
} catch (Exception e) {
429
log.warn("Permission check failed for document {}: {}", documentId, e.getMessage());
430
return false; // Fail securely
431
}
432
}
433
}
434
```
435
436
### Global Exception Handling
437
438
```java { .api }
439
@ControllerAdvice
440
public class SecurityExceptionHandler {
441
442
@ExceptionHandler(AccessDeniedException.class)
443
public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException e) {
444
return ResponseEntity.status(HttpStatus.FORBIDDEN)
445
.body(new ErrorResponse("Access denied: " + e.getMessage()));
446
}
447
448
@ExceptionHandler(NotFoundException.class)
449
public ResponseEntity<ErrorResponse> handleAclNotFound(NotFoundException e) {
450
// ACL not found - might be legitimate (new object) or security issue
451
log.debug("ACL not found: {}", e.getMessage());
452
return ResponseEntity.status(HttpStatus.NOT_FOUND)
453
.body(new ErrorResponse("Resource not found"));
454
}
455
}
456
```
457
458
## Testing Permission Logic
459
460
### Unit Testing
461
462
```java { .api }
463
@ExtendWith(MockitoExtension.class)
464
class DocumentServiceTest {
465
466
@Mock
467
private AclPermissionEvaluator permissionEvaluator;
468
469
@Mock
470
private DocumentRepository documentRepository;
471
472
@InjectMocks
473
private DocumentService documentService;
474
475
@Test
476
void testPreAuthorizeWithPermission() {
477
// Given
478
Document document = new Document(1L, "Test");
479
Authentication auth = createAuthentication("user1");
480
481
when(permissionEvaluator.hasPermission(auth, document, "READ"))
482
.thenReturn(true);
483
484
// When/Then - method should execute without exception
485
assertThat(documentService.viewDocument(document)).isEqualTo(document);
486
}
487
488
@Test
489
void testPreAuthorizeWithoutPermission() {
490
// Given
491
Document document = new Document(1L, "Test");
492
Authentication auth = createAuthentication("user1");
493
494
when(permissionEvaluator.hasPermission(auth, document, "READ"))
495
.thenReturn(false);
496
497
// When/Then - should throw AccessDeniedException
498
assertThatThrownBy(() -> documentService.viewDocument(document))
499
.isInstanceOf(AccessDeniedException.class);
500
}
501
}
502
```
503
504
### Integration Testing
505
506
```java { .api }
507
@SpringBootTest
508
@Transactional
509
class DocumentServiceIntegrationTest {
510
511
@Autowired
512
private DocumentService documentService;
513
514
@Autowired
515
private MutableAclService aclService;
516
517
@Test
518
@WithMockUser(username = "user1", roles = "USER")
519
void testDocumentAccessWithRealAcl() {
520
// Given - create document with ACL
521
Document document = documentRepository.save(new Document("Test"));
522
523
ObjectIdentity identity = new ObjectIdentityImpl(Document.class, document.getId());
524
MutableAcl acl = aclService.createAcl(identity);
525
526
Sid userSid = new PrincipalSid("user1");
527
acl.insertAce(0, BasePermission.READ, userSid, true);
528
aclService.updateAcl(acl);
529
530
// When/Then - user should be able to access document
531
Document result = documentService.viewDocument(document);
532
assertThat(result).isEqualTo(document);
533
}
534
}
535
```
536
537
### Testing with Security Context
538
539
```java { .api }
540
@TestConfiguration
541
static class TestSecurityConfig {
542
543
@Bean
544
@Primary
545
public AclPermissionEvaluator testPermissionEvaluator() {
546
return new AclPermissionEvaluator(aclService()) {
547
@Override
548
public boolean hasPermission(Authentication auth, Object domainObject, Object permission) {
549
// Mock implementation for testing
550
return "user1".equals(auth.getName()) && "READ".equals(permission);
551
}
552
};
553
}
554
}
555
```
556
557
## Best Practices
558
559
### 1. Use Method-Level Security Consistently
560
561
```java { .api }
562
// Good - consistent permission checking
563
@Service
564
public class DocumentService {
565
566
@PreAuthorize("hasPermission(#document, 'READ')")
567
public Document getDocument(Document document) { /* */ }
568
569
@PreAuthorize("hasPermission(#document, 'WRITE')")
570
public Document updateDocument(Document document) { /* */ }
571
572
@PreAuthorize("hasPermission(#documentId, 'com.example.Document', 'DELETE')")
573
public void deleteDocument(Long documentId) { /* */ }
574
}
575
576
// Avoid - mixing security approaches
577
@Service
578
public class MixedSecurityService {
579
580
@PreAuthorize("hasPermission(#document, 'READ')")
581
public Document getDocument(Document document) { /* */ }
582
583
// Inconsistent - manual permission checking
584
public Document updateDocument(Document document) {
585
if (!hasWritePermission(document)) {
586
throw new AccessDeniedException("Access denied");
587
}
588
// Update logic...
589
}
590
}
591
```
592
593
### 2. Handle Null Objects Gracefully
594
595
```java { .api }
596
@Service
597
public class SafeDocumentService {
598
599
// Good - null check built into expression
600
@PostAuthorize("returnObject == null or hasPermission(returnObject, 'READ')")
601
public Document findDocument(Long id) {
602
return documentRepository.findById(id).orElse(null);
603
}
604
605
// Alternative - separate null handling
606
public Document getDocument(Long id) {
607
Document doc = documentRepository.findById(id).orElse(null);
608
if (doc != null && !hasReadPermission(doc)) {
609
throw new AccessDeniedException("Access denied");
610
}
611
return doc;
612
}
613
}
614
```
615
616
### 3. Optimize Collection Operations
617
618
```java { .api }
619
// Good - use @PostFilter for automatic optimization
620
@PostFilter("hasPermission(filterObject, 'READ')")
621
public List<Document> getUserDocuments() {
622
return documentRepository.findAll();
623
}
624
625
// Better - pre-filter at database level when possible
626
public List<Document> getUserDocuments(Authentication authentication) {
627
List<Sid> sids = sidRetrievalStrategy.getSids(authentication);
628
return documentRepository.findDocumentsAccessibleToSids(sids);
629
}
630
```
631
632
### 4. Use Meaningful Permission Names
633
634
```java { .api }
635
// Good - clear business intent
636
@PreAuthorize("hasPermission(#document, 'APPROVE')")
637
@PreAuthorize("hasPermission(#document, 'PUBLISH')")
638
@PreAuthorize("hasPermission(#document, 'ARCHIVE')")
639
640
// Avoid - technical permission names without business context
641
@PreAuthorize("hasPermission(#document, 'PERM_001')")
642
@PreAuthorize("hasPermission(#document, 'FLAG_A')")
643
```
644
645
The permission evaluation system provides a powerful, declarative way to implement fine-grained security. Combined with proper [configuration and setup](configuration.md), it enables maintainable, high-performance access control for complex applications.