How to Create Custom Platform Role

1. Overview

This document explains the core concepts of custom roles and how to configure a RoleTemplate. There are two main ways to build a working custom role — pick either and follow it end to end:

  • FunctionResource method (5.1): select permissions from the product's built-in permission catalog. Simplest option, no RBAC authoring required.
  • ClusterRole aggregation method (5.2): author your own native ClusterRole and pull it into a RoleTemplate via label matching. More setup, but works independently of what any product module has migrated, and is the platform's long-term direction.

customRules (5.3) is a third, lower-level option for one-off native RBAC rules.

Not sure which to pick? Start with Method A (5.1) — it's the simplest and covers almost every need. Choose Method B (5.2) only when you specifically need a role built from your own ClusterRole, or one that must span multiple clusters.

Once a RoleTemplate exists, grant it to users via UserBinding — see Manage Roles for the console steps.

2. Core Concepts

  • RoleTemplate: Custom role template. It defines role semantics and permission sets, and is converted by the controller into ClusterRoles.
  • FunctionResource: An abstraction of K8s resources used by product features; referenced by functionResourceRef in RoleTemplate.
  • ClusterRole: RBAC rule set. A hand-written ClusterRole can be aggregated into a RoleTemplate (or into a built-in system role) via labels.
  • UserBinding: Binding between users and roles/scopes; the controller generates RoleBinding/ClusterRoleBinding based on UserBinding for final authorization.
NOTE
  • The value of functionResourceRef comes from FunctionResource metadata.name.
  • Display names are typically in metadata.annotations, which you can use to map to UI modules.

3. Technical Changes

Since ACP v4.3, feature permissions are gradually migrating from FunctionResource to native K8s ClusterRole management, aggregated into system roles via ClusterRole labels; FunctionResource-based management will be retired in v4.5. You don't need this background to create a role — skip to section 5 for the steps.

NOTE

The migration is still in progress: most product modules haven't published aggregation-ready ClusterRoles yet, so the built-in system labels mentioned in 5.4 (aggregate-to-namespace-developer, etc.) resolve to nothing today. Don't build a custom role by pointing aggregationRules at one and hoping — 5.2 shows the pattern that works, and 5.5 lets you verify. (You may also see hidden legacy-* RoleTemplates in kubectl output; these are compatibility bridges for the built-in roles and can be ignored.)

4. Choosing a Method

All three methods produce a working RoleTemplate; they differ in what you author and what they can express.

Method A: FunctionResource (5.1)Method B: ClusterRole aggregation (5.2)Method C: customRules (5.3)
Best forReusing permissions a product already exposesAn independent role built from your own ClusterRole, especially across clustersA quick one-off native RBAC rule
You authorNothing — pick from the catalogA ClusterRole plus its aggregation labelsInline RBAC rules
Multi-clusterHandled by the platformAdd sync.scope: "platform" to your ClusterRoleHandled by the platform
Cross-scope rulesYesYes — one ClusterRole per scopeNo — base ClusterRole only

5. Configuration

CAUTION

Always set rbac.cpaas.io/version: "v2" and auth.cpaas.io/roletemplate.frozen: "false" on a custom RoleTemplate. For custom roles the controller auto-fills a missing version with v1 (which ignores aggregationRules) and a missing frozen with true (which stops later spec edits from updating the ClusterRoles), handling the two independently — so setting only one still leaves the other at the legacy default. The examples below also set auth.cpaas.io/roletemplate.official: "false" (optional, but never set it to "true" on a custom role — that flips it to a built-in system role) and auth.cpaas.io/roletemplate.level, which decides where the role is granted — pick the smallest that fits: namespace (per namespace), project (all namespaces in a project), cluster (a whole cluster), or platform (the whole platform).

5.1 Method A: FunctionResource

Use this when the permissions you need are already exposed as FunctionResources — this is true for almost all current product modules.

Step 1 — find the FunctionResources you need

kubectl get functionresource -o custom-columns='NAME:.metadata.name,MODULE:.metadata.labels.auth\.cpaas\.io/functionresource\.module,DISPLAY:.metadata.annotations.cpaas\.io/functionresource\.function\.display-name'

metadata.name is what you'll put in functionResourceRef; the display-name annotation tells you which UI feature it maps to.

Step 2 — write the RoleTemplate

apiVersion: auth.alauda.io/v1beta1
kind: RoleTemplate
metadata:
  name: demo-funcrole
  annotations:
    cpaas.io/display-name: Example Function Role
    cpaas.io/description: FunctionResource example
  labels:
    auth.cpaas.io/roletemplate.level: namespace
    auth.cpaas.io/roletemplate.official: "false"
    rbac.cpaas.io/version: "v2"
    auth.cpaas.io/roletemplate.frozen: "false"
spec:
  rules:
    - functionResourceRef: acp-app
      verbs: [get, list, watch]

Step 3 — apply and verify with 5.5.

NOTE

acp-namespace-resource-manage is not allowed in custom roles.

5.2 Method B: ClusterRole aggregation (independent custom role)

Unlike the FunctionResource method, aggregationRules doesn't define permissions by itself — it only pulls rules in from existing ClusterRoles that carry matching labels. To build a genuinely independent custom role this way, you provide both halves yourself: the ClusterRole with real rules, and the label that connects it to your RoleTemplate.

Step 1 — write a native ClusterRole with the real permissions, and label it with your own aggregation label plus a scope label. Don't reuse the built-in system-role labels (5.4) unless you deliberately want to grant these permissions to everyone who already holds that built-in role — for an independent role, invent your own label name.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cpaas:demo-auditor:business-ns:view
  labels:
    rbac.cpaas.io/aggregate-to-demo-auditor: "true"
    rbac.cpaas.io/aggregate-to-scope-business-ns: "true"
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch"]

Step 2 — if this role needs to work in business clusters too, and the ClusterRole above was only created in the global cluster, add rbac.cpaas.io/sync.scope: "platform" so the platform replicates it to every cluster. Without this label the ClusterRole only exists wherever you created it, and the RoleTemplate below will resolve to empty permissions on any cluster that doesn't have it.

  labels:
    rbac.cpaas.io/aggregate-to-demo-auditor: "true"
    rbac.cpaas.io/aggregate-to-scope-business-ns: "true"
    rbac.cpaas.io/sync.scope: "platform"

Step 3 — create the RoleTemplate, referencing the same labels via aggregationRules:

apiVersion: auth.alauda.io/v1beta1
kind: RoleTemplate
metadata:
  name: demo-auditor
  annotations:
    cpaas.io/display-name: Example Aggregation Role
    cpaas.io/description: ClusterRole aggregation example
  labels:
    auth.cpaas.io/roletemplate.level: namespace
    auth.cpaas.io/roletemplate.official: "false"
    rbac.cpaas.io/version: "v2"
    auth.cpaas.io/roletemplate.frozen: "false"
spec:
  aggregationRules:
    - clusterRoleSelectors:
        - matchLabels:
            rbac.cpaas.io/aggregate-to-demo-auditor: "true"
            rbac.cpaas.io/aggregate-to-scope-business-ns: "true"
      scope: business-ns
  rules: []

To cover more than one scope (e.g. also cluster or system-ns), add one ClusterRole per scope with the matching scope label, and one more entry under aggregationRules per scope — see 6.2 for a worked multi-scope example.

Step 4 — apply and verify with 5.5. An empty result means either the labels on the ClusterRole and in clusterRoleSelectors don't actually match, or the ClusterRole hasn't synced to the cluster you're checking (step 2).

CAUTION

Don't point aggregationRules at a system/product label you hope exists (e.g. rbac.cpaas.io/aggregate-to-namespace-auditor) — most modules haven't published theirs yet, so it resolves to nothing and the role silently grants no permissions. Your own ClusterRole plus your own label, as above, always works.

5.3 Method C: customRules

For a one-off native RBAC rule that doesn't warrant a separate ClusterRole object, inline it directly in the RoleTemplate:

apiVersion: auth.alauda.io/v1beta1
kind: RoleTemplate
metadata:
  name: demo-customrules
  annotations:
    cpaas.io/display-name: Example Custom Rules Role
    cpaas.io/description: customRules example
  labels:
    auth.cpaas.io/roletemplate.level: namespace
    auth.cpaas.io/roletemplate.official: "false"
    rbac.cpaas.io/version: "v2"
    auth.cpaas.io/roletemplate.frozen: "false"
spec:
  customRules:
    - apiGroups: [""]
      resources: ["configmaps"]
      verbs: ["get", "list", "watch"]
NOTE

customRules are written only into the role's base ClusterRole — they can't be targeted at a specific scope like cluster or system-ns (there is no scope field on customRules). If you need cross-scope rules, use Method A or Method B.

5.4 Field Reference

The first table covers metadata and the labels every custom role needs; the second covers the permission fields, grouped by the method they belong to (5.1–5.3).

Metadata and labels (on every custom RoleTemplate)

FieldDescriptionValues
metadata.nameThe RoleTemplate's unique name; a UserBinding references it when granting the role to users.Custom string
metadata.annotations.cpaas.io/display-name, .../descriptionHuman-readable name and description shown in the console.Custom string
metadata.labels.auth.cpaas.io/roletemplate.levelWhere the role is granted, and which ClusterRoles the controller generates for it.platform / cluster / project / namespace
metadata.labels.auth.cpaas.io/roletemplate.officialWhether this is a built-in system role. A custom role must not claim to be official."false" or omit; never "true" on a custom role
metadata.labels.rbac.cpaas.io/versionWhich conversion logic the controller applies. v2 understands aggregationRules; the legacy v1 ignores it."v2" (current); omitting defaults to "v1"
metadata.labels.auth.cpaas.io/roletemplate.frozenWhether the generated ClusterRoles are locked. "false" lets later spec edits take effect."false"; omitting defaults to "true" (frozen)

Permission fields (use the set for your method)

FieldMethodDescriptionValues
spec.rules[].functionResourceRefAName of a FunctionResource whose permissions to include. Find names with kubectl get functionresource.FunctionResource metadata.name
spec.rules[].verbsAActions the role allows on that FunctionResource's resources.get / list / watch / create / update / patch / delete / deletecollection
spec.aggregationRules[].scopeBWhich generated ClusterRole the matched rules land in — i.e. where they apply: business-ns (namespaces created under a project), project-ns (a project's own namespace), system-ns (cpaas-system), cluster (cluster-wide), kube-public. Must be compatible with level (see note below).cluster / project-ns / business-ns / system-ns / kube-public
spec.aggregationRules[].clusterRoleSelectorsBPicks which existing ClusterRoles to pull rules from, by their labels.matchLabels (exact key/values) and/or matchExpressions (set-based: In / NotIn / Exists / DoesNotExist)
spec.customRules[].apiGroupsCK8s API groups the rule covers."" (core group) / "*" (all) / a group name
spec.customRules[].resourcesCResource types the rule covers.e.g. pods, configmaps; "*" = all
spec.customRules[].verbsCActions allowed on those resources.same as spec.rules[].verbs
spec.customRules[].resourceNamesCOptional: restrict the rule to specific named objects.list of names
spec.customRules[].nonResourceURLsCOptional: grant access to non-resource URLs (e.g. /healthz); for cluster-level roles.list of URLs
NOTE
  • Use either direct rules (spec.rules / spec.customRules) or spec.aggregationRules for a given scope, not both — where they overlap on the same generated ClusterRole, the aggregation rule wins and the direct rules are dropped.
  • auth.cpaas.io/roletemplate.level (where the role is grantedplatform / cluster / project / namespace) and spec.aggregationRules[].scope (which generated ClusterRole each aggregated rule lands in) are different things. The controller enforces one constraint between them: platform- and cluster-level roles may only use scope: cluster; project- and namespace-level roles may use any of the five scopes (including cluster). Breaking this rule puts the RoleTemplate into a Failed state.

Labels for matchLabels

For an independent custom role, invent your own aggregation label — any rbac.cpaas.io/aggregate-to-<name> string works (5.2). Pair it with one scope label, which controls where the rules apply:

  • rbac.cpaas.io/aggregate-to-scope-cluster: "true"
  • rbac.cpaas.io/aggregate-to-scope-project-ns: "true"
  • rbac.cpaas.io/aggregate-to-scope-business-ns: "true"
  • rbac.cpaas.io/aggregate-to-scope-system-ns: "true"
  • rbac.cpaas.io/aggregate-to-scope-kube-public: "true"

To instead add permissions to a built-in system role (advanced — this grants them to everyone who already holds that role), label your ClusterRole with rbac.cpaas.io/aggregate-to-<built-in-role>, e.g. aggregate-to-namespace-developer or aggregate-to-platform-admin.

Multi-cluster distribution (5.2 step 2): add rbac.cpaas.io/sync.scope: "platform" to a ClusterRole you create by hand to replicate it from the global cluster to all clusters. It has no effect on a RoleTemplate or on the ClusterRoles a RoleTemplate generates.

5.5 Verify a configuration actually takes effect

Check the RoleTemplate status

The RoleTemplate reports what it generated and whether it synced — start here:

kubectl get roletemplate <name> -o jsonpath='{.status.phase}{"\n"}{.status.clusterRoles}{"\n"}'
kubectl get roletemplate <name> -o jsonpath='{.status.conditions}'

status.phase should be Ready; status.clusterRoles lists the ClusterRoles it produced; status.conditions carries the reason/message when something fails. If the phase isn't Ready, status.clusterRoles is empty, or the ClusterRoles it names have empty rules (kubectl get clusterrole <name> -o yaml), the role isn't granting what you expect. Common causes, in rough order of frequency:

  • The RoleTemplate is missing rbac.cpaas.io/version: "v2", or was auto-frozen — see the required-labels caution in section 5. This is the most frequent reason aggregationRules appears to do nothing.
  • A functionResourceRef name is misspelled — the controller skips unknown references silently and produces an empty ClusterRole with no error.
  • Your aggregationRules labels don't match any ClusterRole, or the source ClusterRole hasn't synced to this cluster.

Confirm a user actually gets the permission

Generating ClusterRoles isn't the end goal — a user having access is. After granting the role via UserBinding (see Manage Roles), confirm the real outcome with an impersonated check:

kubectl auth can-i get deployments --as=<user> -n <namespace>

Check whether an aggregation label is usable before writing aggregationRules

Before referencing an rbac.cpaas.io/aggregate-to-* label in clusterRoleSelectors, confirm it actually resolves to a ClusterRole:

kubectl get clusterrole -l rbac.cpaas.io/aggregate-to-<your-label>=true \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.auth\.cpaas\.io/roletemplate}{"\n"}{end}'
  • No output → no ClusterRole carries this label. If you're using your own custom label (5.2), double-check the label was actually applied to your ClusterRole and that it synced to this cluster. If you're using a system label (5.4) hoping a product module published it, it likely hasn't — see 3.
  • Output shows a legacy-* value for auth.cpaas.io/roletemplate → this ClusterRole comes from the FunctionResource-based compatibility bridge described in 3, not from a module that has actually migrated. The permissions still originate from FunctionResource, and this bridge is expected to disappear in v4.5.

5.6 Update and delete

  • Update: edit the RoleTemplate spec. Changes only take effect while auth.cpaas.io/roletemplate.frozen is "false" (see the caution in section 5).
  • Delete: delete the RoleTemplate. The controller garbage-collects the ClusterRoles it generated across all clusters — you don't clean those up by hand. (A ClusterRole you hand-authored for Method B is yours to delete.)

6. Copy & Trim (Examples)

Two worked examples: trimming a built-in template (Method A, 6.1), and building a multi-scope role from your own ClusterRoles (Method B, 6.2). For a FunctionResource role, copying a built-in system template and trimming it is the quickest way to avoid missing module permissions.

6.1 Trim a system RoleTemplate YAML (Method A)

Steps:

  • Keep the required FunctionResources.
  • Reduce verbs to read-only (get/list/watch).
  • Remove unused modules.

Role example: developer auditor without secret permissions

NOTE
  • In custom roles, do not configure Namespace Resource Management (FunctionResource: acp-namespace-resource-manage).
  • Remove User Secret Dictionary (FunctionResource: acp-user-secret).
  • Set all verbs to get/list/watch.

Example YAML:

apiVersion: auth.alauda.io/v1beta1
kind: RoleTemplate
metadata:
  annotations:
    cpaas.io/description: Responsible for development, deployment, and maintenance within the namespace.
    cpaas.io/display-name: Developer Copy
  labels:
    auth.cpaas.io/roletemplate.level: namespace
    auth.cpaas.io/roletemplate.official: "false"
    rbac.cpaas.io/version: "v2"
    auth.cpaas.io/roletemplate.frozen: "false"
  name: namespace-developer-system-copy
spec:
  rules:
    - functionResourceRef: views-acp-userview
      verbs:
        - get
        - list
        - watch
    - functionResourceRef: infrastructure-clusters
      verbs:
        - get
        - list
        - watch
    - functionResourceRef: infrastructure-nodesmanage
      verbs:
        - get
        - list
        - watch
    - functionResourceRef: infrastructure-domains
      verbs:
        - get
        - list
        - watch
    - functionResourceRef: acp-subnet
      verbs:
        - get
        - list
        - watch
    - functionResourceRef: acp-networkpolicies
      verbs:
        - get
        - list
        - watch
    - functionResourceRef: infrastructure-storageclasses
      verbs:
        - get
        - list
        - watch
    - functionResourceRef: infrastructure-persistentvolumes
      verbs:
        - get
        - list
        - watch
    - functionResourceRef: acp-virtualmachineimagetemplates
      verbs:
        - get
        - list
        - watch

6.2 Build a multi-scope auditor role (Method B)

This is the aggregation-method equivalent of 6.1: a namespace-level read-only role that spans both the business and project namespaces, built from your own ClusterRoles and your own label — following the pattern from 5.2.

ClusterRoles — one per scope you want to cover, all sharing the same custom label, each with its own scope label:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cpaas:demo-auditor:business-ns:view
  labels:
    rbac.cpaas.io/aggregate-to-demo-auditor: "true"
    rbac.cpaas.io/aggregate-to-scope-business-ns: "true"
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["configmaps", "services"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cpaas:demo-auditor:project-ns:view
  labels:
    rbac.cpaas.io/aggregate-to-demo-auditor: "true"
    rbac.cpaas.io/aggregate-to-scope-project-ns: "true"
rules:
  - apiGroups: [""]
    resources: ["configmaps", "services"]
    verbs: ["get", "list", "watch"]

RoleTemplate — one aggregationRules entry per scope, reusing the same custom label:

apiVersion: auth.alauda.io/v1beta1
kind: RoleTemplate
metadata:
  name: demo-namespace-auditor
  annotations:
    cpaas.io/display-name: Custom Namespace Auditor
    cpaas.io/description: Read-only audit across business and project namespaces
  labels:
    auth.cpaas.io/roletemplate.level: namespace
    auth.cpaas.io/roletemplate.official: "false"
    rbac.cpaas.io/version: "v2"
    auth.cpaas.io/roletemplate.frozen: "false"
spec:
  aggregationRules:
    - clusterRoleSelectors:
        - matchLabels:
            rbac.cpaas.io/aggregate-to-demo-auditor: "true"
            rbac.cpaas.io/aggregate-to-scope-business-ns: "true"
      scope: business-ns
    - clusterRoleSelectors:
        - matchLabels:
            rbac.cpaas.io/aggregate-to-demo-auditor: "true"
            rbac.cpaas.io/aggregate-to-scope-project-ns: "true"
      scope: project-ns
  rules: []

Add system-ns the same way if you need it, and remember rbac.cpaas.io/sync.scope: "platform" on each ClusterRole (5.2 step 2) if it's only created in the global cluster but the role must work in business clusters too.