diff --git a/docs/platform-engineer-guide/authorization/conditions.md b/docs/platform-engineer-guide/authorization/conditions.md new file mode 100644 index 00000000..be551560 --- /dev/null +++ b/docs/platform-engineer-guide/authorization/conditions.md @@ -0,0 +1,123 @@ +--- +title: Conditions +description: Restrict role grants by request attributes using CEL expressions on AuthzRoleBindings +sidebar_position: 4 +--- + +# Conditions + +A role binding answers _who_, _what_, and _where_. **Conditions** add a fourth constraint — _under what circumstances_. For example, you can grant a developer permission to manage release bindings in the `crm` project, but only when the target environment is `dev` or `staging` — keeping production off-limits. + +Conditions are optional. Omit them and the role mapping behaves like any other RBAC grant — every action the role grants applies within the binding's scope. + +## Condition Structure + +A condition has two parts: a list of **actions** it applies to, and an **expression** that decides whether those actions are permitted. You can attach conditions to a role mapping either in YAML (the `conditions` field on `AuthzRoleBinding` / `ClusterAuthzRoleBinding`) or through the Access Control UI in Backstage. + +| Field | Type | Required | Description | +| ------------ | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `actions` | string[] | Yes | Action patterns this condition applies to — the entry's `expression` is attached to each listed action. Supports exact matches and wildcards. | +| `expression` | string | Yes | A CEL expression that must evaluate to `true` for the action to be permitted by this role mapping. | + +Action patterns follow the same wildcard rules used elsewhere in OpenChoreo RBAC: + +- `releasebinding:create` — a single concrete action +- `releasebinding:*` — every action on the `releasebinding` resource +- `*` — every action in the system + +Only entries whose `actions` match the request contribute to the decision. If a mapping has conditions but none target the requested action, the condition check is skipped for that action. + +## Available Attributes + +CEL expressions reference a predefined set of attributes. Each attribute is registered against the specific actions where it is meaningful; a binding that references an attribute on an action that does not support it will be rejected at creation time. + +Currently the following attributes are available — more will be added in future releases: + +| Attribute | Type | Available on | Description | +| ----------------------------------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | +| `resource.environment`
[(dual-scoped)](#resource-identifiers-dual-scoped) | string | `releasebinding:create`, `releasebinding:view`, `releasebinding:update`, `releasebinding:delete`, `logs:view`, `metrics:view`, `traces:view` | Environment associated with the resource (e.g., `acme/prod`). | + +When a condition lists multiple actions — whether explicitly (`["releasebinding:create", "logs:view"]`) or via a wildcard pattern (`releasebinding:*`) — the expression may only reference attributes registered for **every** action the entry covers. An attribute supported by only some of those actions is not usable in the condition. + +### Resource Identifiers (Dual-Scoped) + +Some resource kinds in OpenChoreo come in two variants — one namespace-scoped, one cluster-scoped. In conditions, both variants share a single logical name (such as `environment`). Conditions don't pick the variant by kind; they pick it by the **shape of the identifier**. + +Attributes that identify such a resource (such as `resource.environment`) carry one of two forms: + +- For the namespace-scoped variant: `{namespace}/{name}` — for example, `acme/prod`. +- For the cluster-scoped variant: just `{name}` — for example, `prod`. + +Match the same form in your CEL expression: `resource.environment == "acme/prod"` targets a namespace-scoped environment named `prod` in `acme`, while `resource.environment == "prod"` targets the cluster-scoped one. + +For resources that exist in only one scope, the resource identifiers simply carry the resource name. + +## How Conditions Affect the Authorization Decision + +For a role mapping to permit a request, four things must all be true: + +1. The subject matches the binding's entitlement. +2. The target resource is within the binding's scope. +3. The role lists the requested action (exactly or via a wildcard). +4. The condition (if any) evaluates to `true`. + +A mapping with no `conditions` skips step four. A mapping with conditions that don't target the request action also skips step four — steps one through three still apply. + +Aggregation across bindings is unchanged. A request is **allowed** only if at least one matching binding has `effect: allow` and no matching binding has `effect: deny`. See [How OpenChoreo RBAC determines access](./overview.md#how-openchoreo-rbac-determines-access) for the full algorithm. + +:::note +If a condition expression cannot be evaluated cleanly at runtime, OpenChoreo treats it as failing closed — see [Fail-Closed Evaluation](./overview.md#fail-closed-evaluation). +::: + +### Multiple Entries on the Same Mapping + +A single role mapping can carry multiple `conditions` entries. Among the entries whose `actions` match the request action, the expressions are combined with **OR** — at least one entry must evaluate to `true` for the role mapping to permit the action: + +```yaml +conditions: + - actions: ["releasebinding:view"] + expression: 'resource.environment == "dev"' + - actions: ["releasebinding:view"] + expression: 'resource.environment == "staging"' +``` + +This binding permits the `releasebinding:view` actions when the target environment is either `dev` or `staging`. Combining alternatives in one entry with CEL's `in` operator (`resource.environment in ["dev", "staging"]`) is equivalent and usually clearer. + +## Examples + +A platform engineer needs to give the `backend-team` group `developer` access — but with two safety rails: release-binding mutations must stay out of production, and log access should be limited to `dev` and `staging`. A single role mapping can carry both rules, one condition per action group: + +```yaml +apiVersion: openchoreo.dev/v1alpha1 +kind: AuthzRoleBinding +metadata: + name: backend-team-binding + namespace: acme +spec: + entitlement: + claim: groups + value: backend-team + roleMappings: + - roleRef: + kind: AuthzRole + name: developer + conditions: + - actions: + - releasebinding:create + - releasebinding:update + - releasebinding:delete + expression: 'resource.environment != "acme/prod"' + - actions: + - logs:view + expression: 'resource.environment in ["acme/dev", "acme/staging"]' + effect: allow +``` + +Read-only actions on `releasebinding` (e.g., `releasebinding:view`) and every other action in the `developer` role remain unrestricted — only the listed actions are gated. + +## Related Reading + +- [Authorization Overview](./overview.md) — Subjects, scopes, actions, and the full evaluation model +- [Custom Roles and Bindings](./custom-roles.mdx) — Walkthrough of role and binding management in Backstage +- [AuthzRoleBinding API Reference](../../reference/api/platform/authzrolebinding.md) — Field reference for namespace-scoped role bindings +- [ClusterAuthzRoleBinding API Reference](../../reference/api/platform/clusterauthzrolebinding.md) — Field reference for cluster-scoped role bindings diff --git a/docs/platform-engineer-guide/authorization/custom-roles.mdx b/docs/platform-engineer-guide/authorization/custom-roles.mdx index 4470a59b..b5ee1979 100644 --- a/docs/platform-engineer-guide/authorization/custom-roles.mdx +++ b/docs/platform-engineer-guide/authorization/custom-roles.mdx @@ -44,12 +44,12 @@ The Roles section has a dropdown to switch between **Cluster Roles** and **Names ## Creating a Role Binding -Role bindings connect a subject to one or more roles, each with its own scope. The UI uses a step-by-step wizard to guide you through the process. +Role bindings connect a subject to one or more roles, each with its own scope. The UI uses a step-by-step wizard on a dedicated page to guide you through the process — use the **Next** and **Back** buttons in the header to move between steps. 1. Go to **Settings → Access Control → Role Bindings** 2. Use the dropdown to select **Cluster Role Bindings** or **Namespace Role Bindings** - For namespace bindings, select the target namespace from the dropdown first -3. Click **New Cluster Role Binding** or **New Namespace Role Binding** +3. Click **New Cluster Role Binding** or **New Namespace Role Binding** to open the wizard page +#### Conditions (Optional) + +Each role mapping can carry one or more **conditions** that gate specific actions on attributes of the request — for example, allowing release-binding mutations only outside production. Conditions are configured inline while editing a role mapping, before you confirm it. + +1. With a role mapping open for editing, locate the **Conditions** section under the role and scope fields. +2. Click **Add condition** to create a new entry. The button is disabled until a role is selected and the role grants at least one action that supports conditions. +3. In the condition card: + - **Actions** — select one or more action patterns this condition applies to. Only actions granted by the role's action list are selectable. + - **Expression** — write a CEL expression that must evaluate to `true` for the listed actions to be permitted. + - **Available attributes** — chips below the Expression field list every attribute valid for your action selection. Click a chip to insert it at the cursor. + +Role binding wizard — condition card in edit mode + +4. Click the **checkmark** to confirm the condition, or **X** to discard it. + +Multiple conditions on the same mapping are combined with **OR** — at least one matching condition must evaluate to `true` for the action to be permitted. A mapping with no conditions, or whose conditions don't target the requested action, behaves as an unrestricted grant for that action. + +:::tip +Available attributes are filtered to those supported by **every** action you've selected. After the first action is picked, the picker hides actions with no overlap, and if the resulting selection has no shared attribute the **Expression** field is disabled until the conflict is removed. For the full attribute model and YAML reference, see [Conditions](./conditions.md). +::: + ### Step 3: Choose Effect and Name Select the effect: diff --git a/docs/platform-engineer-guide/authorization/images/role-binding-creation-allow-deny-selection.png b/docs/platform-engineer-guide/authorization/images/role-binding-creation-allow-deny-selection.png index 697ebf63..4b6d95e6 100644 Binary files a/docs/platform-engineer-guide/authorization/images/role-binding-creation-allow-deny-selection.png and b/docs/platform-engineer-guide/authorization/images/role-binding-creation-allow-deny-selection.png differ diff --git a/docs/platform-engineer-guide/authorization/images/role-binding-creation-conditions-editing.png b/docs/platform-engineer-guide/authorization/images/role-binding-creation-conditions-editing.png new file mode 100644 index 00000000..2c8021ca Binary files /dev/null and b/docs/platform-engineer-guide/authorization/images/role-binding-creation-conditions-editing.png differ diff --git a/docs/platform-engineer-guide/authorization/images/role-binding-creation-review-selection.png b/docs/platform-engineer-guide/authorization/images/role-binding-creation-review-selection.png index 30ee98bb..d5a9a5b6 100644 Binary files a/docs/platform-engineer-guide/authorization/images/role-binding-creation-review-selection.png and b/docs/platform-engineer-guide/authorization/images/role-binding-creation-review-selection.png differ diff --git a/docs/platform-engineer-guide/authorization/images/role-binding-creation-role-mapping-view.png b/docs/platform-engineer-guide/authorization/images/role-binding-creation-role-mapping-view.png index d21418d1..6515db9e 100644 Binary files a/docs/platform-engineer-guide/authorization/images/role-binding-creation-role-mapping-view.png and b/docs/platform-engineer-guide/authorization/images/role-binding-creation-role-mapping-view.png differ diff --git a/docs/platform-engineer-guide/authorization/images/role-binding-creation-role-mapping.png b/docs/platform-engineer-guide/authorization/images/role-binding-creation-role-mapping.png index c5f7d57f..8a898d7d 100644 Binary files a/docs/platform-engineer-guide/authorization/images/role-binding-creation-role-mapping.png and b/docs/platform-engineer-guide/authorization/images/role-binding-creation-role-mapping.png differ diff --git a/docs/platform-engineer-guide/authorization/images/role-binding-creation-subject-selection.png b/docs/platform-engineer-guide/authorization/images/role-binding-creation-subject-selection.png index 9581319a..6a9538f7 100644 Binary files a/docs/platform-engineer-guide/authorization/images/role-binding-creation-subject-selection.png and b/docs/platform-engineer-guide/authorization/images/role-binding-creation-subject-selection.png differ diff --git a/docs/platform-engineer-guide/authorization/overview.md b/docs/platform-engineer-guide/authorization/overview.md index cf7657ab..75905c75 100644 --- a/docs/platform-engineer-guide/authorization/overview.md +++ b/docs/platform-engineer-guide/authorization/overview.md @@ -100,6 +100,12 @@ Two key properties: Each role binding also carries an `effect` field — either `allow` or `deny` (default: `allow`). A `deny` binding is an explicit exception: it revokes access that would otherwise be granted by an `allow` binding at the same or a higher scope. See [How OpenChoreo RBAC determines access](#how-openchoreo-rbac-determines-access) for exactly how allow and deny bindings are combined. +### Conditions + +A role mapping can optionally carry **conditions** that further narrow when the mapping applies — for example, granting a developer permission to manage release bindings in the `crm` project, but only when the target environment is `dev` or `staging`, keeping production off-limits. + +If `conditions` is omitted, the role mapping behaves as a plain RBAC grant. For the full attribute model, evaluation semantics, and examples, see [Conditions on Role Bindings](./conditions.md). + ## How OpenChoreo RBAC determines access When a request arrives, OpenChoreo evaluates it against every role binding the subject matches. For each binding, all of the following must hold for the binding to apply: @@ -107,6 +113,7 @@ When a request arrives, OpenChoreo evaluates it against every role binding the s 1. **The subject matches.** One of the caller's entitlement values (e.g., `groups:platformEngineer`) equals the binding's subject. 2. **The resource is within scope.** The target resource lies at or below the binding's scope in the resource hierarchy. A binding at `namespace: acme` applies to everything inside `acme`; a `ClusterAuthzRoleBinding` with no scope applies cluster-wide. 3. **The role grants the action.** The role referenced by the binding lists the requested action, either exactly (`component:create`) or via a wildcard (`component:*`, `*`). +4. **Conditions are satisfied.** If the matching role mapping defines `conditions`, at least one entry whose `actions` cover the request action must evaluate to `true`. Mappings without conditions, or mappings whose conditions do not target the request action, satisfy this step automatically. A request is **allowed** only if: @@ -117,6 +124,10 @@ A single matching `deny` is enough to block the request, even when multiple `all Bindings default to `effect: allow`. Set `effect: deny` explicitly only when you need to create a targeted exception to a broader allow — for example, granting `developer` access across the `acme` namespace but denying it on the `secret` project within it. +### Fail-Closed Evaluation + +OpenChoreo evaluates authorization **fail-closed**: if any part of a binding cannot be evaluated cleanly — including malformed condition expressions or other corrupted policy state — `allow` bindings do not grant access and `deny` bindings still deny. A misconfiguration can never silently widen access; the conservative outcome wins. CRDs are validated by admission webhooks at create/update time, so most issues are caught before they ever reach evaluation. + ## Authorization CRDs OpenChoreo uses four CRDs to manage authorization. **Roles** define what actions are permitted, and **role bindings** connect subjects to those roles with a specific scope and effect. diff --git a/docs/reference/api/platform/authzrolebinding.md b/docs/reference/api/platform/authzrolebinding.md index f71f9b08..e6f137f5 100644 --- a/docs/reference/api/platform/authzrolebinding.md +++ b/docs/reference/api/platform/authzrolebinding.md @@ -42,12 +42,13 @@ metadata: ### RoleMapping -Each entry in the `roleMappings` array pairs a role reference with an optional scope. +Each entry in the `roleMappings` array pairs a role reference with an optional scope and optional attribute-based conditions. -| Field | Type | Required | Description | -| --------- | --------------------------- | -------- | ------------------------------------------------------------------------------- | -| `roleRef` | [RoleRef](#roleref) | Yes | Reference to the role to bind | -| `scope` | [TargetScope](#targetscope) | No | Narrows the mapping to a specific project or component. Omit for namespace-wide | +| Field | Type | Required | Description | +| ------------ | ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------- | +| `roleRef` | [RoleRef](#roleref) | Yes | Reference to the role to bind | +| `scope` | [TargetScope](#targetscope) | No | Narrows the mapping to a specific project or component. Omit for namespace-wide | +| `conditions` | [AuthzCondition[]](#authzcondition) | No | Attribute-based restrictions on specific actions granted by the role. Omit for no restrictions | ### RoleRef @@ -69,6 +70,19 @@ All fields are optional. Omitted fields mean "all" at that level. `scope.component` requires `scope.project` to be set. This is enforced by a validation rule on the resource. ::: +### AuthzCondition + +Each entry in `conditions` gates a set of actions in the role mapping on a CEL expression evaluated against attributes of the request. + +| Field | Type | Required | Description | +| ------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `actions` | string[] | Yes | Action patterns this condition applies to — the entry's expression is attached to each listed action. Supports exact matches and wildcards. | +| `expression` | string | Yes | A CEL expression that must evaluate to `true` for the action to be permitted by this role mapping. | + +Multiple entries on the same role mapping are combined with **OR** semantics — at least one entry whose `actions` cover the request action must evaluate to `true` for the action to be permitted. Entries whose `actions` do not match the request action do not contribute to the decision. + +For the full list of attributes available to expressions and the evaluation model, see [Conditions on Role Bindings](../../../platform-engineer-guide/authorization/conditions.md). + ## Examples ### Namespace-Wide Developer Access @@ -154,6 +168,33 @@ spec: effect: deny ``` +### Restrict Component Deployments to Non-Production Environments + +Use `conditions` on a role mapping to gate specific actions on request attributes. The binding below grants `developer` access to the `backend-team` group, but blocks release-binding mutations in production. See [Conditions](../../../platform-engineer-guide/authorization/conditions.md) for the full attribute model. + +```yaml +apiVersion: openchoreo.dev/v1alpha1 +kind: AuthzRoleBinding +metadata: + name: backend-team-binding + namespace: acme +spec: + entitlement: + claim: groups + value: backend-team + roleMappings: + - roleRef: + kind: AuthzRole + name: developer + conditions: + - actions: + - releasebinding:create + - releasebinding:update + - releasebinding:delete + expression: 'resource.environment != "acme/prod"' + effect: allow +``` + ## Allow and Deny Both `ClusterAuthzRoleBinding` and `AuthzRoleBinding` carry an **effect** field: either `allow` or `deny`. When multiple bindings match a request, the system follows a **deny-overrides** strategy: diff --git a/docs/reference/api/platform/clusterauthzrolebinding.md b/docs/reference/api/platform/clusterauthzrolebinding.md index cbfda67a..a2c7ffa4 100644 --- a/docs/reference/api/platform/clusterauthzrolebinding.md +++ b/docs/reference/api/platform/clusterauthzrolebinding.md @@ -41,12 +41,13 @@ metadata: ### ClusterRoleMapping -Each entry in the `roleMappings` array pairs a role reference with an optional scope. +Each entry in the `roleMappings` array pairs a role reference with an optional scope and optional attribute-based conditions. -| Field | Type | Required | Description | -| --------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------------- | -| `roleRef` | [RoleRef](#roleref) | Yes | Reference to the cluster role to bind | -| `scope` | [ClusterTargetScope](#clustertargetscope) | No | Narrows the mapping to a specific namespace, project, or component. Omit for cluster-wide | +| Field | Type | Required | Description | +| ------------ | ----------------------------------------- | -------- | ---------------------------------------------------------------------------------------------- | +| `roleRef` | [RoleRef](#roleref) | Yes | Reference to the cluster role to bind | +| `scope` | [ClusterTargetScope](#clustertargetscope) | No | Narrows the mapping to a specific namespace, project, or component. Omit for cluster-wide | +| `conditions` | [AuthzCondition[]](#authzcondition) | No | Attribute-based restrictions on specific actions granted by the role. Omit for no restrictions | ### RoleRef @@ -71,6 +72,19 @@ All fields are optional. Omitted fields mean "all" at that level. - `scope.project` requires `scope.namespace`, and `scope.component` requires `scope.project`. ::: +### AuthzCondition + +Each entry in `conditions` gates a set of actions in the role mapping on a CEL expression evaluated against attributes of the request. + +| Field | Type | Required | Description | +| ------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `actions` | string[] | Yes | Action patterns this condition applies to — the entry's expression is attached to each listed action. Supports exact matches and wildcards. | +| `expression` | string | Yes | A CEL expression that must evaluate to `true` for the action to be permitted by this role mapping. | + +Multiple entries on the same role mapping are combined with **OR** semantics — at least one entry whose `actions` cover the request action must evaluate to `true` for the action to be permitted. Entries whose `actions` do not match the request action do not contribute to the decision. + +For the full list of attributes available to expressions and the evaluation model, see [Conditions](../../../platform-engineer-guide/authorization/conditions.md). + ## Examples ### Grant Admin Access Cluster-Wide @@ -136,6 +150,31 @@ spec: In this example, `acme-admins` gets full `admin` access scoped to the `acme` namespace and cluster-wide read-only visibility into cluster-level resources — all in a single CR. +### Restrict Observability Reads to Lower Environments + +Use `conditions` on a role mapping to gate specific actions on request attributes. The binding below grants the `dashboard-readonly` service account `observability-reader` access cluster-wide, but limits log, metric reads to `dev` and `staging`. See [Conditions](../../../platform-engineer-guide/authorization/conditions.md) for the full attribute model. + +```yaml +apiVersion: openchoreo.dev/v1alpha1 +kind: ClusterAuthzRoleBinding +metadata: + name: lower-env-observability-binding +spec: + entitlement: + claim: sub + value: dashboard-readonly + roleMappings: + - roleRef: + kind: ClusterAuthzRole + name: observability-reader + conditions: + - actions: + - logs:view + - metrics:view + expression: 'resource.environment in ["acme/dev", "acme/staging"]' + effect: allow +``` + ## Allow and Deny Both `ClusterAuthzRoleBinding` and `AuthzRoleBinding` carry an **effect** field: either `allow` or `deny`. When multiple bindings match a request, the system follows a **deny-overrides** strategy: diff --git a/sidebars.ts b/sidebars.ts index 0f8cad33..d351b0d2 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -165,6 +165,11 @@ const sidebars: SidebarsConfig = { id: "platform-engineer-guide/authorization/custom-roles", label: "Custom Roles and Bindings", }, + { + type: "doc", + id: "platform-engineer-guide/authorization/conditions", + label: "Conditions", + }, ], }, {