Your First Protected Endpoint
Annotate the endpoint
Section titled “Annotate the 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.
The model behind the annotation
Section titled “The model behind the annotation”Permissions resolve through three tiers, and two rules are absolute:
- DENY always wins over ALLOW at the same level.
- 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
loansmodule at all? - Action tier — which operations:
read,create,approve,export… - Field tier — per-field visibility, e.g.
ssnhidden from most roles (guide).
Checking access programmatically
Section titled “Checking access programmatically”For decisions inside service logic, inject PermissionChecker — the same resolution the
annotation uses:
@Serviceclass 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.
Granting the permission
Section titled “Granting the permission”Your annotation registered the loans resource at startup
(how). Now grant it — in the
Admin UI permission matrix, or via the API:
# create a permission (resource=loans, action=approve, ALLOW) and attach it to a rolecurl -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).