PlatformEngr® Backstage plugins course

Cheat notes · Module 08 · Secure and extend

Permissions: who may evaluate

A five-minute review for when you have done this module and want it back in your head. Open the full module for the steps and the lab, or the full text to print.

The idea

Define permissions in -common, check them in the backend before acting, and let one policy decide for the whole portal. The frontend check only hides buttons; the backend check is the guard.

Remember

  • Define: createPermission({ name: 'readiness.evaluate', attributes: { action: 'update' } }) in -common, so the backend checks it and the frontend can ask about it.
  • Register and enforce: permissionsRegistry.addPermissions(...), then permissions.authorize([{ permission }], { credentials }) returns ALLOW or DENY; throw NotAllowedError (403) on DENY.
  • Policy: one class with handle(request, user) decides for every plugin, set by a module on the permission plugin through policyExtensionPoint.setPolicy(...). It replaces the allow-all policy.
  • Groups come from sign-in: the policy reads user.info.ownershipEntityRefs (e.g. group:default/platform). After changing membership, sign out and in.
  • usePermission({ permission }) disables the button for people the policy would refuse: a convenience, not a guard.
  • Service principals (plugins, static tokens) are not users: service calls are allowed, so scripts still work.

Pitfalls

  • Dropping the backend check because the button is disabled anyway: the UI looks the same, and anyone with curl can evaluate. Ask in every review: "what happens if someone calls this with curl?"
  • Testing the policy only by clicking. Write one test per permission with mockServices.permissions.factory({ result: AuthorizeResult.DENY }).

Commands

yarn new --select permission-policy-module --option moduleId=acmeA module that sets your policy
yarn --cwd packages/backend remove @backstage/plugin-permission-backend-module-allow-all-policyDrop allow-all
yarn --cwd plugins/readiness-backend test --watchAll=falseIncluding the test that expects 403

A policy

export class AcmePolicy implements PermissionPolicy {
  async handle(request: PolicyQuery, user?: PolicyQueryUser): Promise<PolicyDecision> {
    if (isPermission(request.permission, readinessEvaluatePermission)) {
      const refs = user?.info.ownershipEntityRefs ?? [];
      return {
        result: refs.includes('group:default/platform')
          ? AuthorizeResult.ALLOW
          : AuthorizeResult.DENY,
      };
    }
    return { result: AuthorizeResult.ALLOW };
  }
}

Test yourself

Answer in your head first, then open the answer.

Why do the permissions live in readiness-common?

The backend checks them and the frontend asks about them, so both import them.

The button is disabled for you. Can you still evaluate?

Only if the backend does not check. The button is a hint; the backend decides.

How many permission policies does a backend have?

One, set by a module on the permission plugin, deciding for every plugin.

Where module 08 leaves you: Only members of platform may evaluate on demand; everyone may read scorecards. A test proves the backend says 403. Merged.