Skip to content

bug(permissions): group + its own groupRole in one config can never plan on a fresh host (the domain half of #29) #106

Closed

Description

@2000game

Problem

#29 fixed the scope-resolution deadlock: a group and a grant scoped to that group can now be
declared in one config, because a pending scope ref is re-resolved at apply time after the resource
tier runs.

The domain side of the same problem is still a hard error. A ct.groupRole whose group is
declared in the same config but does not yet exist on the target host cannot plan at all:

✗ Cannot resolve group-role(group=skidscheckinopskoblenz, role=Mitglied) referenced at
  group_role "skidscheckinopskoblenz_mitglied".domainId on https://eqrm-dev.church.tools:
  group "skidscheckinopskoblenz" is declared in this config but not yet created — its
  (group, role) pairing id only exists once the group does. Apply the group first, then
  re-run, or pass a numeric id.

This is deliberate — resolveGroupRole documents it:

Returns a number — never a PendingRef: the pairing id only exists once the group does, so a
same-run-declared (not-yet-created) group is a hard error here

Why it bites in practice

It makes a config non-portable across hosts by construction. The exact same config:

  • plans clean on prod, where the group already exists, and
  • hard-fails on dev, where it does not.

So the promotion model the tool is built around — one config, dev rehearses what prod will get —
breaks for any new group that carries per-group grants. In eqrm/ct-structure this surfaced the
moment we adopted sKidsCheckinOpsKoblenz: prod plan a clean no-op, dev plan exit 1, PR check red.

The workaround is to split every such change into two merges — land the group, wait for apply-dev
to create it, then add the grants in a follow-up PR. That doubles the review cycle for what is
conceptually one change, and it is easy to forget the second half, leaving a Struktur group in
place with none of the rights it exists to carry.

The suggested "pass a numeric id" escape hatch does not help here: the pairing id is host-specific
(prod 44675), which is precisely what a shared config must not contain.

Why it should be solvable

The information needed at apply time is available. Tier ordering already guarantees the group is
created before permissions are applied — executePlan runs the resource tiers first, and
applyPermissionPlan is explicitly called after it, with a comment saying it re-resolves scope
dataIds against the post-execute state for exactly this reason. The domain id could be resolved in
the same window: after the group exists, GET /groups/{id}/roles yields the pairing id, and the
role name → pairing mapping is what resolveGroupRole already does.

The plan-time rendering can stay honest by showing the domain as pending, the way pending scope
refs already render.

Acceptance

  • A config declaring a group and a ct.groupRole on that group plans on a host where the group
    does not yet exist, rendering the domain as pending.
  • ct apply creates the group, then resolves the pairing id and applies the grants in one run.
  • A role name that does not exist on the created group is still a hard error, with the
    available roles listed as today.
  • The same config plans identically on a host where the group already exists (no regression).

Related: #29 (the scope half of this deadlock), #25 (group_role domains by (group, role) pair).

Activity

  1. added 2 commits that reference this issue on Aug 14, 2026
  2. 2000game commented on Aug 14, 2026

    @2000game
    MemberAuthor

    Shipped in #112 (merged 2026-08-14). A ct.groupRole whose group is declared in the same config but absent from the target host now resolves to a pending domain ref, completed at apply time from the freshly created group's own /groups/{id}/roles. See src/permissions/plan.ts (pendingDomain) and tests/permission-pending-domain.test.ts.

    (Not auto-closed: the PR body's Closes **#106** bolding defeats GitHub's keyword parser.)

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