Make snapshot attribute validation enforce a real rule
Closes gap G06. checkNoUnknownAttributes was named for a rule it did not
implement: it rejected blank keys, which is not an allow-list, so a snapshot
carrying malformed or shadowing attribute names passed a check whose name said
otherwise. A rule that passes for the wrong reason is worse than an absent one,
because the report is read as evidence.
raw_attributes_well_formed requires each key to be a valid LDAP attribute
descriptor, forbids shadowing an attribute the canonical model owns (uid, cn,
mail) in any casing, and rejects keys differing only by case, which LDAP treats
as one attribute. spec/ldap-schema.yaml carries the same wording.
Two corrections to the gap's description rather than implementations of it. An
allow-list is not derivable: ldapAttributes is defined as what the canonical
model does not cover, so the schema cannot enumerate what may appear there. And
the reference constraint already existed -- checkValidGroupMemberships only
checks emptiness, but the semantic rule checkReferencedUsersExist resolves every
member against the user set.
Neutering the rule fails four of the five new tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-07 23:22:55 +02:00
|
|
|
---
|
|
|
|
|
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"
|
2026-09-07 23:23:38 +02:00
|
|
|
state_hub_workstream_id: "733ad1a5-c4d8-5c2f-8608-c51bf1d5b17e"
|
Make snapshot attribute validation enforce a real rule
Closes gap G06. checkNoUnknownAttributes was named for a rule it did not
implement: it rejected blank keys, which is not an allow-list, so a snapshot
carrying malformed or shadowing attribute names passed a check whose name said
otherwise. A rule that passes for the wrong reason is worse than an absent one,
because the report is read as evidence.
raw_attributes_well_formed requires each key to be a valid LDAP attribute
descriptor, forbids shadowing an attribute the canonical model owns (uid, cn,
mail) in any casing, and rejects keys differing only by case, which LDAP treats
as one attribute. spec/ldap-schema.yaml carries the same wording.
Two corrections to the gap's description rather than implementations of it. An
allow-list is not derivable: ldapAttributes is defined as what the canonical
model does not cover, so the schema cannot enumerate what may appear there. And
the reference constraint already existed -- checkValidGroupMemberships only
checks emptiness, but the semantic rule checkReferencedUsersExist resolves every
member against the user set.
Neutering the rule fails four of the five new tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-07 23:22:55 +02:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
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
|
2026-09-07 23:23:38 +02:00
|
|
|
state_hub_task_id: "e2921983-853c-501b-8dfc-158751dea093"
|
Make snapshot attribute validation enforce a real rule
Closes gap G06. checkNoUnknownAttributes was named for a rule it did not
implement: it rejected blank keys, which is not an allow-list, so a snapshot
carrying malformed or shadowing attribute names passed a check whose name said
otherwise. A rule that passes for the wrong reason is worse than an absent one,
because the report is read as evidence.
raw_attributes_well_formed requires each key to be a valid LDAP attribute
descriptor, forbids shadowing an attribute the canonical model owns (uid, cn,
mail) in any casing, and rejects keys differing only by case, which LDAP treats
as one attribute. spec/ldap-schema.yaml carries the same wording.
Two corrections to the gap's description rather than implementations of it. An
allow-list is not derivable: ldapAttributes is defined as what the canonical
model does not cover, so the schema cannot enumerate what may appear there. And
the reference constraint already existed -- checkValidGroupMemberships only
checks emptiness, but the semantic rule checkReferencedUsersExist resolves every
member against the user set.
Neutering the rule fails four of the five new tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-07 23:22:55 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`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
|
2026-09-07 23:23:38 +02:00
|
|
|
state_hub_task_id: "de4caf0a-39c6-5148-a986-db18a1308505"
|
Make snapshot attribute validation enforce a real rule
Closes gap G06. checkNoUnknownAttributes was named for a rule it did not
implement: it rejected blank keys, which is not an allow-list, so a snapshot
carrying malformed or shadowing attribute names passed a check whose name said
otherwise. A rule that passes for the wrong reason is worse than an absent one,
because the report is read as evidence.
raw_attributes_well_formed requires each key to be a valid LDAP attribute
descriptor, forbids shadowing an attribute the canonical model owns (uid, cn,
mail) in any casing, and rejects keys differing only by case, which LDAP treats
as one attribute. spec/ldap-schema.yaml carries the same wording.
Two corrections to the gap's description rather than implementations of it. An
allow-list is not derivable: ldapAttributes is defined as what the canonical
model does not cover, so the schema cannot enumerate what may appear there. And
the reference constraint already existed -- checkValidGroupMemberships only
checks emptiness, but the semantic rule checkReferencedUsersExist resolves every
member against the user set.
Neutering the rule fails four of the five new tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-07 23:22:55 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
2026-09-07 23:23:38 +02:00
|
|
|
state_hub_task_id: "8a48c6cc-1e7e-5fd4-abfa-2b893c3d400d"
|
Make snapshot attribute validation enforce a real rule
Closes gap G06. checkNoUnknownAttributes was named for a rule it did not
implement: it rejected blank keys, which is not an allow-list, so a snapshot
carrying malformed or shadowing attribute names passed a check whose name said
otherwise. A rule that passes for the wrong reason is worse than an absent one,
because the report is read as evidence.
raw_attributes_well_formed requires each key to be a valid LDAP attribute
descriptor, forbids shadowing an attribute the canonical model owns (uid, cn,
mail) in any casing, and rejects keys differing only by case, which LDAP treats
as one attribute. spec/ldap-schema.yaml carries the same wording.
Two corrections to the gap's description rather than implementations of it. An
allow-list is not derivable: ldapAttributes is defined as what the canonical
model does not cover, so the schema cannot enumerate what may appear there. And
the reference constraint already existed -- checkValidGroupMemberships only
checks emptiness, but the semantic rule checkReferencedUsersExist resolves every
member against the user set.
Neutering the rule fails four of the five new tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-07 23:22:55 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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.
|