Authorization Webhook¶
The Authorization Webhook evaluates every kcp SubjectAccessReview by querying OpenFGA to determine if the request should be allowed. It does not write tuples or models - it only performs checks against existing data created by the Security Operator and Account Operator.
Overview¶
sequenceDiagram
participant kcp
participant Webhook
participant OpenFGA
kcp->>Webhook: SubjectAccessReview
Webhook->>OpenFGA: Check(user, relation, object)
OpenFGA-->>Webhook: allowed: true/false
Webhook-->>kcp: SubjectAccessReview response
Architecture¶
Handler Chain¶
The webhook uses a handler chain evaluated in order:
| Order | Handler | Source File |
|---|---|---|
| 1 | NonResourceAttributes | pkg/handler/nonresourceattributes/non_resource_attrs.go |
| 2 | Orgs | pkg/handler/orgs/orgs.go |
| 3 | Contextual | pkg/handler/contextual/contextual.go |
The chain uses union.New(...) from pkg/authorization/union/union.go:
- The first handler that explicitly allows, denies, or aborts ends the chain
- If all handlers return NoOpinion, the request remains not allowed
OpenFGA Connection¶
- Address: Configured via
openfga-addrinpkg/config/config.go - Orgs Store: Resolved at startup by
ListStores(Name: "orgs")incmd/serve.go
Handlers¶
Non-Resource Requests¶
Non-resource paths (e.g., /api, /openapi) are allowed if they match configured prefixes.
Configuration: webhook-allowed-nonresource-prefixes in pkg/config/config.go
Orgs Handler¶
Handles requests to the root orgs workspace.
When it runs: Only when Extra[webhook-cluster-key] equals the orgs workspace cluster ID (resolved from root:orgs).
OpenFGA Check:
StoreId: <orgs-store-id>
Object: tenancy_kcp_io_workspace:orgs
Relation: <verb>_<group>_<resource>
User: user:<subject>
<group>is capped and normalized (.→_) viapkg/util/rules_helper.go- If allowed → request is allowed
- If denied → handler aborts the chain (explicit deny)
Contextual Handler¶
Handles requests to per-account workspaces.
When it runs: For requests not handled by the orgs handler.
Data Sources:
| Data | Source |
|---|---|
| Store ID | AccountInfo.spec.fga.store.id |
| Account Origin | AccountInfo.spec.account.originClusterId |
| Account Name | AccountInfo.spec.account.name |
| Workspace Cluster | Extra[webhook-cluster-key] |
Authorization Resolution:
| Verb | Resolved On |
|---|---|
| create/list/watch | Parent (account or namespace) |
| Other verbs | Resource object |
Object and Relation Formats:
- Resource object type:
<group>_<singular>(group capped to 50 chars; dots replaced) - Resource object:
<group>_<singular>:<cluster>/<resource-name> - Parent account:
core_platform-mesh_io_account:<account-origin-cluster-id>/<account-name> - Relation for create/list/watch:
<verb>_<group>_<resource>on parent - Relation for other verbs:
<verb>on resource object
Contextual Tuples Added:
For namespaced resources:
If verb is not create/list/watch:
For cluster-scoped resources:
Result Behavior:
- If allowed → request is allowed
- If denied → handler returns NoOpinion (no abort), ending chain as not allowed
Component Interaction¶
graph TB
subgraph "Security Operator"
SO[Creates stores & models]
SOT[Writes baseline tuples]
end
subgraph "Account Operator"
AO[Writes account tuples]
end
subgraph "OpenFGA"
Store[(Store)]
Model[Authorization Model]
Tuples[Tuples]
end
subgraph "Webhook"
WH[Check requests]
end
SO --> Store
SO --> Model
SOT --> Tuples
AO --> Tuples
WH --> Store
WH --> Model
WH --> Tuples
| Component | Responsibility |
|---|---|
| Security Operator | Creates stores, writes models, optionally writes baseline tuples |
| Account Operator | Writes account relationship tuples (creator, owner, parent) |
| Webhook | Checks relations against tuples, adds contextual parent relations |
Examples¶
Create Namespaced Resource¶
Request: Create a deployment in namespace team-a
Webhook → OpenFGA:
{
"storeId": "<account-store-id>",
"tupleKey": {
"object": "core_namespace:<workspace-cluster-id>/team-a",
"relation": "create_apps_deployments",
"user": "user:alice@example.com"
},
"contextualTuples": {
"tupleKeys": [
{
"object": "core_namespace:<workspace-cluster-id>/team-a",
"relation": "parent",
"user": "core_platform-mesh_io_account:<account-origin-cluster-id>/<account-name>"
}
]
}
}
Get Namespaced Resource¶
Request: Get deployment demo in namespace team-a
Webhook → OpenFGA:
{
"storeId": "<account-store-id>",
"tupleKey": {
"object": "apps_deployment:<workspace-cluster-id>/demo",
"relation": "get",
"user": "user:alice@example.com"
},
"contextualTuples": {
"tupleKeys": [
{
"object": "core_namespace:<workspace-cluster-id>/team-a",
"relation": "parent",
"user": "core_platform-mesh_io_account:<account-origin-cluster-id>/<account-name>"
},
{
"object": "apps_deployment:<workspace-cluster-id>/demo",
"relation": "parent",
"user": "core_namespace:<workspace-cluster-id>/team-a"
}
]
}
}
Orgs Workspace Request¶
Request: List workspaces in the orgs workspace
Webhook → OpenFGA:
{
"storeId": "<orgs-store-id>",
"tupleKey": {
"object": "tenancy_kcp_io_workspace:orgs",
"relation": "list_core_workspaces",
"user": "user:alice@example.com"
}
}
If allowed: false, the orgs handler aborts the chain (explicit deny).
Message Flow Reference¶
SubjectAccessReview (kcp → Webhook)¶
{
"apiVersion": "authorization.k8s.io/v1",
"kind": "SubjectAccessReview",
"spec": {
"user": "alice@example.com",
"groups": ["system:authenticated"],
"extra": {
"authorization.kubernetes.io/cluster-name": ["<workspace-cluster-id>"]
},
"resourceAttributes": {
"verb": "create",
"group": "apps",
"version": "v1",
"resource": "deployments",
"namespace": "team-a",
"name": "demo"
}
}
}