Skip to main content

Capability catalog

A capability is a named permission that a role can grant, such as calendar:create. Each module declares its capabilities in code. At start-up, CMDX copies them into the database, where roles can use them. This page lists the fields of a capability, how a module registers one, and how start-up keeps the database in step. The code is in modules.capability_catalog.

Summary​

PartWhereWhat it does
register_capability()modules.capability_catalog.permissionsAdds one capability to the in-memory registry.
register_all_capabilities()modules.capability_catalog.bootstrapCalls each module's registration function.
ReconciliationCapabilityReconciliationServiceCopies the registry into the permissions and rbac_capabilities tables at start-up and after a plugin changes state.
Catalog APIGET /api/v1/capabilitiesLists the capabilities in the database.

Capability fields​

register_capability() takes these keyword arguments. All are required.

FieldTypeDescription
keystrUnique key, for example calendar:create. Roles and route guards refer to the capability by this key.
display_namestrShort name shown to administrators.
descriptionstrOne sentence on what the capability allows.
risk_levelstrlow, medium or high.
applicable_contextslist[str]One or more of global, organization (a tenant) and course. Context-scoped role overrides are checked against this list.
modulestrName of the module that owns the capability.

register_capability() raises ValueError when a field is empty, risk_level isn't an allowed value, or a context isn't an allowed value. It raises DuplicateCapabilityError when the key is already registered.

Key format​

Write keys as <resource>:<action>, where both parts are lowercase letters, digits and underscores, starting with a letter. For example: calendar:read, developer_settings:manage.

For a key in this format, reconciliation also fills the resource and action columns of the permissions row. Other formats are stored, but those two columns stay empty.

Register a module's capabilities​

A module keeps its capabilities in capabilities.py: a list of field dictionaries and a function that registers each one. The function is then called from register_all_capabilities() in modules/capability_catalog/bootstrap.py. A module that isn't listed there isn't registered.

modules/calendar/capabilities.py
from modules.calendar.permissions import CALENDAR_CREATE, CALENDAR_READ
from modules.capability_catalog.permissions import register_capability

CALENDAR_CAPABILITIES = [
{
"key": CALENDAR_READ, # "calendar:read"
"display_name": "View Calendar",
"description": "View calendar events visible to the caller",
"risk_level": "low",
"applicable_contexts": ["global"],
"module": "calendar",
},
{
"key": CALENDAR_CREATE, # "calendar:create"
"display_name": "Create Calendar Events",
"description": "Create calendar events",
"risk_level": "medium",
"applicable_contexts": ["global"],
"module": "calendar",
},
]


def register_calendar_capabilities() -> None:
for capability in CALENDAR_CAPABILITIES:
register_capability(**capability)

Plugins declare their capabilities in their manifest instead. See plugin system.

Register a plugin's capabilities​

sync_plugin_capabilities() in modules.plugins.capability_sync registers the manifest keys of every plugin that is installed or enabled. Each key gets these fields:

FieldValue
keyThe manifest key, for example gamification.view.
display_nameThe key.
descriptionDeclared by the <plugin> plugin.
risk_levelmedium.
applicable_contextsglobal and organization.
moduleplugin:<plugin>, for example plugin:gamification.

A key that doesn't start with <plugin>. is skipped with a warning. So is a key that another module already registered.

Installing, upgrading, enabling, disabling or uninstalling a plugin runs the sync again, and so does each refresh of the plugin registry, including the one at start-up. Reconciliation then runs with module_prefix="plugin:", so it touches only plugin rows. The capabilities of an uninstalled plugin, or of a global plugin that is disabled, are marked stale. Disabling a per_tenant plugin for one tenant leaves them in place.

Start-up reconciliation​

When the backend starts, it registers every module's capabilities, then the capabilities of each installed or enabled plugin, and then reconciles the registry with the database. While INSTALL_MODE is on, start-up skips this step. The install wizard runs it instead, when it installs the roles module.

For each registered capability, reconciliation:

  1. Finds the permissions row whose name is the key, or creates one. The row's description is set to the registered description.
  2. Creates or updates the rbac_capabilities row that points to that permission, with the display name, risk level, contexts and module. The row is marked not stale.

Then, for each rbac_capabilities row whose key is no longer registered, it sets is_stale to true. Rows are never deleted.

A stale capability grants nothing. Permission checks skip it even when a role still lists it. The catalog API leaves it out unless you ask for it.

Reconciliation writes these log events, each with a capability_key field where it applies:

EventWhen
capability_reconcile_startedReconciliation began.
capability_createdA new capability row was added.
capability_updatedAn existing row was refreshed.
capability_staleA row was marked stale.
capability_reconcile_completedReconciliation finished. Has inserted, updated and stale counts.
capability_reconcile_failedReconciliation raised an error.

Catalog API​

Both endpoints need a signed-in user.

EndpointReturns
GET /api/v1/capabilitiescapabilities, a list, and total. Optional query parameters: module, risk_level, and include_stale (default false).
GET /api/v1/capabilities/{key}One capability, or 404 if the key isn't in the catalog.

Each capability in a response has key, display_name, description, risk_level, applicable_contexts, is_stale and module.