Skip to content

fix(dashboard): let dashboard users read backup CRD schemas - #4236

Merged
Andrey Kolkov (androndo) merged 1 commit into
mainfrom
fix/backup-tenant-crd-rbac
Sep 17, 2026
Merged

Andrey Kolkov (androndo) merged 1 commit into
mainfrom
fix/backup-tenant-crd-rbac

Conversation

@androndo

@androndo Andrey Kolkov (androndo) commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

The dashboard's backup build/edit forms (BackupPlanCreatePage, BackupJobCreatePage, BackupRestoreJobCreatePage, BackupCreatePage, BackupClassCreatePage, BackupClassEditPage, BackupResourceEditPage) build their form from the CRD's OpenAPI schema, which useCRDSchema fetches directly from apiextensions.k8s.io with the logged-in user's token — a cluster-scoped get on the CRD by name.

No role a tenant user carries granted customresourcedefinitions, so every tenant user — tenant super-admin included — hit a 403 on the schema lookup and the form never rendered, even though they already hold write access to the backup CRs through cozy:backups:admin. kubectl kept working because that only touches the CRs, not their CRDs.

The tenant personas (cozy:tenant:{view,use,admin,super-admin}) are bound only by RoleBindings, which authorize namespaced requests but never a cluster-scoped one, so the grant belongs on cozystack-dashboard-readonly in packages/system/dashboard/templates/rbac.yaml: it is bound to system:authenticated by a ClusterRoleBinding and already carries the dashboard's other cluster-scoped reads (storageclasses, applicationdefinitions, backupclasses). This adds a get on the five backups.cozystack.io CRDs, scoped by resourceNames to those schemas only — exactly the by-name get useCRDSchema performs, no wildcard and no collection listing.

Verification: helm template renders cozystack-dashboard-readonly with the new rule cleanly; the real contract is kubectl auth can-i get customresourcedefinitions.apiextensions.k8s.io/plans.backups.cozystack.io --as-group=<tenant>-super-admin, which belongs in e2e.

Fixes: #4053

Downstream repositories

  • No downstream repository is affected by this change

Release note

fix(dashboard): grant dashboard users read access to the backup CRD schemas so the backup create/edit forms stop returning 403

Summary by CodeRabbit

  • Access Updates
    • Read-only dashboard access now includes viewing the definitions of five backup-related resource types: plans, backup jobs, restore jobs, backups, and backup classes.
    • This enables the dashboard to recognize and display information about these backup resource definitions while maintaining read-only access.

@androndo Andrey Kolkov (androndo) added the kind/backport Categorizes issue or PR as requiring a backport to the current release line label Sep 14, 2026
@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug size/S This PR changes 10-29 lines, ignoring generated files labels Sep 14, 2026
@coderabbitai

coderabbitai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: ce2dc765-35d6-4d60-a286-048c440e4249

📥 Commits

Reviewing files that changed from the base of the PR and between f3207cf and 3a8a06b.

📒 Files selected for processing (1)
  • packages/system/dashboard/templates/rbac.yaml

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The dashboard read-only ClusterRole now permits get access to five backup-related CustomResourceDefinition objects. The permission applies only to the named CRDs in the backups.cozystack.io group.

Changes

Backup CRD access

Layer / File(s) Summary
Grant restricted CRD schema access
packages/system/dashboard/templates/rbac.yaml
The ClusterRole adds get permission for customresourcedefinitions in apiextensions.k8s.io, restricted to five backup-related CRD names.

Priority: ➖ Normal

Estimated code review effort: 1 (Trivial) | ~5 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 3a8a0

The change provides the intended narrowly scoped schema access without introducing an identified merge-blocking risk.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning FAIL. The new permission is in a ClusterRole bound to system:authenticated. Therefore, every authenticated user can read the five backup CRDs. #4053 requires tenant users, but it does not establish … Restrict the permission to the required tenant-user authorization path, such as tenant groups or a tenant-scoped aggregate role. Keep the five resourceNames and get verb. Do not bind this permission to system:authenticated unless broa…
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: granting dashboard users access to backup CRD schemas.
Linked Issues check ✅ Passed PASS. The change addresses #4053. cozystack-dashboard-readonly grants get on customresourcedefinitions in apiextensions.k8s.io for the five backup CRD names required by the forms. The existing…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Full details: Out of Scope Changes check

Explanation

FAIL. The new permission is in a ClusterRole bound to system:authenticated. Therefore, every authenticated user can read the five backup CRDs. #4053 requires tenant users, but it does not establish that all authenticated users need this backup-schema permission. The resourceNames restriction limits objects, not subjects.

Resolution

Restrict the permission to the required tenant-user authorization path, such as tenant groups or a tenant-scoped aggregate role. Keep the five resourceNames and get verb. Do not bind this permission to system:authenticated unless broader access is explicitly required and documented in scope.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/backup-tenant-crd-rbac

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM

This rule never reaches the authorizer. A get on a CRD is a cluster-scoped request; the 403 quoted in #4053 spells that out, "at the cluster scope". cozy:tenant:view is bound only by a RoleBinding, in packages/apps/tenant/templates/tenant.yaml. A RoleBinding pointing at a ClusterRole grants that role inside the binding's own namespace, and cluster-scoped requests are checked against ClusterRoleBindings. I grepped packages/ for cozy:tenant:view, cozy:tenant:use, cozy:tenant:admin and cozy:tenant:super-admin: four RoleBindings in that one file, and no ClusterRoleBinding on any tenant persona anywhere. The label and the aggregation chain are right, and a tenant user still gets the same 403.

A carrier that would work already exists. cozystack-dashboard-readonly in packages/system/dashboard/templates/rbac.yaml is bound to system:authenticated by a ClusterRoleBinding in the same file, and it already holds the cluster-scoped reads the dashboard depends on: storageclasses, applicationdefinitions, backupclasses. The five CRD names there would land in a binding that can authorize a cluster-scoped get. That reaches every authenticated user rather than tenant members only, same as the three rules already in that role.

The rule itself is fine otherwise: apiextensions.k8s.io, customresourcedefinitions, five resourceNames matching the five files under definitions/, verbs stopping at get, nothing wildcarded.

No test with it. A rendering test over this template would pass either way, because the render is correct and the binding is what is wrong. The check that catches it is kubectl auth can-i get customresourcedefinitions.apiextensions.k8s.io/plans.backups.cozystack.io --as-group=<tenant>-super-admin, and that belongs in e2e.

The red E2E is not from this diff. opensearch is the cluster-wide failure whose fix landed after this branch was cut. The four *-backup-roundtrip suites fail on the BackupJob reaching Failed during the S3 upload, which runs under the controller's own ServiceAccount, and tenant view permissions are not on that path.

@androndo Andrey Kolkov (androndo) changed the title fix(backup-controller): let tenant users read backup CRD schemas fix(dashboard): let dashboard users read backup CRD schemas Sep 15, 2026
@androndo

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) you're right — thanks for catching it. A RoleBinding pointing at the aggregated tenant ClusterRole can't authorize a cluster-scoped get on a CRD, so the rule never reached the authorizer and the 403 stood.

Moved the grant to cozystack-dashboard-readonly, which is bound to system:authenticated by a ClusterRoleBinding and already carries the dashboard's other cluster-scoped reads. Same rule otherwise: get on the five backups.cozystack.io CRDs, scoped by resourceNames, no wildcard, no list. Reverted the backup-controller change. Retitled to fix(dashboard): ....

Agreed on the red E2E being unrelated (the opensearch cluster-wide failure and the controller-SA S3 path in the *-backup-roundtrip suites), and on the real check being kubectl auth can-i ... --as-group=<tenant>-super-admin in e2e rather than a render test.

@androndo Andrey Kolkov (androndo) added area/dashboard Issues or PRs related to the dashboard / UI and removed area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review labels Sep 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

cozystack-dashboard-readonly is the right carrier. Its ClusterRoleBinding names system:authenticated, so a cluster-scoped get on a CRD is now checked against a binding that can authorize it, which is what round one was missing. The backup-controller copy is gone: customresourcedefinitions appears nowhere else under that package.

Five resourceNames, five files in definitions/, names match one for one. get alone is still right, since useCRDSchema does a by-name get and a rule carrying resourceNames can't authorize a list anyway.

No cluster on my side, so that authorization claim is read off the rendered objects rather than a real kubectl auth can-i. The console sends the user's own token (client.ts sets Authorization: Bearer), and every logged-in user carries system:authenticated.

The scope warning from the bot is half right. The wider audience is real. Its suggested fix, granting through a tenant-view aggregation path, is the same shape that never reached the authorizer last round, so don't take it. Widening is the standing tradeoff for this role: storageclasses went in for the same reason in v1.1.4, and applicationdefinitions and backupclasses sit there on the same terms.

Worth correcting the PR body before this merges, since the body lands on main. The verification line names --as-group=<tenant>-super-admin, which reads like a tenant-scoped grant. It isn't: the same check passes for any authenticated user carrying no tenant group at all.

Two follow-ups, neither of them this PR's to fix. The migration forms have the same bug: MigrationSourceCreatePage, MigrationSourceEditPage and MigrationTaskCreatePage call the same hook on vmimportsources.forklift.cozystack.io and vmimporttasks.forklift.cozystack.io, and cozy:migration:view grants forklift.cozystack.io resources only, so those forms still 403 on the schema lookup. Second, a test would discriminate now that the rule sits in this chart: tests/cluster_usage_rbac_test.yaml already pins a cluster-scoped grant together with its binding's subject in this package, and the same assert here would have gone red on the previous head.

The dashboard backup build/edit forms build each form from the CRD's
OpenAPI schema, which useCRDSchema fetches directly from
apiextensions.k8s.io with the logged-in user's token — a cluster-scoped
get on the CRD by name. No role a tenant user carries granted
customresourcedefinitions, so every tenant user hit a 403 on the schema
lookup and the form never rendered, while kubectl kept working through
the existing backup CR grants.

The tenant personas are bound only by RoleBindings, which cannot
authorize a cluster-scoped request, so the grant belongs on
cozystack-dashboard-readonly: it is bound to system:authenticated by a
ClusterRoleBinding and already carries the dashboard's other
cluster-scoped reads. Add a get on the five backups.cozystack.io CRDs,
scoped by resourceNames to those schemas only.

Fixes: #4053

Assisted-By: LLM
Signed-off-by: Andrey Kolkov <[email protected]>
@androndo
Andrey Kolkov (androndo) merged commit c9aa64c into main Sep 17, 2026
19 checks passed
@androndo
Andrey Kolkov (androndo) deleted the fix/backup-tenant-crd-rbac branch September 17, 2026 14:29
@github-actions

Copy link
Copy Markdown

Successfully created backport PR for release-1.6:

myasnikovdaniil added a commit that referenced this pull request Sep 23, 2026
…p CRD schemas (#4307)

# Description
Backport of #4236 to `release-1.6`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/dashboard Issues or PRs related to the dashboard / UI kind/backport Categorizes issue or PR as requiring a backport to the current release line kind/bug Categorizes issue or PR as related to a bug size/S This PR changes 10-29 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard backup forms return 403 for tenant users: the console reads CRDs from apiextensions.k8s.io, no tenant role grants it

2 participants