Skip to content

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
Hold "Alt" / "Option" to enable pan & zoom

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-addr in pkg/config/config.go
  • Orgs Store: Resolved at startup by ListStores(Name: "orgs") in cmd/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 (._) via pkg/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:

core_namespace:<cluster>/<namespace>  parent  core_platform-mesh_io_account:<origin>/<name>

If verb is not create/list/watch:

<resource-object>  parent  core_namespace:<cluster>/<namespace>

For cluster-scoped resources:

<resource-object>  parent  core_platform-mesh_io_account:<origin>/<name>

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
Hold "Alt" / "Option" to enable pan & zoom
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"
    }
  }
}

SubjectAccessReview Response (Webhook → kcp)

{
  "apiVersion": "authorization.k8s.io/v1",
  "kind": "SubjectAccessReview",
  "status": {
    "allowed": true,
    "denied": false
  }
}