--- id: KEY-WP-0021 type: workplan title: "Make snapshot attribute validation enforce a real rule" domain: infotech repo: key-cape status: finished owner: claude topic_slug: snapshot-attribute-validation created: "2026-09-07" updated: "2026-09-07" state_hub_workstream_id: "733ad1a5-c4d8-5c2f-8608-c51bf1d5b17e" --- Closes gap G06 of `history/2026-09-05-011726-scope-intent-assessment.md`: `checkNoUnknownAttributes` is named for a rule it does not implement. It rejects blank keys, which is not an allow-list, so a snapshot carrying arbitrary or malformed LDAP attribute names passes a check whose name says otherwise. A rule that passes for the wrong reason is worse than an absent one, because a report listing `no_unknown_attributes: passed` is read as evidence. One part of the assessment's description needs correcting rather than implementing: it says group membership validation checks nonempty member IDs rather than complete referenced identity existence. `checkValidGroupMemberships` does only check emptiness, but the semantic rule `checkReferencedUsersExist` already resolves every member against the user set and fails on an unknown one. The reference constraint exists; it lives in a different rule than the one the assessment looked at. ## Enforce real attribute constraints ```task id: KEY-WP-0021-T01 status: done priority: medium state_hub_task_id: "e2921983-853c-501b-8dfc-158751dea093" ``` `ldapAttributes` is defined by the canonical model as raw attributes *not covered by* the model, so an allow-list of permitted names cannot be derived from the schema — the field exists precisely to carry what the schema does not name. Two constraints are derivable and worth enforcing: - the attribute name must be a syntactically valid LDAP attribute descriptor (RFC 4512: a letter followed by letters, digits or hyphens, or a numeric OID), since a name that cannot be written to a directory cannot round-trip; and - it must not shadow a canonical field's documented LDAP mapping (`uid`, `cn`, `mail`), because two sources for one value is how an export and a directory come to disagree, and the model already owns those. Attribute descriptors are case-insensitive in LDAP, so keys differing only by case are a collision, not two attributes. Keep the blank-key check. Rename the rule to describe what it actually enforces rather than leaving a name that overpromises. Implemented as `raw_attributes_well_formed`, replacing `no_unknown_attributes` in both `validator.go` and `spec/ldap-schema.yaml`, with the schema entry stating plainly that this is not an allow-list and why one cannot be derived. ## Prove each rule with an invalid snapshot ```task id: KEY-WP-0021-T02 status: done priority: medium state_hub_task_id: "de4caf0a-39c6-5148-a986-db18a1308505" ``` The assessment asks for invalid-snapshot cases proving each stated rule. Add one per constraint — malformed descriptor, shadowed canonical mapping, case-only duplicate, blank key — plus passing cases for legitimate raw attributes and a valid numeric OID, so the rule is shown to be selective rather than merely strict. Eleven malformed-descriptor cases, six canonical-mapping cases across casings, a case-only duplicate, a message-content case, and five accepted-attribute cases. Confirmed they test the rule rather than merely passing: making the check return early fails four of the five tests. ## Reconcile the records ```task id: KEY-WP-0021-T03 status: done priority: medium state_hub_task_id: "8a48c6cc-1e7e-5fd4-abfa-2b893c3d400d" ``` Update `SCOPE.md` and G06's status. State what the rule now enforces and, just as importantly, what it still does not: this is not schema-derived validation of every attribute against a directory schema, and the canonical model cannot provide one for a field defined as everything the model does not cover. Correct the assessment's reference-constraint premise rather than restating it. `history/…-scope-intent-assessment.md` was being edited by a concurrent session when this workplan was written; make this edit only against a clean tree. Done against a clean tree after the other session committed. G06's status records both corrections: no allow-list is derivable for a field defined as what the model does not cover, and the reference constraint was already implemented in `checkReferencedUsersExist` rather than missing.