Skip to content

bug(dynamic): unportablized ruleset ids are written silently — wrong-host rulesets fail quietly, not loudly #101

Closed

Description

@2000game

Problem

Ruleset portablization only rewrites references to managed resources. Anything pointing at an
unmanaged target stays a raw numeric id — and nothing says so.

Across eqrm/ct-structure's rulesets/ (9 files), portablized: group-type-role ×22,
campus ×7, group-type ×4, group ×2. Still raw in 5 of 9 files:

Ruleset Dimensions left numeric
skidscheckinopskoblenz (adopted 2026-08-13) ctgroup.id, role.id
sflow2_5viewer ctgroup.groupTypeId, role.id
merkmal_alle_2_5_mz ctgroup.id, person.campusId
sintegrationmeeting ctgroup.id
skidscheckinopsmz ctgroup.id

Why it matters: it fails SILENTLY and WRONGLY

ChurchTools treats a ruleset as opaque JSON. It does not validate the ids inside. So a ruleset
carrying prod's ctgroup.id applied to dev does not error — the auto-group simply collects the
wrong people
, or nobody, and ct plan stays green because the ruleset round-trips byte-identically
against the host it was written for.

Compare with how #98 handled the same class of problem for permission scopes: an unresolvable
{ department: … } ref is a hard error, explicitly "strictly better than a silent numeric
misgrant". Rulesets currently take the opposite position for the same kind of id.

This is materially worse than a wrong permission scope, because the payload of an auto-group is
group membership — and in this instance Struktur membership carries 33 grants.

Suggested fix

Ranked by value:

  1. Report what could not be portablized. At ct adopt --with-dynamic and at plan time, list
    each dimension left numeric and why (target not managed / no ref kind for this dimension). Even
    with no other change this converts silent wrongness into a known risk.
  2. Extend the ref kinds to the dimensions that have resolvable catalogs — role.id maps to
    role-def (/group/roles), which already exists as a RefKind, and person.campusId /
    ctgroup.groupTypeId are just campus and group-type refs appearing in a ruleset position rather
    than a scope position.
  3. ctgroup.id needs the target group adopted, exactly like a cdb_gruppe scope — so the
    right behaviour is the feat(permissions): portable scope refs for non-group dimensions (cdb_station, cdb_bereich, cdb_gruppentyp) #98 one: resolve if managed, otherwise error (or warn under an opt-out)
    rather than freeze prod's id into a cross-host file.

A strictRulesets: true config switch that refuses to write a ruleset containing an
unportablized id would let a repo opt into the safe behaviour before it becomes the default.

Acceptance

  • Adoption reports every dimension it left numeric, with the reason.
  • role.id, person.campusId and ctgroup.groupTypeId portablize when the target resolves.
  • ctgroup.id portablizes when the target is managed; otherwise it is surfaced, not swallowed.
  • A repo can opt into "refuse to write an unportablized ruleset".

Related: #76 (the original portablization work), #98 (the resolve-or-error precedent for scopes).

Activity

  1. 2000game commented on Aug 13, 2026

    @2000game
    MemberAuthor

    Live evidence: a plan that is about to write prod ids onto another host

    The dev plan for eqrm/ct-structure#19, adopting sKidsCheckinOpsKoblenz from prod and applying it to dev:

    + group.skidscheckinopskoblenz
        groupStatusId: 1
        name: "sKidsCheckinOpsKoblenz"
        groupTypeId: 15                      ← correctly resolved (prod 9 → dev 15)
        dynamic: {"description":"Automatische Mitgliedschaft für Gruppe #1092",
                  …"oneof":[{"var":"ctgroup.id"},[3200,3090]]…
                  …"oneof":[{"var":"role.id"},[84,85,17,16]]…
                  …"groupTypeRoleId":66…}
    
    Plan: 1 to create, 0 to update, 0 to delete.
    

    Everything ct models as a resource portablized correctly — groupTypeId resolved 9 → 15 across hosts. Everything inside the ruleset did not:

    • ctgroup.id: [3200, 3090] are prod group ids (3200 = Bereich Kids Koblenz on prod).
    • role.id: [84,85,17,16] and groupTypeRoleId: 66 are prod ids.
    • Even the human-readable description embeds prod's group #1092.

    On dev those ids address different groups or none. CT accepts the ruleset regardless, so the outcome is an auto-group that quietly collects the wrong people — with a green plan, a green apply, and a subsequent ct plan that is a clean no-op because the ruleset round-trips byte-identically.

    Note the contrast within a single plan line: the resource fields are portable and the ruleset payload is not, yet nothing in the output distinguishes them. That is the whole of this issue in one diff — the plan renders host-specific ids and portable refs identically, so a reviewer has no signal.

    This is also why the dev instance cannot currently serve as a faithful rehearsal for auto-groups: the rehearsal exercises the apply path but not the ruleset semantics.

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