Skip to content

feat(permissions): opt-in partial grant ownership — one module-scoped grant currently blocks 403 of 590 grants #102

Closed

Description

@2000game

Problem

ct.groupRole owns a whole role instance: every live grant absent from the declaration lands in
toDelete. So one unmanageable grant makes the entire role undeclarable — the repo's own doctrine
is that "a partial declaration is strictly worse than none: it turns a clean no-op into a
destructive plan".

On eqrm.church.tools that costs most of the estate. Of 590 authored per-group grants across 59
role instances
, only 187 (44 instances) are declarable. The other 403 grants (15 instances)
are blocked — and often by a tiny minority of the grants in the role:

Role instance Grants Blocked by
Team Office / Leiter 41 1 dimension: cc_html_template
Team Office / Organisator 41 1: cc_html_template
Team Equip / Leiter, Organisator 5 each 1: cc_html_template
Area Administration / Leiter, Organisator 4 each 1: cc_html_template
Bereich Pastoral Care / Leiter, Organisator, Teilnehmer 1 each 1: cdb_comment_viewer
sIntegrationmeeting / Mitglied 43 ccm_data_category, cdb_comment_viewer
Implementierung Churchtools / Leiter, Mitglied 116, 113 11–12 module dimensions

Every blocking dimension is module data — calendars, wiki, services, songs, resources, finance
queries, flow categories, OAuth clients — i.e. things the tool deliberately does not manage and
never will. The structural grants sitting next to them are perfectly expressible.

What is missing

An explicit, opt-in way to say: own these rights on this role; leave anything you do not recognise
alone.
Sketch:

ct.groupRole({
  key: "team_office_leiter",
  group: "team_office",
  role: "Leiter",
  // Do not revoke live grants that are absent from `grants` — this role also carries
  // cc_html_template grants that this scaffold has no business owning.
  preserveUnknown: true,
  grants: [ /* the 40 structural ones */ ],
});

Requirements for it to be safe:

  • Opt-in per declaration, never a global default — the current strict behaviour is the right
    default and must stay.
  • Loud in the plan: render the preserved grants explicitly (~ N grant(s) preserved, not managed), so "I forgot one" and "I deliberately left module grants alone" never look alike.
  • Ideally scoped, e.g. preserveUnknown: ["cc_html_template", "ccm_data_category"], so a
    genuinely unexpected new grant on a managed dimension still shows up as drift rather than being
    swallowed.

That last variant is the one I would build: it keeps the blast radius of the escape hatch to
dimensions the author has consciously excluded.

Acceptance

  • A role can be declared with unknown grants preserved rather than revoked, opt-in only.
  • The plan renders preserved grants explicitly; they are never invisible.
  • Restricting preservation to named scope dimensions is supported.
  • Default behaviour is unchanged: undeclared grant → revoke.

Context: full scope composition in eqrm/ct-structure docs/coverage-gaps.md.

Activity

  1. 2000game commented on Aug 14, 2026

    @2000game
    MemberAuthor

    The cdb_comment_viewer half is solved — via REST, not the legacy endpoint (PR #112)

    #109 suggested this dimension would need the master-data registry. It does not: /person/commentviewers is conventional REST — [{id, name, sortKey}] plus POST/PUT/DELETE on the item path (live-probed on eqrm-dev, CT 3.135.2, 2026-08-14).

    So it is just another catalog ref:

    { right: "churchdb:view comments", scope: [{ commentViewer: "gemeindeleitung" }] }

    ct get comment-viewers lists the names. This unblocks the three role instances on Bereich Pastoral Care that were each blocked by exactly one comment_viewer-scoped grant, so the whole role instance fell out of ct coverage as undeclarable.

    One detail worth knowing: id: 0 is a real row on a live instance ("Alle"), so anything treating a falsy id as "not found" would silently drop the scope. There is a test pinning that.

    Comment viewers are a catalog ref for now, not a managed resource — their REST surface would fit the resource registry unchanged, so promoting them later is a scope decision rather than a technical one. Nothing has needed to declare one yet.

    The preserveUnknown half of this issue is unaffected.

  2. added a commit that references this issue on Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageUnsorted intake — decide in the weekly sweep

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions