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

permission-evaluation.mddocs/

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.