Filtering and self-review
Start with the Access management overview for the tenancy model and role tiers. Identity concepts come from the Identity overview.
Instead of an all-or-nothing "allowed / denied" answer, Hub computes a predicate from your grants and returns only the rows you may see. This page explains how the predicate is built and how to inspect it before writing against it.
Two grant sources combine with OR
A listing request inside a realm draws visibility from two independent sources:
- Realm roles.
realm-admin,realm-editor, andrealm-viewerapply to every control plane in the realm, regardless of the control plane'sspec.identityProviders.realm-editorandrealm-viewermap to the aggregation ClusterRoles — bothcrossplane-editandcontrolplane-editfor an editor, bothcrossplane-viewandcontrolplane-viewfor a viewer — and whichever of those the control plane actually has contribute their rules.realm-adminis unconditional: it sees every resource thehub-connectorsynced, bounded by the connector's sync scope rather than by a ClusterRole. See What a realm role gets inside a control plane. - Per-control-plane RBAC. Each
ControlPlanecan declare anidentityProviderslist. Each entry says "identities from thisIdentityProvidershould be looked up inside this control plane'sClusterRoleBindings andRoleBindings, with the following mapping". If your identity matches an entry, the control plane's own RBAC is evaluated against your mapped username and groups; whatever it grants is added to your visibility for that control plane.
The two sources combine with OR: you see a row if any of your grants
admits it. A realm-viewer in realm A already sees whatever the
crossplane-view / controlplane-view role exposes on every control plane
in A. If prod in A also trusts the entra IdentityProvider and has a
ClusterRoleBinding granting crossplane-edit to your user, your
visibility for prod becomes read+write, while the rest of A stays at the
view projection.
Per-CP RBAC via spec.identityProviders is purely additive: it grants
extra privileges on top of what the realm role already provides. A control
plane that lists no IdPs still gives realm-viewers and realm-editors the
base aggregation projection; identityProviders isn't required for realm
roles to work.
The same predicate drives both the actual listing and the answer to a
SelfSubjectAccessReview, which is why "ask SelfSubjectAccessReview what
you would see" is always trustworthy.
Pass Hub identities through to a control plane
By default, a control plane's Kubernetes RBAC applies to whatever identities
the cluster's own authenticator produces — typically service accounts on
that cluster. If you want the control plane's RoleBinding and
ClusterRoleBinding resources to recognize the same prefixed identities
Hub uses (corp:alice@example.com, corp:platform-admins), you tell
Hub to forward the resolved identity through unchanged.
Set userMappingType: Exact on the ControlPlane, referencing the
IdentityProvider whose identities should pass through:
apiVersion: hub.upbound.io/v1beta1
kind: ControlPlane
metadata:
name: production
namespace: default
spec:
identityProviders:
- name: corp # matches an IdentityProvider name
userMappingType: Exact # forward the resolved identity 1:1
With Exact mapping, the control plane sees corp:alice@example.com (and
groups such as corp:platform-admins) in its Kubernetes user.Info and
evaluates its own RBAC against those names.
Now you can write a control plane ClusterRole and ClusterRoleBinding
that Hub-authenticated users match:
# Applied inside the control plane.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: rds-viewer
rules:
- apiGroups:
- database.aws.upbound.io
resources:
- rdsinstances
verbs:
- list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: alice-rds-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: rds-viewer
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: "corp:alice@example.com"
Alice can now list rdsinstances on this control plane whether she reaches
the API through kubectl directly or through Hub — the RBAC rule fires the
same way in both cases.
When to use pass-through
Reach for userMappingType: Exact when:
- You want a uniform mental model of access across Hub and every managed control plane.
- You already have (or want to write) fine-grained control-plane RBAC and don't want to duplicate the grants at the Hub tier.
- Your
IdentityProviderand control-plane authenticator issue the same prefixed identities (which is whatuserInfoPrefixgives you).
Stick with the default (no identityProviders entry for this IdP) when:
- Your control planes authenticate against a completely different provider or against the host cluster only. In that case, bind Kubernetes-level roles against whatever identities the control plane's own authenticator produces, and rely on Hub's realm-role projection (see access-management overview) for coarse access from the Hub side.
userMappingType currently supports only Exact. Additional mapping
modes (translated names, group-only propagation, etc.) will be added
here as they land.
Inspect your grants with SelfSubjectAccessReview
SelfSubjectAccessReview answers "can this caller perform this verb on
this resource?" without actually performing it. It is the canonical way to
debug "why doesn't my list show anything?".
POST /apis/authorization.hub.upbound.io/v1beta1/selfsubjectaccessreviews
Authorization: Bearer <hub-token>
Content-Type: application/json
{
"apiVersion": "authorization.hub.upbound.io/v1beta1",
"kind": "SelfSubjectAccessReview",
"spec": {
"hubResourceRequest": {
"group": "hub.upbound.io",
"version": "v1beta1",
"resource": "resources",
"verb": "list",
"realm": "prod"
}
}
}
version, resource, and verb are required and must be fully qualified —
no wildcards. subresource and name are optional and narrow the review
further.
group is optional. Omitting it, or sending "", selects the Kubernetes
core API group. That's the same convention Kubernetes itself uses, where
"" names the group that holds Pods, Services, and Namespaces — it isn't a
wildcard and doesn't mean "any group".
Hub serves exactly one resource in the core group: namespaces, a
read-only facade over realms. It exists so realm-scoped resources can set
metadata.namespace=<realm-name>, and reviewing it is how you check whether
a caller can enumerate realms through that path:
{
"spec": {
"hubResourceRequest": {
"group": "",
"version": "v1",
"resource": "namespaces",
"verb": "list"
}
}
}
The facade serves get and list only, so a review of create, update,
or delete on namespaces comes back Deny for every caller regardless of
role. Realms themselves are writable through hub.upbound.io/realms.
Every other Hub resource lives under a named group — hub.upbound.io,
authorization.hub.upbound.io, authentication.hub.upbound.io, and the
feature-gated catalog.hub.upbound.io, metrics.hub.upbound.io, and
registry.hub.upbound.io among them — so those reviews always carry an
explicit group.
realm distinguishes three cases that Kubernetes SubjectAccessReview
conflates into one:
realm | Means |
|---|---|
| Omitted (null) | Cluster-scoped resources only |
"" | Every realm of a realm-scoped resource |
"prod" | That realm |
The response
status.decision carries the outcome and is always populated. It's one of
three values:
Allow. Nostatus.filter. Every row passes.Deny. Nostatus.filter. No grant applies; the request would be rejected.Filtered.status.filterholds the row-level predicate. The request is admitted, and applying that predicate to a list of the requested resource returns exactly the rows you can see.
Two optional fields accompany the decision. status.reason is the
authorizer's human-readable explanation. status.error carries an error
message, and can appear alongside any decision.
The filter expression
status.filter is a recursive tree. Each node is an object with exactly
one of these four keys set:
| Key | Shape | Asserts |
|---|---|---|
and | {"parts": [<node>, ...]} | Every operand holds. At least two operands. |
or | {"parts": [<node>, ...]} | Some operand holds. At least two operands. |
stringVarIn | {"variable": "<name>", "values": ["a", "b"]} | The variable's value is in the set. At least one value. |
boolEq | {"variable": "<name>", "value": true} | The boolean variable equals the literal. |
There is no equality variant for strings. An equality constraint arrives as
a stringVarIn whose values list holds a single element.
variable is one of kind, group, apiVersion, apiResource, name,
namespace, controlPlane, space, realm, owner, or shared — the
same variable names the CEL filter parameter accepts on list endpoints.
The tree never contains a boolean constant. An always-true predicate is
reported as decision: "Allow" with no filter, and an always-false one as
decision: "Deny".
A Filtered response for a caller who can see all of realm prod, plus
the apps and extensions groups in realm staging:
{
"status": {
"decision": "Filtered",
"filter": {
"or": {
"parts": [
{
"stringVarIn": { "variable": "realm", "values": ["prod"] }
},
{
"and": {
"parts": [
{ "stringVarIn": { "variable": "realm", "values": ["staging"] } },
{ "stringVarIn": { "variable": "group", "values": ["apps", "extensions"] } }
]
}
}
]
}
}
}
}
Reading a filter tells you exactly what you can reach: a
stringVarIn on controlPlane with values: ["cp-1"] means cp-1 is the
only control plane you see. If the decision is Allow but the list is
still empty, the grant isn't the problem — the underlying data isn't there.
Verifying who you are
SelfSubjectAccessReview answers "what am I allowed to do?".
SelfSubjectReview answers "who does Hub think I am?" (identity, not
authorization). Reach for SelfSubjectReview first:
kubectl auth whoami
See Verifying your identity
for interpreting the output. If the username or groups are wrong, the fix
is on the IdentityProvider side; if they look right and access is still
off, use SelfSubjectAccessReview above.