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(...), thenpermissions.authorize([{ permission }], { credentials })returns ALLOW or DENY; throwNotAllowedError(403) on DENY. - Policy: one class with
handle(request, user)decides for every plugin, set by a module on the permission plugin throughpolicyExtensionPoint.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=acme | A module that sets your policy |
yarn --cwd packages/backend remove @backstage/plugin-permission-backend-module-allow-all-policy | Drop allow-all |
yarn --cwd plugins/readiness-backend test --watchAll=false | Including 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.