or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

acl-services.mdcaching-performance.mdconfiguration.mddomain-model.mdindex.mdpermission-evaluation.mdstrategy-interfaces.md

strategy-interfaces.mddocs/

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.