Skip to content

Your First Protected Endpoint

@RestController
@RequestMapping("/api/loans")
class LoanController(private val loanService: LoanService) {
@GetMapping
@RequiresPermission(resource = "loans", action = "read")
fun list(): List<LoanDto> = loanService.findAll()
@PostMapping
@RequiresPermission(resource = "loans", action = "create")
fun create(@RequestBody request: CreateLoanRequest): LoanDto = loanService.create(request)
@PostMapping("/{id}/approve")
@RequiresPermission(resource = "loans", action = "approve")
fun approve(@PathVariable id: UUID): LoanDto = loanService.approve(id)
}

A request only reaches the method body if the authenticated user’s resolved permissions allow that (resource, action) pair. Otherwise IAM returns 403 with a structured error.

Permissions resolve through three tiers, and two rules are absolute:

  1. DENY always wins over ALLOW at the same level.
  2. Implicit deny — no permission means no access.
User ── direct roles ──┐
User ── groups ── group roles ──┤→ merge role hierarchy → permissions
│ (resource, action, field, ALLOW|DENY)
└→ resolved object, cached (default 5 min)
  • Feature tier — can the user access the loans module at all?
  • Action tier — which operations: read, create, approve, export
  • Field tier — per-field visibility, e.g. ssn hidden from most roles (guide).

For decisions inside service logic, inject PermissionChecker — the same resolution the annotation uses:

@Service
class LoanExportService(private val permissions: PermissionChecker) {
fun export(loanId: UUID): ExportResult {
if (!permissions.canDo("loans", "export")) {
throw IamAccessDeniedException("loans", "export")
}
// sensitive column only for users who may see it
val includeSsn = permissions.isFieldVisible("loans", "ssn")
return exporter.run(loanId, includeSsn)
}
}

PermissionChecker is safe-deny: any resolution failure returns false, never an exception.

Your annotation registered the loans resource at startup (how). Now grant it — in the Admin UI permission matrix, or via the API:

Terminal window
# create a permission (resource=loans, action=approve, ALLOW) and attach it to a role
curl -X POST localhost:8080/api/iam/v1/permissions \
-H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
-d '{"resourceCode": "loans", "actionCode": "approve", "effect": "ALLOW"}'

Assign the role to a user, and the next /me/permissions resolution reflects it (cache TTL 5 minutes by default, invalidated on permission changes).