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)
}
}
}
}