design/design-doc-rbac-manager.md
Crossplane, as a project, consists of three key building blocks:
Each provider is a distinct process that is typically deployed as a pod. The API extensions and package manager controllers are part of the 'core' Crossplane controller manager process. The core controller manager is therefore responsible for extending Crossplane. Its controllers add remove Custom Resource Definitions (CRDs) to and from the API server. The core Crossplane controllers define custom resources (CRs) that represent:
These controllers are novel in that they define custom resources and start
controllers in response to the creation of other custom resources. Creating a
Provider custom resource, for example, would cause the package manager to
deploy a new provider controller manager, and to define the custom resources
that it can reconcile.
When Crossplane defines new custom resources some entities (or subjects, in Kubernetes RBAC terminology) must be granted access to those resources in order for them to be useful. These subjects can be categorised as software, or users.
Software, in this context, corresponds to the service accounts that Crossplane
or a provider run as. When the package manager reconciles a Provider, it runs
the provider pod as a certain service account. The provider cannot reconcile its
managed resources unless its service account is granted access to do so.
Furthermore, the core Crossplane controllers cannot create the provider's
managed resources, and thus cannot compose them into a higher level resource.
Users are the people who use Crossplane to orchestrate external systems. Crossplane typically categorises these people into three roles, which may overlap:
These people are unable to configure and use Crossplane to orchestrate the systems they wish to orchestrate unless they (i.e. their RBAC user or group) are granted access to perform the tasks they need to perform. Furthermore, no one is able to grant this access because the API server requires that a subject be granted a particular RBAC role before they are able to grant that role to another subject.
In either case - software or user - it is possible for a superuser to manually
author the appropriate RBAC roles and bind them to the appropriate subjects to
make Crossplane work. A superuser in this context is either a subject bound to
the built in cluster-admin RBAC ClusterRole, or a subject bound to a role
that grants the escalate verb on RBAC Roles and/or ClusterRoles. Manually
authoring and binding RBAC roles is the most flexible and secure approach, but
it is onerous and often boilerplate. Kubernetes addresses this by offering
admin, edit, and view RBAC ClusterRoles that when bound grant varying
levels of broad access to the majority of the resources an API server supports.
The goal of this document is to allow Crossplane to dynamically manage (and in some cases bind) RBAC roles that grant subjects access to use Crossplane as it is extended by installing providers and defining composite resources.
Crossplane should be able to create and bind RBAC roles that grant:
Crossplane should also create (but not bind) RBAC roles that grant:
Finally, it should be possible to opt-out of RBAC role management. This allows Crossplane to be run without any special privileges (i.e. without the ability to escalate its own privileges) at the expense of deferring RBAC role management to platform operators.
This document proposes that Crossplane introduce an 'RBAC manager'. The RBAC
manager would run as a distinct pod, and would be responsible for creating and
binding the RBAC roles Crossplane and its users need. This allows Crossplane and
its providers run as service accounts that are limited to a specific set of
resources and verbs (e.g. create, watch, etc). The RBAC manager would also run
as a service account limited to a specific set of resources and verbs, but would
be granted the special escalate and bind on the Role and ClusterRole
resources, thus allowing it to grant access that it does not have.
Privilege escalation is an unfortunate by-product of a controller manager that may dynamically define new custom resources. Centralising the ability to escalate privileges into the RBAC manager has two nice properties:
The RBAC manager will be responsible for the management of several cluster
roles, using ClusterRole aggregation to form a set
of cluster roles that may change over time as providers and composite resources
are added to and removed from the API server. The RBAC manager would manage the
following cluster roles. Note that these roles are aggregated - they consist of
several more granular roles that are not covered here. The following examples
are 'flattened' aggregations represented as a single fixed ClusterRole to
demonstrate specifically what permissions each role will have.
The Crossplane ClusterRole is bound to the service account the Crossplane pod
runs as. It grants the following rules:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane
rules:
# Crossplane creates events to report its status.
- apiGroups: [""]
resources: [events]
verbs: [create]
# Crossplane manages the secrets of its composite resources. It never explicitly
# deletes secrets, but instead relies on garbage collection.
- apiGroups: [""]
resources: [secrets]
verbs: [get, create, update]
# Crossplane creates CRDs in response to the creation of XRDs.
- apiGroups: [apiextensions.k8s.io]
resources: [customresourcedefinitions]
verbs: [get, create, update, delete]
# Crossplane has full access to the types it defines.
- apiGroups:
- apiextensions.crossplane.io
- pkg.crossplane.io
resources: ["*"]
verbs: ["*"]
# Crossplane has full access to the composite resources and claims it defines.
- apiGroups: [xr.example.org]
resources:
- examplecomposites
- examplecomposites/status
- exampleclaims
- exampleclaims/status
verbs: ["*"]
# Crossplane has access to the provider resource that it may need to compose.
- apiGroups: [provider.example.org]
resources:
- examplemanageds
- exampleproviderconfigs
verbs: ["*"]
---
# Crossplane uses a fixed (at runtime) namespace to unpack packages and run
# providers. This role allows it to do so. Note that it is included for
# illustrative purposes; it is static and is thus managed by the Crossplane Helm
# chart rather than the RBAC manager.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: crossplane-system
name: crossplane
rules:
# Crossplane uses jobs to unpack packages.
- apiGroups: [batch, extensions]
resources: [jobs]
verbs: [get, create, update, delete]
# Crossplane reads pod logs to unpack packages.
- apiGroups: [""]
resources: [pods]
verbs: [get, list]
- apiGroups: [""]
resources: [pods/log]
verbs: [get]
# Crossplane uses deployments to run providers.
- apiGroups: [extensions, apps]
resources: [deployments]
verbs: [list, watch, get, create, update, delete]
# Crossplane manages service accounts for providers.
- apiGroups: [""]
resources: [serviceaccounts]
verbs: [list, watch, get, create, update, delete]
A Crossplane provider's ClusterRole is bound to the service account the
provider pod runs as. It grants the following rules:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: example-provider
rules:
# Providers create events to report their status.
- apiGroups: [""]
resources: [events]
verbs: [create]
# Providers read their credentials from secrets. They may also create and update
# secrets containing managed resource connection details.
- apiGroups: [""]
resources: [secrets]
verbs: [get, create, update]
# Providers have access to the resources they define.
- apiGroups: [provider.example.org]
resources:
- examplemanageds
- examplemanageds/status
- exampleproviderconfigs
- exampleproviderconfigs/status
verbs: [get, list, watch, update, patch]
Crossplane's user-facing cluster roles are inspired by the user-facing roles
of Kubernetes - admin, edit, and view, albeit with a few differences:
cluster-admin cluster role, which is intended
to be granted at cluster scope via a cluster role binding, from the admin,
edit, and view cluster roles, which are intended to be granted within a
particular namespace using a role binding. Crossplane does not make this
distinction. The crossplane-admin, crossplane-edit, and crossplane-view
roles are intended to be granted at the cluster scope, and thus grant admin,
edit, or view access at the cluster scope.browse role, which grants view access to composite
resources and compositions (not providers and managed resources). This role
may be granted to subjects who can author resource claims in a namespace (or
namespaces), and who should be able to browse the compositions or composite
resources that they may reference or select.The crossplane-admin role is automatically bound to the crossplane:masters
group for convenience. It grants the following rules:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane-admin
rules:
# Crossplane administrators have access to view events.
- apiGroups: [""]
resources: [events]
verbs: [get, list, watch]
# Crossplane administrators must create provider credential secrets, and may
# need to read or otherwise interact with connection secrets. They may also need
# to create or annotate namespaces.
- apiGroups: [""]
resources: [secrets, namespaces]
verbs: ["*"]
# Crossplane administrators have access to view the cluster roles that they may
# be able to grant to other subjects.
- apiGroups: [rbac.authorization.k8s.io]
resources: [clusterroles]
verbs: [get, list, watch]
# Crossplane administrators have access to grant the access they have to other
# subjects.
- apiGroups: [rbac.authorization.k8s.io]
resources: [clusterrolebindings, rolebindings]
verbs: [*]
# Crossplane administrators have full access to built in Crossplane types.
- apiGroups:
- apiextensions.crossplane.io
- pkg.crossplane.io
resources: ["*"]
verbs: ["*"]
# Crossplane administrators have full access to all of the composite resources
# and claims it defines.
- apiGroups: [xr.example.org]
resources:
- examplecomposites
- examplecomposites/status
- exampleclaims
- exampleclaims/status
verbs: ["*"]
# Crossplane administrators have full access to all resources defined by
# installed providers.
- apiGroups: [provider.example.org]
resources:
- examplemanageds
- exampleproviderconfigs
verbs: ["*"]
The crossplane-edit role is identical to crossplane-admin, sans the ability
to grant others access by managing role bindings.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane-edit
rules:
# Crossplane editors have access to view events.
- apiGroups: [""]
resources: [events]
verbs: [get, list, watch]
# Crossplane editors must create provider credential secrets, and may need to
# read or otherwise interact with connection secrets.
- apiGroups: [""]
resources: [secrets]
verbs: ["*"]
# Crossplane editors may see which namespaces exist, but not edit them.
- apiGroups: [""]
resources: [namespaces]
verbs: [get, list, watch]
# Crossplane editors have full access to built in Crossplane types.
- apiGroups:
- apiextensions.crossplane.io
- pkg.crossplane.io
resources: ["*"]
verbs: ["*"]
# Crossplane editors have full access to all of the composite resources and
# claims it defines.
- apiGroups: [xr.example.org]
resources:
- examplecomposites
- examplecomposites/status
- exampleclaims
- exampleclaims/status
verbs: ["*"]
# Crossplane viewers have full access to all resources defined by installed
# providers.
- apiGroups: [provider.example.org]
resources:
- examplemanageds
- exampleproviderconfigs
verbs: ["*"]
The crossplane-view role allows read-only access to all Crossplane resources.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane-view
rules:
# Crossplane viewers have access to view events.
- apiGroups: [""]
resources: [events]
verbs: [get, list, watch]
# Crossplane viewers may see which namespaces exist.
- apiGroups: [""]
resources: [namespaces]
verbs: [get, list, watch]
# Crossplane viewers have read-only access to built in Crossplane types.
- apiGroups:
- apiextensions.crossplane.io
- pkg.crossplane.io
resources: ["*"]
verbs: [get, list, watch]
# Crossplane viewers have read-only access to all of the composite resources and
# claims it defines.
- apiGroups: [xr.example.org]
resources:
- examplecomposites
- examplecomposites/status
- exampleclaims
- exampleclaims/status
verbs: [get, list, watch]
# Crossplane viewers have read-only access to all resources defined by installed
# providers.
- apiGroups: [provider.example.org]
resources:
- examplemanageds
- exampleproviderconfigs
verbs: [get, list, watch]
The crossplane-browse role allows read-only access to composite resources and
compositions.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane-browse
rules:
# Crossplane browsers have access to view events.
- apiGroups: [""]
resources: [events]
verbs: [get, list, watch]
# Crossplane browsers have read-only access to compositions and XRDs. This
# allows them to discover and select an appropriate composition when creating a
# resource claim.
- apiGroups:
- apiextensions.crossplane.io
resources: ["*"]
verbs: [get, list, watch]
# Crossplane browsers have read-only access to all of the composite resources it
# defines. This allows them to claim a statically provisioned composite
# resource.
- apiGroups: [xr.example.org]
resources:
- examplecomposites
verbs: [get, list, watch]
The RBAC manager creates admin, edit, and view roles for each namespace.
Maintaining roles for each namespace allows the specific access that these roles
grant to vary from namespace to namespace. A platform operator who has been
granted the crossplane-admin role may influence which resource claims are
available to a particular namespaces by annotating the namespace (see Composite
Resource ClusterRole Mechanics for details).
The crossplane-admin role grants the following rules:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
# This role applies only to the 'example' namespace.
namespace: example
name: crossplane-admin
rules:
# Crossplane namespace admins have access to view events.
- apiGroups: [""]
resources: [events]
verbs: [get, list, watch]
# Crossplane namespace admins may need to read or otherwise interact with
# resource claim connection secrets.
- apiGroups: [""]
resources: [secrets]
verbs: ["*"]
# Crossplane namespace admins have access to view the cluster roles that they
# may be able to grant to other subjects.
- apiGroups: [rbac.authorization.k8s.io]
resources: [roles]
verbs: [get, list, watch]
# Crossplane namespace admins have access to grant the access they have to other
# subjects.
- apiGroups: [rbac.authorization.k8s.io]
resources: [rolebindings]
verbs: [*]
# Crossplane namespace admins have full access to all of the composite resource
# claims that an admin has chosen to enable in their namespace.
- apiGroups: [xr.example.org]
resources:
- exampleclaims
verbs: ["*"]
The crossplane-edit role is identical to crossplane-admin, sans the ability
to grant others access by managing role bindings.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
# This role applies only to the 'example' namespace.
namespace: example
name: crossplane-edit
rules:
# Crossplane namespace editors have access to view events.
- apiGroups: [""]
resources: [events]
verbs: [get, list, watch]
# Crossplane namespace editors may need to read or otherwise interact with
# resource claim connection secrets.
- apiGroups: [""]
resources: [secrets]
verbs: ["*"]
# Crossplane editors have full access to all of the composite resource claims
# that an admin has chosen to enable in their namespace.
- apiGroups: [xr.example.org]
resources:
- exampleclaims
verbs: ["*"]
The crossplane-view role grants read-only access to the resource claims that
are available to said namespace:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
# This role applies only to the 'example' namespace.
namespace: example
name: crossplane-view
rules:
# Crossplane namespace viewers have access to view events.
- apiGroups: [""]
resources: [events]
verbs: [get, list, watch]
# Crossplane namespace viewers have read-only access to all of the composite
# resource claims that an admin has chosen to enable in their namespace.
- apiGroups: [xr.example.org]
resources:
- exampleclaims
verbs: [get, list, watch]
Each of the RBAC roles that Crossplane manages is
in fact an aggregation of several cluster roles. Typically each role will be an
aggregation of a 'base' role - a fixed set of rules - and zero or more roles
that are created by the RBAC manager in response to the installation of a
provider, or the definition of a composite resource. The crossplane-admin
cluster role might be an aggregation of the following roles:
crossplane:aggregate-to-admincrossplane:provider:7f63e3661:aggregate-to-editcrossplane:composite:examplecomposites.xr.example.org:aggregate-to-editFor example the below crossplane:aggregate-to-admin role aggregates to the
user-facing crossplane-admin role.
---
# The 'user-facing' crossplane-admin role.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane-admin
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.crossplane.io/aggregate-to-crossplane-admin: "true"
---
# The 'base' role containing the fixed rules that always aggregate to the above
# crossplane-admin cluster role.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane:aggregate-to-admin
labels:
rbac.crossplane.io/aggregate-to-crossplane-admin: "true"
rules:
- apiGroups: [""]
resources: [events]
verbs: [get, list, watch]
- apiGroups: [""]
resources: [secrets, namespaces]
verbs: ["*"]
- apiGroups: [rbac.authorization.k8s.io]
resources: [clusterroles]
verbs: [get, list, watch]
- apiGroups: [rbac.authorization.k8s.io]
resources: [clusterrolebindings, rolebindings]
verbs: [*]
- apiGroups:
- apiextensions.crossplane.io
- pkg.crossplane.io
resources: ["*"]
verbs: ["*"]
The cluster roles intended to be bound at the cluster scope and their base roles
can be created outside of Crossplane - typically by Crossplane's Helm chart. All
other roles that aggregate to crossplane-admin are created as required by the
RBAC manager. Roles that may only be bound at the namespace scope on the other
hand must be created by the RBAC manager, though their base roles can be created
outside of Crossplane.
The naming convention established by this design is that user-facing roles use
hyphen separated names, while system facing and aggregated roles use colon
user-facing role. There is only one crossplane:aggregate-to-admin base role
and only separated names. Note that aggregation may be transitive. For example
the crossplane-admin role is identical to the crossplane-edit role except
for a few extra rules in its base role. Provider and composite resource roles
can therefore aggregate only to crossplane-edit, which in turn aggregates to
crossplane-admin.
When a composite resource is defined the RBAC manager will create two cluster
roles. For example creating an XRD named composites.example.org would trigger
the creation of the following roles:
crossplane:composite:composites.example.org:aggregate-to-editcrossplane:composite:composites.example.org:aggregate-to-viewThe aggregate-to-edit role aggregates to the crossplane, crossplane-admin,
and crossplane-edit cluster roles, as well as any namespaced edit role for a
namespace in which the composite resource may claimed. The aggregate-to-view
cluster role aggregates to the crossplane-view cluster role, and any view
role for a namespace in which the composite resource may be claimed. Below is an
example of an edit composite resource cluster role.
# This ClusterRole aggregates up to any namespaced role that declares
# compatibility by including the XRD label. The same role also aggregates to the
# crossplane-edit role (and thus transitively to the crossplane-admin role).
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane:composite:examplecomposites.xr.example.org:aggregate-to-edit
labels:
rbac.crossplane.io/aggregate-to-edit: "true"
rbac.crossplane.io/aggregate-to-ns-edit: "true"
rbac.crossplane.io/xrd: examplecomposites.xr.example.org
rules:
- apiGroups: [xr.example.org]
resources:
- exampleclaims
- exampleclaims/status
# Composite resources are cluster scoped. Binding this cluster role at the
# namespace scope will not actually grant access to these composite resources.
# Including them in this cluster role allows one crossplane:composite role to
# aggregate to both the crossplane-ns-example-edit and crossplane-edit roles.
- examplecomposites
- examplecomposites/status
verbs: ["*"]
Composite resources may be claimed in any namespace that has an annotation with
the key rbac.crossplane.io/composites.example.org, set to xrd-claim-accepted
(where composites.example.org is the name of the XRD). When the RBAC manager
encounters a namespace with one or more such annotations, it creates admin,
edit and view roles in that namespace.
apiVersion: v1
kind: Namespace
metadata:
name: example
annotations:
rbac.crossplane.io/composites.example.org: xrd-claim-accepted
rbac.crossplane.io/examplecomposites.xr.example.org: xrd-claim-accepted
The above namespace, for example, will result in the creation of the following role:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: crossplane-edit
rules:
# Crossplane namespace editors have access to view events.
- apiGroups: [""]
resources: [events]
verbs: [get, list, watch]
# Crossplane namespace editors may need to read or otherwise interact with
# resource claim connection secrets.
- apiGroups: [""]
resources: [secrets]
verbs: ["*"]
# Crossplane editors have full access to all of the composite resource claims
# that an admin has chosen to enable in their namespace.
- apiGroups: [xr.example.org]
resources:
- exampleclaims
verbs: ["*"]
Note that Kubernetes only supports the aggregation of cluster roles, not of
namespaced roles. The RBAC manager therefore uses an opinionated approximation
of cluster role aggregation to build namespaced RBAC roles. For the namespaced
edit role, for example, it:
rbac.crossplane.io/aggregate-to-ns-edit: "true" and
rbac.crossplane.io/base-of-ns-edit: "true"rbac.crossplane.io/aggregate-to-ns-edit: "true" and
rbac.crossplane.io/xrd: examplecomposites.xr.example.org, where
examplecomposites.xr.example.org is the name of an XRD that is accepted in
its namespace.When a provider is installed (via the package manager) the RBAC manager will define three cluster roles:
crossplane:provider:7f63e3661:aggregate-to-editcrossplane:provider:7f63e3661:aggregate-to-viewcrossplane:provider:7f63e3661:systemNote that these role names are derived from the active ProviderRevision. In
this example 7f63e3661 is an abbreviated hash. The aggregate-to-edit role
aggregates to the crossplane, crossplane-admin and crossplane-edit roles,
while the aggregate-to-view role aggregates to the crossplane-view role. The
system role does not aggregate; it is granted by the RBAC manager to the
service account under which the provider pod runs.
To create a cluster role for a Provider (package), the RBAC manager:
ProviderRevision.ProviderRevision.ClusterRole that grants access to the defined CRs.In the case of the system role the RBAC manager creates a ClusterRoleBinding
to the ServiceAccount under which the provider runs. This service account is
also controlled by the aforementioned ProviderRevision, and can be discovered
similarly.
Below is an example of the system provider role and its role binding.
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane:provider:7f63e3661:system
rules:
- apiGroups: [provider.example.org]
resources:
- examplemanageds
- examplemanageds/status
- exampleproviderconfigs
- exampleproviderconfigs/status
verbs: [get, list, watch, update, patch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: crossplane:provider:7f63e3661:system
roleRef:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
name: crossplane:provider:7f63e3661:system
subjects:
- kind: ServiceAccount
name: crossplane-provider-7f63e3661
namespace: crossplane-system
Below is an example of an edit provider role. Note that in practice the role
would likely have many more rules - one for each custom resource defined by the
provider.
---
# This role aggregates to the crossplane and crossplane-edit roles. It thus
# aggregates transitively to the crossplane-admin role via crossplane-edit.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane:provider:7f63e3661:aggregate-to-edit
labels:
rbac.crossplane.io/aggregate-to-crossplane: "true"
rbac.crossplane.io/aggregate-to-edit: "true"
rules:
- apiGroups: [provider.example.org]
resources:
- examplemanageds
- exampleproviderconfigs
verbs: [*]
Building and deploying the RBAC manager as a distinct process allows Crossplane users to completely opt-out of RBAC management. Users may choose not to deploy the RBAC manager without compromising any core Crossplane functionality. In addition to completely disabling the RBAC manager it will be possible to limit the scope of the RBAC manager via a flag. The following levels will exist:
--manage=all - Include all functionality described in this design. (Default)--manage=serviceaccounts - Manage only the 'software' roles - i.e. the
crossplane role and the roles for each provider.--manage=basic - Manage the software roles plus the crossplane-admin,
crossplane-edit, and crossplane-view roles.The following alternatives were considered before arriving at this design.
Crossplane currently relies on the package manager to manage its RBAC roles.
This has become less desirable primarily because it's now possible to extend
Crossplane (i.e. define new APIs) using the API extensions controllers. While
many composite resources will be defined via a Configuration package it's
equally possible that they will be defined directly.
Kubernetes provides three cluster roles; admin, edit, and view that are
intended to be bound to a subject at the namespace scope using a RoleBinding.
This allows a subject to be "an administrator of a namespace" or "a viewer of a
namespace" similar to the design put forward by this namespace. The limitation
of this approach is that "administrator" or "viewer" must mean the same thing to
every namespace; it is not possible for the administrator role for a namespace
to grant different access to that namespace from the administrator role of
another namespace.
This design proposes that the RBAC manager react to the definition of composite resources and the installation of providers by creating and aggregating opinionated RBAC roles. It may be possible to extend this design by making the RBAC manager rule driven; allowing it to be configured by a new set of custom resources to create and aggregate arbitrary RBAC rules when new Crossplane types (or even arbitrary types) are created. Crossplane is choosing to be opinionated about its RBAC roles at this time, but may explore this path in the future if opinionated RBAC roles prove to be insufficient.
This design proposes that the RBAC manager approximate cluster role aggregation when managing namespaced RBAC roles. This is driven by a desire to leverage role aggregation at the namespace scope, which is not supported by Kubernetes. An alternative to this approach would be to create 'namespace-aligned' cluster roles for each namespace and bind them to subjects in that namespace using a role binding. This alternative approach is appealing in that it leverages 'native' RBAC role aggregation, but it has a few downsides:
In all the difference between using namespace-aligned cluster roles and actual namespaced roles is negligible both in terms of functionality and complexity, so the decision comes down to which approach is more 'idiomatic'.