Skip to content

sec(auth): Standard Users and Read-Only Users are not system_managed, so #1737's guard does not protect them #168

Description

@cristim

Two seeded groups are not system_managed, so PUT /api/groups/{id} can corrupt their permissions even after PR LeanerCloud/cloud-commitments-cli#1737 lands its grant ceiling and system_managed guard.

group id system_managed
Administrators + 3 others …000001–…000004 TRUE (000059)
Purchaser …000007 TRUE (000064)
Standard Users …000005 FALSE
Read-Only Users …000006 FALSE

Traced through every migration touching the column: 000057 inserts both with an INSERT column list that omits system_managed, so they take DEFAULT FALSE. 000059's UPDATE … SET system_managed = TRUE covers only …000001–…000004. 000064 covers Purchaser. 000086 and 000088 modify those groups' permissions but never the column.

Why LeanerCloud/cloud-commitments-cli#1737's ceiling does not cover them

Two independent reasons, both read out of the committed code:

  1. A narrowing write is invisible to the ceiling. checkGrantCeiling (internal/auth/group_ceiling.go:81) iterates the requested permissions only. It never diffs against existing to detect removals. sec(frontend): group edit drops unrepresentable permissions and widens their resource to * cloud-commitments-cli#1629's primary harm is the group-edit form silently dropping cancel-own / retry-own / approve-own on purchases — all three present on Standard Users (000057:30-32), none representable in the form's hardcoded action list. That write passes the ceiling untouched.
  2. Admin widening is permitted for non-carved-out verbs, which is sec(frontend): group edit drops unrepresentable permissions and widens their resource to * cloud-commitments-cli#1629's exact scenario. grantCeilingAllows (:104) checks checkAdminPermission(held) first; view:* is not in adminCarvedOuts, so it returns true. view:history widens to view:*.

LeanerCloud/cloud-commitments-cli#1629 names this group explicitly: "The seeded user role-mirror group has the same shape."

What is and is not affected

The decision this needs — a product call, not an engineering one

Should Standard Users and Read-Only Users be system_managed?

  • If they are meant to be fixed role mirrors, a one-line migration setting the column TRUE closes this entirely and is the smallest possible fix.
  • If operators are meant to customise them — plausible, since "Standard Users" reads like a default an organisation would tailor — then locking them is wrong, and the gap must instead be closed by making the ceiling detect removals as well as additions. That is a larger change with its own risk: an over-strict removal check would block legitimate permission tidying.

This was deliberately not decided inside LeanerCloud/cloud-commitments-cli#1737. Marking a seeded group immutable changes what operators can configure, which is not a call to make as a side effect of a security PR.

If the removal-detection route is chosen

Note it is a genuinely different guard from the grant ceiling. "You may not grant what you do not hold" and "you may not silently drop what you did not intend to" are separate properties, and a single check that tries to be both will likely get one of them wrong. Any implementation needs a negative control proving deliberate permission removal still works.

Found during the pre-push review of LeanerCloud/cloud-commitments-cli#1737.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions