C3 AI Documentation Home

Privileged Actions

A privileged action is a method that the platform trusts to invoke a fixed set of child actions that the calling user would not otherwise be authorized to run. It is the mechanism that lets a small, reviewed piece of code perform a sensitive operation on behalf of an unprivileged caller without granting that caller a standing permission.

The canonical example is a method that reads a secret from a Config on behalf of a user who has no direct access to that secret. The user cannot call getSecret() directly, but they can call the privileged method, and the privileged method is trusted to call getSecret() for them.

Because a privileged action deliberately bypasses normal authorization, the platform gates it behind three independent controls: the action must be explicitly registered in a trusted grant list, it must be declared @action(authz='always'), and — for methods implemented in a runtime language — its source code must match a recorded fingerprint. Trust is withdrawn automatically when the fingerprint no longer matches, or explicitly by removing or disabling a grant.

Terminology

  • Action — a single method invocation in a running dispatch. Actions form a parent/child chain: the root action is the entry point the user called, and every method it invokes becomes a child action of it. See Action.
  • Privileged action — the parent action that holds the grant. It is identified by <Type>#<method> and, optionally, a source-code fingerprint.
  • Granted (child) action — an action that the privileged action is allowed to invoke even though the user is not otherwise authorized for it.
  • Grant list — the registry mapping each privileged action to the set of child actions it may invoke.

The end-to-end flow

Every action dispatched on the server passes through Authorizer#authorizeAction. Authorization for a single action resolves in the following order, and the first control that grants access wins:

  1. Fast-path grants. The action is authorized immediately if the caller is root or a cluster admin, if the action itself is a cluster-admin action the caller holds, if it is an authentication action, or if it is a plain dispatcher with no authorization action groups.

  2. Inline methods. An inline method has no frame of its own — its body executes directly in the caller's frame — so there is no separate action to authorize. Inline methods are therefore auto-authorized as part of their caller. Because of this, a privileged @action must never be an inline method, and (see the fingerprint rule below) a privileged type must not be remixed: those two rules keep the trusted, fingerprinted body the only code that can run under a privileged grant.

    Auto-authorizing an inline method only lets its body run in-frame; it never confers authority on the child actions that body dispatches. So even if a privileged action calls an inline method whose body an app has remixed, any protected child action that body invokes is still authorized on its own — and is granted only if it is on the privileged action's grant list (with a matching fingerprint). The grant-list check on the child dispatch, not the inline auto-authorization of the wrapper, is the boundary that contains a remixed inline body.

  3. Security level. If the action carries a SecurityLevel the caller does not meet, access is denied and the flow falls through to the privileged-action check.

  4. Role permissions. The action is authorized if any of the caller's roles grants a Permission for it. See Define Permissions.

  5. Privileged-action check. If none of the above grants access, PrivilegedAction#authorize is the last resort. It asks the privileged-action registry whether some ancestor of the current action is a registered privileged action that is allowed to invoke this action. If so, access is granted; otherwise the action is denied (and, when requested, a NotAuthorized exception is thrown).

How the privileged-action check resolves

The privileged-action check looks up the current child action in the reverse grant map, then walks the action's parent chain looking for the matching privileged action. When it finds a candidate parent, it enforces two more conditions before granting access:

  • The privileged action must be @action(authz='always'). This forces the privileged action itself to be authorized on every call, so a caller can never reach the privileged method unless they were explicitly permitted to run it. If the registered privileged action is not authz='always', the grant is rejected and a warning is logged.

  • The source-code fingerprint must match (runtime-implemented methods only). For a method implemented in a runtime language (for example JavaScript), the grant records a fingerprint of the method's source file. At authorization time the platform recomputes the fingerprint of the deployed source and compares it to the recorded value. If they differ, the grant is rejected. This ensures that the trusted body cannot be silently altered after review — any change to the source invalidates the grant until the fingerprint is updated through review. Methods implemented in Java carry no fingerprint, because the compiled platform artifact is the unit of trust.

Only when the parent is a registered privileged action, is authz='always', and (where applicable) has a matching fingerprint does the child action gain access. Both the baseline registry and the runtime grants (below) are checked in the same single walk of the parent chain and are verified identically.

Where grants come from

Grants are drawn from two sources, and a child action is authorized if either source grants it:

  • The immutable baseline. A code-reviewed registry compiled into the platform (in the PrivilegedAction implementation). It cannot be changed at runtime and is the primary place platform- and package-shipped privileged actions are declared.

  • The cluster-admin-managed runtime extension. A cluster admin can extend the baseline from a running cluster without a platform rebuild, through PrivilegedAction.Config. This is described in Extending grants at runtime below.

Registering a baseline privileged action

A baseline privileged action is registered by mapping its identity to the child actions it is permitted to invoke. Each entry is keyed by "<Type>#<method> <fingerprint>" (the fingerprint is omitted for Java-implemented methods) and lists the child actions — as "<Type>#<action>" or the bare "<Type>" to allow all of that type's actions — that the privileged action may call.

Registering a new baseline privileged action is a security-sensitive change and must go through code review, because it intentionally widens what an unprivileged caller can cause to happen.

Extending grants at runtime

The baseline is fixed at build time, but a cluster admin can add further grants at runtime through PrivilegedAction.Config. Runtime grants extend — never relax — the baseline: every runtime grant is subject to the exact same checks (the granting action must be @action(authz='always'), and the recorded fingerprint must match).

Because PrivilegedAction.Config follows the standard Config framework override precedence and its mutators are in the cluster-admin action group, only a cluster admin (or root) can change runtime grants, so an application cannot use them to escalate its own privileges.

The runtime surface is:

Each runtime grant is a PrivilegedAction.Grant value with a privilegedAction ("<Type>#<action>"), an actionFingerprint (required; pins the grant to a specific version of the action's source), and grantedChildren (the child actions to grant, each "<Type>#<childAction>" or a bare "<Type>"). add merges grantedChildren into an existing grant with the same privilegedAction and actionFingerprint; remove matches on those two fields only and ignores grantedChildren.

For a task-oriented walkthrough of the runtime API, see Grant Privileged Actions at Runtime.

How trust is withdrawn

Trust in a privileged action is not permanent. A grant may need to be withdrawn when the trusted code is found to be unsafe, when it should no longer run, or when a runtime grant was a mistake. The platform withdraws trust through three mechanisms, from automatic to explicit.

Fingerprint invalidation (automatic)

For a method implemented in a runtime language, the grant records a fingerprint of the method's source. If the trusted body is modified after review, its recomputed fingerprint no longer matches the recorded one, and the grant stops applying automatically until the fingerprint is updated through review. No operator action is required — changing the source is itself sufficient to invalidate the grant. Methods implemented in Java carry no fingerprint, because the compiled platform artifact is the unit of trust and can only change through a platform release.

Removing a runtime grant

A cluster admin can revoke a specific runtime grant with PrivilegedAction#remove, matching it by its privilegedAction and actionFingerprint. This withdraws exactly that grant while leaving the rest of PrivilegedAction.Config — and the immutable baseline — untouched. It is the right response when a single runtime grant should no longer be honored.

Disabling all runtime grants (break-glass)

PrivilegedAction#disableDynamicGrants is a kill-switch: it makes the platform ignore every runtime grant at once, leaving only the immutable baseline in effect, without clearing the configured grants. It applies uniformly across the cluster and is the fastest response to a suspected problem with runtime grants. Because it does not clear the grants, PrivilegedAction#enableDynamicGrants restores them once the issue is resolved.

The immutable baseline cannot be withdrawn at runtime by any of these mechanisms; a baseline grant is changed only by a code-reviewed platform change.

See also

Was this page helpful?