0
# Strategy Interfaces
1
2
Spring Security ACL provides several strategy interfaces that allow customization of core behaviors. These interfaces enable you to adapt the ACL system to your specific domain model, security requirements, and infrastructure.
3
4
## Overview
5
6
The ACL module uses the Strategy pattern for key extensibility points:
7
8
- **`ObjectIdentityRetrievalStrategy`** - Extract ObjectIdentity from domain objects
9
- **`ObjectIdentityGenerator`** - Create ObjectIdentity from ID and type
10
- **`SidRetrievalStrategy`** - Extract security identities from Authentication
11
- **`PermissionGrantingStrategy`** - Control permission evaluation logic
12
- **`AclAuthorizationStrategy`** - Control ACL modification permissions
13
- **`PermissionFactory`** - Create Permission instances from masks/names
14
- **`AuditLogger`** - Log permission grant/deny events
15
16
## ObjectIdentityRetrievalStrategy
17
18
Extracts `ObjectIdentity` from domain objects:
19
20
```java { .api }
21
package org.springframework.security.acls.model;
22
23
public interface ObjectIdentityRetrievalStrategy {
24
25
/**
26
* Obtain the ObjectIdentity from a domain object
27
* @param domainObject the domain object
28
* @return ObjectIdentity representing the domain object
29
*/
30
ObjectIdentity getObjectIdentity(Object domainObject);
31
}
32
```
33
34
### Default Implementation
35
36
```java { .api }
37
package org.springframework.security.acls.domain;
38
39
public class ObjectIdentityRetrievalStrategyImpl implements ObjectIdentityRetrievalStrategy {
40
41
@Override
42
public ObjectIdentity getObjectIdentity(Object domainObject) {
43
return new ObjectIdentityImpl(domainObject);
44
}
45
}
46
```
47
48
### Custom Implementation Example
49
50
```java { .api }
51
@Component
52
public class CustomObjectIdentityRetrievalStrategy implements ObjectIdentityRetrievalStrategy {
53
54
@Override
55
public ObjectIdentity getObjectIdentity(Object domainObject) {
56
if (domainObject instanceof Document) {
57
Document doc = (Document) domainObject;
58
// Use custom identifier format
59
return new ObjectIdentityImpl(Document.class, doc.getUuid());
60
}
61
62
if (domainObject instanceof Folder) {
63
Folder folder = (Folder) domainObject;
64
// Use hierarchical identifier
65
return new ObjectIdentityImpl("com.example.Folder",
66
folder.getPath() + "/" + folder.getId());
67
}
68
69
// Fallback to default behavior
70
return new ObjectIdentityImpl(domainObject);
71
}
72
}
73
```
74
75
## ObjectIdentityGenerator
76
77
Creates `ObjectIdentity` from identifier and type:
78
79
```java { .api }
80
package org.springframework.security.acls.model;
81
82
public interface ObjectIdentityGenerator {
83
84
/**
85
* Create ObjectIdentity from identifier and type
86
* @param id the object identifier
87
* @param type the object type
88
* @return ObjectIdentity for the specified object
89
*/
90
ObjectIdentity createObjectIdentity(Serializable id, String type);
91
}
92
```
93
94
### Implementation Example
95
96
```java { .api }
97
@Component
98
public class UuidObjectIdentityGenerator implements ObjectIdentityGenerator {
99
100
@Override
101
public ObjectIdentity createObjectIdentity(Serializable id, String type) {
102
// Validate UUID format for certain types
103
if ("com.example.Document".equals(type) && id instanceof String) {
104
String uuid = (String) id;
105
if (!isValidUuid(uuid)) {
106
throw new IllegalArgumentException("Invalid UUID format: " + uuid);
107
}
108
}
109
110
return new ObjectIdentityImpl(type, id);
111
}
112
113
private boolean isValidUuid(String uuid) {
114
try {
115
UUID.fromString(uuid);
116
return true;
117
} catch (IllegalArgumentException e) {
118
return false;
119
}
120
}
121
}
122
```
123
124
## SidRetrievalStrategy
125
126
Extracts security identities from Spring Security's `Authentication`:
127
128
```java { .api }
129
package org.springframework.security.acls.model;
130
131
public interface SidRetrievalStrategy {
132
133
/**
134
* Obtain security identities from Authentication
135
* @param authentication current authentication
136
* @return list of security identities
137
*/
138
List<Sid> getSids(Authentication authentication);
139
}
140
```
141
142
### Default Implementation
143
144
```java { .api }
145
package org.springframework.security.acls.domain;
146
147
public class SidRetrievalStrategyImpl implements SidRetrievalStrategy {
148
149
private boolean roleHierarchySupported = false;
150
private RoleHierarchy roleHierarchy;
151
152
@Override
153
public List<Sid> getSids(Authentication authentication) {
154
List<Sid> sids = new ArrayList<>();
155
156
// Add principal SID
157
sids.add(new PrincipalSid(authentication));
158
159
// Add authority SIDs
160
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
161
162
if (roleHierarchySupported && roleHierarchy != null) {
163
authorities = roleHierarchy.getReachableGrantedAuthorities(authorities);
164
}
165
166
for (GrantedAuthority authority : authorities) {
167
sids.add(new GrantedAuthoritySid(authority));
168
}
169
170
return sids;
171
}
172
173
public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
174
this.roleHierarchy = roleHierarchy;
175
this.roleHierarchySupported = true;
176
}
177
}
178
```
179
180
### Custom Implementation with Group Support
181
182
```java { .api }
183
@Component
184
public class GroupAwareSidRetrievalStrategy implements SidRetrievalStrategy {
185
186
@Autowired
187
private GroupService groupService;
188
189
@Override
190
public List<Sid> getSids(Authentication authentication) {
191
List<Sid> sids = new ArrayList<>();
192
193
// Add principal SID
194
sids.add(new PrincipalSid(authentication));
195
196
// Add direct authorities
197
for (GrantedAuthority authority : authentication.getAuthorities()) {
198
sids.add(new GrantedAuthoritySid(authority));
199
}
200
201
// Add group memberships
202
String username = authentication.getName();
203
List<String> groups = groupService.getUserGroups(username);
204
205
for (String group : groups) {
206
sids.add(new GrantedAuthoritySid("GROUP_" + group));
207
}
208
209
// Add organizational hierarchy
210
String department = getUserDepartment(authentication);
211
if (department != null) {
212
sids.add(new GrantedAuthoritySid("DEPT_" + department));
213
}
214
215
return sids;
216
}
217
218
private String getUserDepartment(Authentication authentication) {
219
// Extract department from user details or external service
220
if (authentication.getPrincipal() instanceof UserDetails) {
221
return ((CustomUserDetails) authentication.getPrincipal()).getDepartment();
222
}
223
return null;
224
}
225
}
226
```
227
228
## PermissionGrantingStrategy
229
230
Controls the core permission evaluation logic:
231
232
```java { .api }
233
package org.springframework.security.acls.model;
234
235
public interface PermissionGrantingStrategy {
236
237
/**
238
* Evaluate if permissions are granted
239
* @param acl the ACL containing entries to evaluate
240
* @param permission required permissions
241
* @param sids security identities to check
242
* @param administrativeMode if true, skip auditing
243
* @return true if permission is granted
244
*/
245
boolean isGranted(Acl acl, List<Permission> permission, List<Sid> sids,
246
boolean administrativeMode) throws NotFoundException, UnloadedSidException;
247
}
248
```
249
250
### Default Implementation
251
252
```java { .api }
253
package org.springframework.security.acls.domain;
254
255
public class DefaultPermissionGrantingStrategy implements PermissionGrantingStrategy {
256
257
private final AuditLogger auditLogger;
258
259
public DefaultPermissionGrantingStrategy(AuditLogger auditLogger) {
260
this.auditLogger = auditLogger;
261
}
262
263
@Override
264
public boolean isGranted(Acl acl, List<Permission> permission, List<Sid> sids,
265
boolean administrativeMode) throws NotFoundException, UnloadedSidException {
266
267
List<AccessControlEntry> aces = acl.getEntries();
268
269
for (Permission p : permission) {
270
// Check each permission against ACL entries
271
for (AccessControlEntry ace : aces) {
272
if (ace.getPermission().getMask() == p.getMask()) {
273
if (sids.contains(ace.getSid())) {
274
// Log audit event
275
if (!administrativeMode) {
276
auditLogger.logIfNeeded(true, ace);
277
}
278
return ace.isGranting();
279
}
280
}
281
}
282
}
283
284
// Check parent ACL if inheritance is enabled
285
if (acl.isEntriesInheriting() && acl.getParentAcl() != null) {
286
return isGranted(acl.getParentAcl(), permission, sids, administrativeMode);
287
}
288
289
throw new NotFoundException("Unable to locate a matching ACE for passed permissions and SIDs");
290
}
291
}
292
```
293
294
### Custom Permission Strategy
295
296
```java { .api }
297
@Component
298
public class CustomPermissionGrantingStrategy implements PermissionGrantingStrategy {
299
300
private final AuditLogger auditLogger;
301
302
@Override
303
public boolean isGranted(Acl acl, List<Permission> permission, List<Sid> sids,
304
boolean administrativeMode) throws NotFoundException, UnloadedSidException {
305
306
List<AccessControlEntry> aces = acl.getEntries();
307
308
// Custom logic: check deny entries first (explicit deny takes precedence)
309
for (AccessControlEntry ace : aces) {
310
if (!ace.isGranting() && sids.contains(ace.getSid())) {
311
for (Permission p : permission) {
312
if (hasPermission(ace.getPermission(), p)) {
313
if (!administrativeMode) {
314
auditLogger.logIfNeeded(false, ace);
315
}
316
return false; // Explicit deny
317
}
318
}
319
}
320
}
321
322
// Then check grant entries
323
for (AccessControlEntry ace : aces) {
324
if (ace.isGranting() && sids.contains(ace.getSid())) {
325
for (Permission p : permission) {
326
if (hasPermission(ace.getPermission(), p)) {
327
if (!administrativeMode) {
328
auditLogger.logIfNeeded(true, ace);
329
}
330
return true;
331
}
332
}
333
}
334
}
335
336
// Check inheritance
337
if (acl.isEntriesInheriting() && acl.getParentAcl() != null) {
338
return isGranted(acl.getParentAcl(), permission, sids, administrativeMode);
339
}
340
341
return false; // Default deny
342
}
343
344
private boolean hasPermission(Permission acePermission, Permission requiredPermission) {
345
// Custom permission matching logic (e.g., bitwise operations)
346
return (acePermission.getMask() & requiredPermission.getMask()) == requiredPermission.getMask();
347
}
348
}
349
```
350
351
## AclAuthorizationStrategy
352
353
Controls who can modify ACL entries:
354
355
```java { .api }
356
package org.springframework.security.acls.domain;
357
358
public interface AclAuthorizationStrategy {
359
360
/**
361
* Check if current user can change ACL ownership
362
*/
363
void securityCheck(Acl acl, int changeType);
364
365
// Change type constants
366
int CHANGE_OWNERSHIP = 0;
367
int CHANGE_AUDITING = 1;
368
int CHANGE_GENERAL = 2;
369
}
370
```
371
372
### Default Implementation
373
374
```java { .api }
375
public class AclAuthorizationStrategyImpl implements AclAuthorizationStrategy {
376
377
private final GrantedAuthority gaGeneralChanges;
378
private final GrantedAuthority gaModifyAuditing;
379
private final GrantedAuthority gaTakeOwnership;
380
381
public AclAuthorizationStrategyImpl(GrantedAuthority... auths) {
382
// Configure required authorities for different operations
383
this.gaTakeOwnership = auths[0];
384
this.gaModifyAuditing = auths.length > 1 ? auths[1] : auths[0];
385
this.gaGeneralChanges = auths.length > 2 ? auths[2] : auths[0];
386
}
387
388
@Override
389
public void securityCheck(Acl acl, int changeType) {
390
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
391
392
switch (changeType) {
393
case CHANGE_OWNERSHIP:
394
if (!hasAuthority(auth, gaTakeOwnership) && !isOwner(auth, acl)) {
395
throw new AccessDeniedException("Access denied");
396
}
397
break;
398
case CHANGE_AUDITING:
399
if (!hasAuthority(auth, gaModifyAuditing)) {
400
throw new AccessDeniedException("Access denied");
401
}
402
break;
403
case CHANGE_GENERAL:
404
if (!hasAuthority(auth, gaGeneralChanges) && !isOwner(auth, acl)) {
405
throw new AccessDeniedException("Access denied");
406
}
407
break;
408
}
409
}
410
411
private boolean hasAuthority(Authentication auth, GrantedAuthority required) {
412
return auth.getAuthorities().contains(required);
413
}
414
415
private boolean isOwner(Authentication auth, Acl acl) {
416
Sid owner = acl.getOwner();
417
return owner != null && owner.equals(new PrincipalSid(auth));
418
}
419
}
420
```
421
422
## PermissionFactory
423
424
Creates `Permission` instances from masks or names:
425
426
```java { .api }
427
package org.springframework.security.acls.domain;
428
429
public interface PermissionFactory {
430
431
/**
432
* Create permission from bitmask
433
*/
434
Permission buildFromMask(int mask);
435
436
/**
437
* Create permission from name
438
*/
439
Permission buildFromName(String name);
440
441
/**
442
* Create multiple permissions from names
443
*/
444
List<Permission> buildFromNames(List<String> names);
445
}
446
```
447
448
### Default Implementation
449
450
```java { .api }
451
public class DefaultPermissionFactory implements PermissionFactory {
452
453
private final Map<Integer, Permission> registeredPermissions = new HashMap<>();
454
private final Map<String, Permission> namedPermissions = new HashMap<>();
455
456
public DefaultPermissionFactory() {
457
// Register built-in permissions
458
registerPublicPermissions(BasePermission.class);
459
}
460
461
public void registerPublicPermissions(Class<?> clazz) {
462
Field[] fields = clazz.getFields();
463
464
for (Field field : fields) {
465
if (Permission.class.isAssignableFrom(field.getType())) {
466
try {
467
Permission permission = (Permission) field.get(null);
468
registeredPermissions.put(permission.getMask(), permission);
469
namedPermissions.put(field.getName(), permission);
470
} catch (IllegalAccessException e) {
471
// Skip inaccessible fields
472
}
473
}
474
}
475
}
476
477
@Override
478
public Permission buildFromMask(int mask) {
479
Permission permission = registeredPermissions.get(mask);
480
if (permission == null) {
481
throw new IllegalArgumentException("Unknown permission mask: " + mask);
482
}
483
return permission;
484
}
485
486
@Override
487
public Permission buildFromName(String name) {
488
Permission permission = namedPermissions.get(name.toUpperCase());
489
if (permission == null) {
490
throw new IllegalArgumentException("Unknown permission name: " + name);
491
}
492
return permission;
493
}
494
495
@Override
496
public List<Permission> buildFromNames(List<String> names) {
497
return names.stream()
498
.map(this::buildFromName)
499
.collect(Collectors.toList());
500
}
501
}
502
```
503
504
## AuditLogger
505
506
Logs permission evaluation events:
507
508
```java { .api }
509
package org.springframework.security.acls.domain;
510
511
public interface AuditLogger {
512
513
/**
514
* Log access attempt if auditing is enabled for the ACE
515
* @param granted whether access was granted
516
* @param ace the access control entry being evaluated
517
*/
518
void logIfNeeded(boolean granted, AccessControlEntry ace);
519
}
520
```
521
522
### Console Implementation
523
524
```java { .api }
525
public class ConsoleAuditLogger implements AuditLogger {
526
527
@Override
528
public void logIfNeeded(boolean granted, AccessControlEntry ace) {
529
if (ace instanceof AuditableAccessControlEntry) {
530
AuditableAccessControlEntry auditableAce = (AuditableAccessControlEntry) ace;
531
532
if ((granted && auditableAce.isAuditSuccess()) ||
533
(!granted && auditableAce.isAuditFailure())) {
534
535
System.out.println("GRANTED: " + granted +
536
", ACE: " + ace +
537
", Principal: " + getCurrentUsername());
538
}
539
}
540
}
541
542
private String getCurrentUsername() {
543
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
544
return auth != null ? auth.getName() : "anonymous";
545
}
546
}
547
```
548
549
### Custom Audit Logger
550
551
```java { .api }
552
@Component
553
public class DatabaseAuditLogger implements AuditLogger {
554
555
@Autowired
556
private AuditEventRepository auditRepository;
557
558
@Override
559
public void logIfNeeded(boolean granted, AccessControlEntry ace) {
560
if (ace instanceof AuditableAccessControlEntry) {
561
AuditableAccessControlEntry auditableAce = (AuditableAccessControlEntry) ace;
562
563
boolean shouldLog = (granted && auditableAce.isAuditSuccess()) ||
564
(!granted && auditableAce.isAuditFailure());
565
566
if (shouldLog) {
567
AuditEvent event = new AuditEvent();
568
event.setTimestamp(Instant.now());
569
event.setPrincipal(getCurrentUsername());
570
event.setObjectIdentity(ace.getAcl().getObjectIdentity().toString());
571
event.setPermission(ace.getPermission().getPattern());
572
event.setSid(ace.getSid().toString());
573
event.setGranted(granted);
574
event.setAceId(ace.getId());
575
576
auditRepository.save(event);
577
}
578
}
579
}
580
581
private String getCurrentUsername() {
582
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
583
return auth != null ? auth.getName() : "anonymous";
584
}
585
}
586
```
587
588
## Configuration
589
590
Configure strategy implementations in your Spring configuration:
591
592
```java { .api }
593
@Configuration
594
@EnableGlobalMethodSecurity(prePostEnabled = true)
595
public class AclStrategyConfig {
596
597
@Bean
598
public ObjectIdentityRetrievalStrategy objectIdentityRetrievalStrategy() {
599
return new CustomObjectIdentityRetrievalStrategy();
600
}
601
602
@Bean
603
public ObjectIdentityGenerator objectIdentityGenerator() {
604
return new UuidObjectIdentityGenerator();
605
}
606
607
@Bean
608
public SidRetrievalStrategy sidRetrievalStrategy() {
609
return new GroupAwareSidRetrievalStrategy();
610
}
611
612
@Bean
613
public PermissionGrantingStrategy permissionGrantingStrategy() {
614
return new CustomPermissionGrantingStrategy(auditLogger());
615
}
616
617
@Bean
618
public AclAuthorizationStrategy aclAuthorizationStrategy() {
619
return new AclAuthorizationStrategyImpl(
620
new SimpleGrantedAuthority("ROLE_ADMIN"),
621
new SimpleGrantedAuthority("ROLE_AUDITOR"),
622
new SimpleGrantedAuthority("ROLE_ACL_ADMIN")
623
);
624
}
625
626
@Bean
627
public PermissionFactory permissionFactory() {
628
DefaultPermissionFactory factory = new DefaultPermissionFactory();
629
factory.registerPublicPermissions(CustomPermission.class);
630
return factory;
631
}
632
633
@Bean
634
public AuditLogger auditLogger() {
635
return new DatabaseAuditLogger();
636
}
637
638
@Bean
639
public AclPermissionEvaluator permissionEvaluator(AclService aclService) {
640
AclPermissionEvaluator evaluator = new AclPermissionEvaluator(aclService);
641
evaluator.setObjectIdentityRetrievalStrategy(objectIdentityRetrievalStrategy());
642
evaluator.setObjectIdentityGenerator(objectIdentityGenerator());
643
evaluator.setSidRetrievalStrategy(sidRetrievalStrategy());
644
evaluator.setPermissionFactory(permissionFactory());
645
return evaluator;
646
}
647
}
648
```
649
650
## Best Practices
651
652
### 1. Strategy Interface Compatibility
653
654
Ensure your custom strategies work well together:
655
656
```java { .api }
657
@Component
658
public class CompatibleStrategies {
659
660
@Autowired
661
private ObjectIdentityRetrievalStrategy objectIdentityRetrievalStrategy;
662
663
@Autowired
664
private SidRetrievalStrategy sidRetrievalStrategy;
665
666
@PostConstruct
667
public void validateCompatibility() {
668
// Test with sample objects to ensure strategies work together
669
Document sampleDoc = new Document();
670
sampleDoc.setId(1L);
671
672
ObjectIdentity identity = objectIdentityRetrievalStrategy.getObjectIdentity(sampleDoc);
673
674
MockAuthentication auth = new MockAuthentication("testuser");
675
List<Sid> sids = sidRetrievalStrategy.getSids(auth);
676
677
Assert.notNull(identity, "ObjectIdentity strategy failed");
678
Assert.notEmpty(sids, "SID retrieval strategy failed");
679
}
680
}
681
```
682
683
### 2. Performance Considerations
684
685
Optimize strategy implementations for your use case:
686
687
```java { .api }
688
@Component
689
public class CachingObjectIdentityRetrievalStrategy implements ObjectIdentityRetrievalStrategy {
690
691
private final Cache<Object, ObjectIdentity> cache = Caffeine.newBuilder()
692
.maximumSize(10000)
693
.expireAfterWrite(Duration.ofMinutes(30))
694
.build();
695
696
@Override
697
public ObjectIdentity getObjectIdentity(Object domainObject) {
698
return cache.get(domainObject, this::createObjectIdentity);
699
}
700
701
private ObjectIdentity createObjectIdentity(Object domainObject) {
702
return new ObjectIdentityImpl(domainObject);
703
}
704
}
705
```
706
707
### 3. Testing Strategies
708
709
Thoroughly test custom strategy implementations:
710
711
```java { .api }
712
@ExtendWith(SpringExtension.class)
713
class CustomStrategiesTest {
714
715
@Mock
716
private GroupService groupService;
717
718
@InjectMocks
719
private GroupAwareSidRetrievalStrategy sidStrategy;
720
721
@Test
722
void shouldIncludeGroupSids() {
723
when(groupService.getUserGroups("testuser"))
724
.thenReturn(Arrays.asList("developers", "managers"));
725
726
Authentication auth = new UsernamePasswordAuthenticationToken(
727
"testuser", "password", Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
728
729
List<Sid> sids = sidStrategy.getSids(auth);
730
731
assertThat(sids).hasSize(4); // principal + role + 2 groups
732
assertThat(sids).contains(new GrantedAuthoritySid("GROUP_developers"));
733
assertThat(sids).contains(new GrantedAuthoritySid("GROUP_managers"));
734
}
735
}
736
```
737
738
Strategy interfaces provide powerful customization capabilities for the ACL system. The next step is understanding how these strategies integrate with [permission evaluation](permission-evaluation.md) in your application.