Skip to main content
Version: v0.13

Command Authorization Extension

The PolicyCommandExtension automatically intercepts command execution to enforce authorization policies. This provides transparent authorization without modifying individual command implementations.

Overview

The command extension integrates with the IDK's command execution pipeline:

Command Extension Flow

Command IDs

Commands are identified by hierarchical IDs following the pattern domain.resource.operation:

data class CommandId(
val domain: String, // e.g., "identity", "party", "kms"
val resource: String, // e.g., "identities", "parties", "keys"
val operation: String // e.g., "create", "read", "update", "delete"
) {
val fullId: String get() = "$domain.$resource.$operation"
}

// Examples
CommandId.parse("identity.identities.create")
// domain=identity, resource=identities, operation=create

CommandId.parse("party.contacts.delete")
// domain=party, resource=contacts, operation=delete

Pattern Matching

Include and exclude patterns use glob-style matching:

PatternMatches
identity.**All identity commands
*.*.createAll create operations
party.contacts.*All contact operations
kms.keys.signExact match
val config = AuthZenConfig(
// Skip authorization for health and discovery
excludePatterns = listOf(
"health.**",
"discovery.**",
"actuator.**"
),
// Only authorize these patterns
includePatterns = listOf("**") // All commands
)

PolicyCommandExtension

The extension implements ICommandExecutionExtension to intercept commands:

class PolicyCommandExtension<Arg : Any, SuccessResult : Any, ErrorResult : IdkErrorType>(
private val policyEngineProvider: PolicyEngineProvider,
private val config: AuthZenConfig,
private val sessionContextProvider: () -> SessionContext
) : ICommandExecutionExtension<Arg, SuccessResult, ErrorResult> {

override fun beforeExecute(
service: Command<Arg, SuccessResult, ErrorResult>,
args: Arg,
plugin: ISurePlugin?
) {
// Skip if disabled
if (!config.enabled) return

// Check include/exclude patterns
if (!shouldAuthorize(service.id)) return

// Evaluate authorization
runBlockingCompat {
val result = evaluateAuthorization(service, args)
result.fold(
success = { decision ->
if (decision.isDenied) {
throw AuthorizationDeniedException(
commandId = service.id,
reason = decision.reasons.firstOrNull() ?: "Access denied"
)
}
},
failure = { error ->
handleError(service.id, error)
}
)
}
}

private suspend fun evaluateAuthorization(
command: Command<*, *, *>,
args: Any
): IdkResult<PolicyDecision, IdkError> {
val context = sessionContextProvider()

// Extract subjects from session (user, workload, roles)
val subjects = extractSubjects(context)

// Map command to action
val action = mapAction(command.id)

// Map arguments to resource
val resource = mapResource(command.id, args)

// Build request and evaluate
val request = PolicyRequest(
principal = subjects.first(),
action = action,
resource = resource,
context = PolicyContext(
tenantId = context.tenant.tenantId,
sessionId = context.sessionId,
timestamp = Clock.System.now().toEpochMilliseconds()
)
)

return policyEngineProvider.getEngine().evaluate(request)
}
}

Subject Extraction

The extension extracts subjects from JWT tokens in the session context:

class JwtSubjectExtractor(
private val roleClaimNames: List<String> = listOf("roles", "groups", "realm_access.roles")
) : AuthZenSubjectExtractor {

override suspend fun extractSubjects(
sessionContext: SessionContext
): IdkResult<List<AuthZenSubject>, IdkError> {
val subjects = mutableListOf<AuthZenSubject>()

// User subject from principal
val principal = sessionContext.principal
if (principal != null) {
subjects.add(AuthZenSubject(
type = "user",
id = principal.toString(),
properties = buildMap {
put("tenant_id", JsonPrimitive(sessionContext.tenant.tenantId))
sessionContext.accessToken?.let { token ->
token.email?.let { put("email", JsonPrimitive(it)) }
token.name?.let { put("name", JsonPrimitive(it)) }
}
}
))
}

// Workload subject from JWT issuer
sessionContext.accessToken?.issuer?.let { issuer ->
subjects.add(AuthZenSubject(
type = "workload",
id = issuer,
properties = emptyMap()
))
}

// Role subjects from JWT claims
val roles = extractRoles(sessionContext.accessToken)
roles.forEach { role ->
subjects.add(AuthZenSubject(
type = "role",
id = role,
properties = emptyMap()
))
}

return Ok(subjects)
}
}

Action Mapping

Commands are mapped to policy actions with operation categories:

class DefaultActionMapper : AuthZenActionMapper {

override fun mapAction(commandId: String): AuthZenAction {
val parsed = CommandId.parse(commandId)
?: return AuthZenAction(name = commandId)

return AuthZenAction(
name = parsed.fullId,
properties = buildMap {
put("domain", JsonPrimitive(parsed.domain))
put("resource", JsonPrimitive(parsed.resource))
put("operation", JsonPrimitive(parsed.operation))
put("operation_category", JsonPrimitive(
getOperationCategory(parsed.operation)
))
}
)
}

private fun getOperationCategory(operation: String): String {
return when (operation) {
"get", "list", "find", "search", "exists" -> "read"
"create", "add", "register" -> "create"
"update", "patch", "modify" -> "update"
"delete", "remove", "unregister" -> "delete"
"enable", "disable", "configure" -> "admin"
"execute", "run", "invoke" -> "execute"
"sign", "verify", "encrypt", "decrypt" -> "crypto"
else -> "other"
}
}
}

Resource Mapping

Custom mappers extract resource information from command arguments:

interface AuthZenResourceMapper<Arg> {
fun mapResource(commandId: String, args: Arg): AuthZenResource?
fun supports(commandId: String): Boolean = true
}

// Example: Identity resource mapper
class IdentityResourceMapper : AuthZenResourceMapper<IdentityArgs> {

override fun mapResource(commandId: String, args: IdentityArgs): AuthZenResource {
return AuthZenResource(
type = "Identity",
id = args.identityId,
properties = buildMap {
args.partyId?.let { put("party_id", JsonPrimitive(it)) }
args.tenantId?.let { put("tenant_id", JsonPrimitive(it)) }
}
)
}

override fun supports(commandId: String): Boolean {
return commandId.startsWith("identity.identities.")
}
}

// Register mappers
val registry = DefaultResourceMapperRegistry().apply {
register(IdentityArgs::class, IdentityResourceMapper())
register(PartyArgs::class, PartyResourceMapper())
}

Error Handling

Authorization failures throw specific exceptions:

class AuthorizationDeniedException(
val commandId: String,
val reason: String
) : RuntimeException("Authorization denied for $commandId: $reason")

class AuthorizationException(
val commandId: String,
val reason: String
) : RuntimeException("Authorization error for $commandId: $reason")

Handle in your service layer:

try {
val result = command.execute(args, context)
// Success
} catch (e: AuthorizationDeniedException) {
// User not authorized - return 403
throw ForbiddenException(e.reason)
} catch (e: AuthorizationException) {
// Authorization system error - use fallback or fail
when (config.fallbackPolicy) {
FallbackPolicy.ALLOW -> {
// Proceed anyway (development only)
command.execute(args, context)
}
FallbackPolicy.DENY -> throw ForbiddenException("Authorization unavailable")
FallbackPolicy.FAIL -> throw e
}
}

Configuration

Per-Command Exclusions

val config = AuthZenConfig(
enabled = true,
excludePatterns = listOf(
// System endpoints
"health.**",
"actuator.**",
"discovery.**",

// Public endpoints
"public.**",

// Skip specific commands
"identity.tokens.refresh",
"oauth2.*.authorize"
),
includePatterns = listOf("**")
)

Domain-Specific Authorization

val config = AuthZenConfig(
enabled = true,
excludePatterns = listOf("health.**"),
// Only authorize specific domains
includePatterns = listOf(
"party.**", // All party operations
"identity.**", // All identity operations
"kms.keys.*" // Key operations only
)
)

Spring Boot Integration

The command extension is automatically configured with Spring Boot auto-configuration:

sphereon:
authzen:
enabled: true
pdp:
type: cedarling
base-url: http://cedarling:5000
exclude-patterns:
- "health.**"
- "actuator.**"
include-patterns:
- "**"

The extension is registered as a Spring bean and injected into the command execution pipeline.

Testing

Unit Testing

@Test
fun `should deny unauthorized access`() {
// Setup mock policy engine
val mockEngine = mockk<PolicyEngine>()
coEvery { mockEngine.evaluate(any()) } returns Ok(
PolicyDecision(decision = Decision.DENY, reasons = listOf("Not authorized"))
)

val extension = PolicyCommandExtension(
policyEngineProvider = { mockEngine },
config = AuthZenConfig(enabled = true),
sessionContextProvider = { testSessionContext }
)

// Should throw
assertThrows<AuthorizationDeniedException> {
extension.beforeExecute(testCommand, testArgs, null)
}
}

@Test
fun `should skip excluded commands`() {
val config = AuthZenConfig(
enabled = true,
excludePatterns = listOf("health.**")
)

val extension = PolicyCommandExtension(
policyEngineProvider = { mockEngine },
config = config,
sessionContextProvider = { testSessionContext }
)

// Health command should not evaluate policy
extension.beforeExecute(healthCheckCommand, args, null)

coVerify(exactly = 0) { mockEngine.evaluate(any()) }
}

Integration Testing

@SpringBootTest
class AuthorizationIntegrationTest {

@Autowired
lateinit var partyService: PartyService

@Test
fun `admin can create party`() {
// Login as admin
withAdminContext {
val party = partyService.createParty(input)
assertNotNull(party)
}
}

@Test
fun `regular user cannot delete party`() {
withUserContext {
assertThrows<AuthorizationDeniedException> {
partyService.deleteParty(partyId)
}
}
}
}