RBAC and Permissions¶
This document describes the complete permission model for the Kuadrant Backstage plugin.
Overview¶
The Kuadrant plugin uses Backstage's RBAC system for access control across API Products, API Keys, and Plan Policies. Permissions follow a consistent .own / .all pattern for resource-level access control.
Design Principles¶
When adding or modifying permissions, follow these principles:
1. Pure RBAC Only¶
All authorisation decisions must use Backstage RBAC permissions. Never bypass RBAC with:
- Group membership checks (e.g.,
if (user.groups.includes('api-owners'))) - Data-based ownership checks for UI visibility (e.g.,
if (user owns any API products)) - Role flags derived from user identity
If you need to control access to a feature, create a permission for it.
2. One Permission Per Distinct Capability¶
Each permission should represent a single, well-defined capability. Don't overload permissions with multiple meanings.
Example: kuadrant.apikey.update.own was overloaded:
- Consumers used it to edit their pending requests
- API Owners used it to imply approval queue access
Solution: Added kuadrant.apikey.approve as a separate permission for approval queue access.
3. UI Visibility = Permission Check¶
If a UI element should be hidden from certain users, gate it with a permission check. Don't use data queries to determine visibility.
// correct - pure RBAC
const { allowed } = useKuadrantPermission(kuadrantApiKeyApprovePermission);
if (allowed) {
return <ApprovalQueueCard />;
}
// incorrect - data-based check
const userOwnsProducts = apiProducts.some(p => p.owner === userId);
if (userOwnsProducts) {
return <ApprovalQueueCard />;
}
4. Backend Enforces, Frontend Hints¶
Permissions in the frontend are for UX (hiding buttons, showing appropriate UI). The backend must always enforce permissions independently - never trust the frontend.
5. Scope Permissions Appropriately¶
.own- user can act on resources they created/own.all- user can act on any resource regardless of ownership- No scope - permission applies globally (e.g.,
list,create)
For actions that don't fit the ownership model (like viewing an approval queue), use an unscoped permission.
Permission Structure¶
Naming Convention¶
Permissions follow the pattern: kuadrant.<resource>.<action>[.scope]
- resource:
planpolicy,apiproduct,apikey - action:
create,read,update,delete,list - scope:
own(user's resources) orall(any resource) - omitted for non-scoped permissions
Permission Types¶
Basic Permissions: No ownership scope, apply globally
kuadrant.planpolicy.createkuadrant.planpolicy.readkuadrant.planpolicy.list
Scoped Permissions: Ownership-aware access control
kuadrant.apiproduct.read.own- read your own API Productskuadrant.apiproduct.read.all- read any API Product
Resource Permissions: Include resource references for fine-grained control
kuadrant.apikey.createwith resource refapiproduct:namespace/name
Complete Permission List¶
PlanPolicy Permissions¶
| Permission | Description | Notes |
|---|---|---|
kuadrant.planpolicy.create |
Create plan policies | Not exposed via plugin - managed on cluster |
kuadrant.planpolicy.read |
Read plan policy details | |
kuadrant.planpolicy.update |
Update plan policies | Not exposed via plugin |
kuadrant.planpolicy.delete |
Delete plan policies | Not exposed via plugin |
kuadrant.planpolicy.list |
List plan policies |
APIProduct Permissions¶
| Permission | Description | Scope |
|---|---|---|
kuadrant.apiproduct.create |
Create API Products | - |
kuadrant.apiproduct.read.own |
Read your own API Products | Own |
kuadrant.apiproduct.read.all |
Read any API Product | All |
kuadrant.apiproduct.update.own |
Update your own API Products | Own |
kuadrant.apiproduct.update.all |
Update any API Product | All |
kuadrant.apiproduct.delete.own |
Delete your own API Products | Own |
kuadrant.apiproduct.delete.all |
Delete any API Product | All |
kuadrant.apiproduct.list |
List API Products (filtered by read permissions) | - |
APIKey Permissions¶
| Permission | Description | Scope |
|---|---|---|
kuadrant.apikey.create |
Request API access | Resource (APIProduct) |
kuadrant.apikey.read.own |
Read requests you created | Own |
kuadrant.apikey.read.all |
Read any request | All |
kuadrant.apikey.update.own |
Edit your own pending requests | Own |
kuadrant.apikey.update.all |
Update any request | All |
kuadrant.apikey.delete.own |
Delete your own requests | Own |
kuadrant.apikey.delete.all |
Delete any request | All |
kuadrant.apikey.approve |
Access approval queue, approve/reject requests | - |
kuadrant.apikey.list |
List requests (filtered by read permissions) | - |
Role Definitions¶
The Kuadrant plugin defines four personas with distinct responsibilities and permissions:
API Consumer¶
Purpose: End users who consume APIs
Permissions:
kuadrant.apiproduct.read.all- browse API catalogkuadrant.apiproduct.listkuadrant.apikey.create- request API accesskuadrant.apikey.read.own- view own requestskuadrant.apikey.update.own- edit own pending requestskuadrant.apikey.delete.own- cancel own requestskuadrant.apikey.read.own- view own API keyskuadrant.apikey.delete.own- revoke own API keys
Cannot:
- Create or manage API Products
- Approve/reject requests
- View other users' API keys
API Owner¶
Purpose: Users who publish and manage their own APIs
Permissions:
- All API Consumer permissions, plus:
kuadrant.planpolicy.read- view plan policies (for reference)kuadrant.planpolicy.listkuadrant.apiproduct.create- create API Productskuadrant.apiproduct.read.own- view own API Productskuadrant.apiproduct.update.own- update own API Productskuadrant.apiproduct.delete.own- delete own API Productskuadrant.apikey.approve- access approval queue, approve/reject requests for own APIskuadrant.apikey.read.own- view API keys for own APIskuadrant.apikey.delete.own- delete API keys for own APIs
Cannot:
- View or modify other owners' API Products
- Create/update/delete PlanPolicies (managed on cluster)
- Approve requests for other owners' APIs
API Admin¶
Purpose: Platform engineers who manage all API Products
Responsibilities:
- Manages all API Products across the platform
- Approves/rejects any API key request (cross-team)
- Troubleshoots issues on behalf of API Owners
- Provides second-level support for API management
Permissions:
- All
.allscoped permissions kuadrant.apiproduct.create- create any API Productkuadrant.apiproduct.read.all- view all API Productskuadrant.apiproduct.update.all- update any API Productkuadrant.apiproduct.delete.all- delete any API Productkuadrant.apikey.read.all- view all requestskuadrant.apikey.update.all- update any requestkuadrant.apikey.approve- access approval queue, approve/reject any requestkuadrant.apikey.delete.all- delete any request/API key- RBAC policy management permissions
Cannot:
- Create/update/delete PlanPolicies (managed on cluster)
- Modify platform infrastructure (HTTPRoutes, Gateways)
Platform Engineer¶
Purpose: Infrastructure engineers who manage Kuadrant platform
Responsibilities:
- Manages cluster infrastructure (Gateways, HTTPRoutes, PlanPolicies)
- Creates PlanPolicy resources with rate limit tiers
- Annotates HTTPRoutes with
backstage.io/expose: "true"to make them available for publishing - Coordinates with API Admins and API Owners when changing rate limits
- Does not typically manage individual API Products (delegated to API Admins/Owners)
Permissions:
- Full cluster admin access (Kubernetes RBAC)
- Create/read/update/delete PlanPolicy resources
- Create/read/update/delete HTTPRoute resources
- Create/read/update/delete Gateway resources
- Manage RBAC policies
Cannot:
- Typically does not manage day-to-day API Products (delegates to API Admin)
RBAC Permissions Matrix¶
Comprehensive view of what each persona can and cannot do:
| Persona | Can Do | Cannot Do |
|---|---|---|
| Platform Engineer | • Manage Kuadrant infrastructure (Gateways, HTTPRoutes) • Create/update/delete PlanPolicy resources • Annotate HTTPRoutes with backstage.io/expose: "true"• Manage RBAC policies and permissions • Configure platform-wide settings • Full cluster admin access for platform management |
• Typically does not manage day-to-day API Products (delegates to API Admin/Owner) • Should coordinate with API Admins and API Owners before changing rate limits |
| API Admin | • Read all APIProducts • Create/update/delete any APIProduct • Approve/reject any API key requests • Manage all API keys (read/delete) • View all APIKeys • Troubleshoot on behalf of API Owners • All .all scoped permissions |
• Cannot create/update/delete PlanPolicy • Cannot modify platform infrastructure (HTTPRoutes, Gateways) |
| API Owner | • Read/list HTTPRoutes (to publish APIs) • Create/update/delete own APIProducts • Read all APIProducts • Approve/reject API key requests for own APIs • Delete API key requests for own APIs • Manage own API documentation • View/manage API keys for own APIs |
• Cannot create/update PlanPolicy • Cannot modify platform infrastructure • Cannot approve requests for other owners' APIs • Cannot update/delete other owners' APIProducts |
| API Consumer | • Read/list APIProduct • Create APIKey • Read/update/delete own APIKeys • View own request status • Manage own API keys • Use APIs within rate limit quotas |
• Cannot approve requests • Cannot view others' requests • Cannot create or publish APIs • Cannot modify rate limits |
Permission Breakdown by Resource¶
PlanPolicy (rate limit tiers):
- Platform Engineer: create, read, update, delete
- API Admin: read, list (for reference)
- API Owner: read, list (for reference)
- API Consumer: none
HTTPRoute:
- Platform Engineer: create, read, update, delete, annotate
- API Admin: read, list (for reference)
- API Owner: read, list (to select for publishing)
- API Consumer: none (indirect read through APIProduct)
APIProduct (catalog entries):
- Platform Engineer: typically none (delegated to API Admin/Owner)
- API Admin: create, read, update, delete (all)
- API Owner: create, read (all), update (own), delete (own)
- API Consumer: read, list
APIKey (access requests):
- Platform Engineer: typically none (delegated to API Admin)
- API Admin: create, read (all), update (all), delete (all), approve
- API Owner: create, read (own), update (own), delete (own), approve (for own APIs)
- API Consumer: create, read (own), update (own - edit pending), delete (own)
Role Hierarchy¶
The four personas form a clear hierarchy:
- Platform Engineer - infrastructure layer (cluster, gateways, rate limits)
- API Admin - management layer (all API Products, all requests)
- API Owner - ownership layer (own API Products, own API requests)
- API Consumer - consumption layer (browse, request, use)
Each layer builds on the capabilities below it, with clear boundaries of responsibility.
Ownership Model¶
Ownership Tracking¶
APIProducts track ownership via the standard Backstage annotation:
The owner reference uses Backstage's entity reference format: kind:namespace/name
Immutability: The ownership annotation is set on creation and cannot be modified. This prevents ownership hijacking and maintains clear accountability.
Timestamp: Kubernetes automatically sets metadata.creationTimestamp for audit purposes.
Backend Enforcement Pattern¶
All sensitive endpoints use tiered permission checks:
// 1. try .all permission first (admin access)
const allDecision = await permissions.authorize(
[{ permission: kuadrantApiProductUpdateAllPermission }],
{ credentials }
);
if (allDecision[0].result !== AuthorizeResult.ALLOW) {
// 2. fallback to .own permission
const ownDecision = await permissions.authorize(
[{ permission: kuadrantApiProductUpdateOwnPermission }],
{ credentials }
);
if (ownDecision[0].result !== AuthorizeResult.ALLOW) {
throw new NotAllowedError('unauthorised');
}
// 3. verify ownership
const apiProduct = await k8sClient.getCustomResource(...);
const owner = apiProduct.metadata?.annotations?.['backstage.io/owner'];
const ownerUserId = extractUserIdFromOwner(owner); // extracts "jmadigan" from "user:default/jmadigan"
if (ownerUserId !== userId) {
throw new NotAllowedError('you can only update your own api products');
}
}
// proceed with operation
List Endpoint Filtering¶
List endpoints return different results based on permissions:
// GET /apiproducts
if (hasReadAllPermission) {
return allApiProducts;
} else if (hasReadOwnPermission) {
return allApiProducts.filter(p => {
const owner = p.metadata?.annotations?.['backstage.io/owner'];
const ownerUserId = extractUserIdFromOwner(owner);
return ownerUserId === userId;
});
} else {
throw new NotAllowedError('unauthorised');
}
Approval Workflow¶
Approval Queue Visibility¶
The approval queue card is gated by kuadrant.apikey.approve:
const { allowed } = useKuadrantPermission(kuadrantApiKeyApprovePermission);
if (allowed) {
return <ApprovalQueueCard />;
}
- API Consumers: No approval queue (no
approvepermission) - API Owners: See approval queue, can approve requests for their own APIs
- API Admins: See approval queue, can approve any request
Backend Approval Enforcement¶
The backend uses tiered permission checks for the actual approve/reject action:
- Check
kuadrant.apikey.update.all(admin access) - Fallback to ownership verification (API owner must own the APIProduct)
// approval endpoint logic
const updateAllDecision = await permissions.authorize(
[{ permission: kuadrantAPIKeyUpdateAllPermission }],
{ credentials }
);
if (updateAllDecision[0].result !== AuthorizeResult.ALLOW) {
// not an admin - verify ownership of the APIProduct
const apiProduct = await k8sClient.getCustomResource(...);
const owner = apiProduct.metadata?.annotations?.['backstage.io/owner'];
const ownerUserId = extractUserIdFromOwner(owner);
if (ownerUserId !== userId) {
throw new NotAllowedError('you can only approve requests for your own api products');
}
}
Note: kuadrant.apikey.approve controls UI visibility of the approval queue. The backend enforces ownership separately to ensure API Owners can only approve requests for their own APIs.
Per-APIProduct Access Control¶
The kuadrant.apikey.create permission supports resource references for fine-grained control:
# allow all consumers to request any API
p, role:default/api-consumer, kuadrant.apikey.create, create, allow, apiproduct:*/*
# restrict specific APIs to specific roles
p, role:default/partner, kuadrant.apikey.create, create, allow, apiproduct:toystore/toystore-api
p, role:default/internal, kuadrant.apikey.create, create, allow, apiproduct:internal/*
Backend checks include the resource reference:
const resourceRef = `apiproduct:${apiNamespace}/${apiName}`;
const decision = await permissions.authorize([{
permission: kuadrantAPIKeyCreatePermission,
resourceRef,
}], { credentials });
Catalog Integration¶
The APIProduct entity provider only syncs products with ownership annotations to the Backstage catalog:
const owner = product.metadata.annotations?.['backstage.io/owner'];
if (!owner) {
console.warn(`skipping apiproduct ${namespace}/${name} - no ownership annotation`);
return null;
}
const entity: ApiEntity = {
spec: {
owner, // "user:default/jmadigan"
// ...
}
};
This ensures clean separation between Backstage-managed and kubectl-managed resources.
RBAC Configuration¶
Policy File Location¶
rbac-policy.csv at repository root
Configuration Reference¶
See app-config.local.yaml:
Testing Different Roles¶
Use the included helper scripts:
yarn user:consumer # switch to API Consumer role
yarn user:owner # switch to API Owner role
yarn user:default # restore default permissions
After switching roles, restart with yarn dev.
Security Considerations¶
Input Validation¶
All mutating endpoints use Zod schemas to validate request bodies with explicit whitelists:
const patchSchema = z.object({
spec: z.object({
displayName: z.string().optional(),
description: z.string().optional(),
// only allowed fields - targetRef, namespace, etc. excluded
}).partial(),
});
Ownership Immutability¶
PATCH endpoints explicitly prevent modification of ownership annotation:
// prevent ownership hijacking
if (req.body.metadata?.annotations) {
delete req.body.metadata.annotations['backstage.io/owner'];
}
Authentication Required¶
All endpoints require valid authentication with no guest fallbacks:
const credentials = await httpAuth.credentials(req);
if (!credentials || !credentials.principal) {
throw new NotAllowedError('authentication required');
}
Frontend Permission Checks¶
Use the custom useKuadrantPermission hook for permission-aware UI:
import { useKuadrantPermission } from '../../utils/permissions';
const { allowed, loading, error } = useKuadrantPermission(
kuadrantApiProductCreatePermission
);
if (loading) return <Progress />;
if (!allowed) return null; // hide button
For ownership-aware actions:
import { canDeleteResource } from '../../utils/permissions';
const canDelete = canDeleteResource(
resource.spec.requestedBy.userId, // owner
currentUserId, // current user
canDeleteOwnPermission, // permission to delete own
canDeleteAllPermission // permission to delete all
);
Two-Layer RBAC Model¶
The Kuadrant plugin uses separate RBAC layers with clear separation:
Layer 1: Backstage RBAC (Portal Access Control)
- Catalog visibility: who can see API entities
- Request creation: who can request API keys
- Approval: who can approve/reject requests
- Management: who can create/delete APIProducts
Layer 2: Kuadrant/Gateway RBAC (Runtime Access Control)
- API key validation: is this key valid? (AuthPolicy)
- Rate limiting: what limits apply? (PlanPolicy predicate checks)
- Authentication: does request have valid auth? (AuthPolicy)
No overlap: Backstage controls who gets API keys, Kuadrant/Gateway enforces runtime limits.