A named authorization bean can centralize tenant ownership checks while @PreAuthorize keeps the decision before a protected method runs.
Spring method security ownership bean: authorize before the service changes state
Keep the guard near the command
A receipt mutation is dangerous even when its controller route requires a generic write scope. The service must still prove that the signed-in tenant owns the target receipt. A named bean used by @PreAuthorize can perform that check before the method body executes. This avoids the post-authorization side-effect trap. It does not remove the need for tenant predicates in the final database write, because ownership can change between a preliminary check and mutation.
Use a narrow input contract
Give the authorization bean the authenticated tenant and receipt ID, not an entire request body or a detached entity. Reject missing identity and nonexistent receipts without disclosing another tenant’s record. The repository should query by both tenant and ID and use an index that supports that predicate. Method security advice runs through the Spring bean proxy, so self-invocation inside the same service does not activate the annotation.
Test bypass attempts
Call the service through its Spring bean with an owner, another tenant and no authentication. Assert only the owner reaches the mutation. Then call the repository write in a concurrent test with a changed owner or revision to prove that the final predicate still protects the row. A method guard is one layer; versioned tenant writes close the race at the storage boundary.
Implementation contract
@PreAuthorize("@receiptAccess.owns(authentication, #tenantId, #receiptId)")
@Transactional
public void voidReceipt(UUID tenantId, UUID receiptId) {
repository.voidOwnedReceipt(tenantId, receiptId);
}
// receiptAccess is a Spring bean that checks the authenticated tenant.Cost and verification
A database-backed ownership check may add one SELECT before the write. A tenant-and-version predicate on the write adds little query cost with the right index and protects against a stale authorization decision.
Common Mistakes
- Do not treat a write scope as proof of ownership of every receipt.
- Do not put the first ownership check after a method has already mutated state.
- Do not rely on @PreAuthorize during self-invocation that bypasses the bean proxy.
Read next
Spring method authorization: test the proxied service boundary, Spring method security: authorization advice runs through the bean proxy, Spring @PostAuthorize: denial happens after the method has run, Spring JDBC versioned tenant update: inspect the affected row count, Spring Security scope versus tenant ownership: two separate decisions.
