0.22.0: four ACL fixes and features (#104, #105, #96, #106) - #107
Conversation
Resolve `users:` keys by username or email (#104). Keys matched `user.name` only, which is email-shaped for SSO self-registrations and handle-shaped for admin-created accounts, so writing someone's email when their account uses a handle silently skipped the grant. Precedence now decides, a clash with a different account's email is a warning rather than a rejection of the tool's own `--yaml` capture, and only genuinely undecidable keys are fatal. Add `config.default_policy: true`, the floor every managed role stands on (#105). `config.default_role` is a fallback that fires only when no SSO mapping matched, so a specific grant used to cost a user the general one. Composing at the policy layer grants the same permissions without adding a role to anyone's set. Add per-bucket `config.no_preflight` under a new `buckets:` block (#96), so a cross-account bucket already prepared owner-side registers without the global all-or-nothing flag, and make a bucket that does not register a loud, named, non-zero-exit failure instead of one warning among many. Add `--create-and-email-users` (#106), which creates accounts for `sso.email` roster addresses that have none. CLI-only, because the registry mails a welcome and password-reset link as part of creating an account with no suppress flag; the addresses are printed before the prompt and a run over `--max-created-users` is refused.
drernie
left a comment
There was a problem hiding this comment.
Code review — 0.22.0 ACL changes
Reviewed the single commit (df1a46f, ~1000 lines of new logic in quiltx/acl.py and quiltx/tools/catalog/acl.py): the buckets: block with per-bucket config.no_preflight, config.default_policy, email-keyed users: resolution, --create-and-email-users, and the new failure-reporting blocks. Working tree clean; ./poe test passes (618 passed, 1 skipped).
Three findings, inline below — the first two make a documented configuration unusable:
--create-and-email-userscannot create anyone (quiltx/acl.py) — the derived username is the email address, which the registry rejects.config.default_policy+ anyconfig.unmanagedrole makes every successful apply exit 1 (quiltx/acl.py) — an informational note lands indiff.warnings, and_runexits 1 on any non-emptywarnings._unmakeable_accountsdouble-counts held addresses (quiltx/tools/catalog/acl.py) — minor reporting bug.
Areas I checked and found sound: per-bucket graphql_only_buckets selection and the narrowed control_account_id fetch in apply_acl; _resolve_configured_user precedence, collision, and the two fatal cases; default_policy composition after the ladder (no KeyError path — role_updates always holds every non-unmanaged role name, and source_policies/_synthesized_role_name stay untouched); default_policy_titles threading into the KeyError handlers; resolved_users plumbing through analyze_user_downgrades/_projected_user_access and the applied_user_names back-compat branch; the unregistered post-apply bucket check; and the new parse validation for buckets:, config.default_policy/synthesize, and entry-key hints.
🤖 Generated with Claude Code
Review finding 1: `--create-and-email-users` could never create anyone. The derived username was the email address, and the registry validates an explicitly supplied username against `^[a-z][a-z0-9_]*$`, deriving `email[:64]` itself only when the name is omitted — which quiltx cannot do, since `UserInput.name` is required. Every address therefore failed at the registry. The grammar is now a documented constant, `plan_user_creations` refuses an address it cannot name, and no derivation is invented: mapping an address to a handle depends on whether an SSO login reconciles a pre-created account by email, which is registry behaviour this repo cannot verify, and guessing it wrong would send irreversible mail and orphan the roles. Review finding 2: `config.default_policy` plus any `config.unmanaged` role made every apply that had work to do exit 1. The note saying default policies do not reach unmanaged roles is informational, but it landed in `diff.warnings`, and a non-empty warnings list means failure. `_DesiredAclState` gains a notices channel and the note goes there, printing as NONFATAL. Review finding 3: `_unmakeable_accounts` counted an ambiguous already-held address as uncreatable, reporting one address twice in contradictory ways. `UserCreationPlan` now separates notices from warnings. Fix #110: a missing default policy no longer deletes anything. Because the floor composes into every managed role, its absence made every role create fail — and the run then continued into the delete phases, removing the roles and policies the file drops while provably unable to create the ones it adds. `apply_acl` now stops at a gate before the role loops, keeping the policy changes that landed and deleting nothing. Fix #112: CLI-level coverage that an unresolvable `users:` key exits 1 with no mutation attempted. The behaviour was already correct; the tests pin it against a fixture that would otherwise mutate.
Second commit:
|
SSO login reconciles a pre-created account by email, which is the fact that makes pre-creation safe: the handle quiltx supplies is an administrative label, not the identity. derive_username folds the whole address (alice@example.com -> alice_example_com), keeping the domain so alice@example.com and alice@contractor.example do not contend for one name. The fold always satisfies USERNAME_PATTERN, pinned by a parametrized invariant test rather than a branch that cannot fire. Folding is not injective, so the handle is checked against server usernames and against the rest of the roster in a second pass; either clash refuses those addresses instead of appending a suffix that would depend on the order the file was written in. Existence is not identity. quiltx never calls users.set_email, so an address no account holds may be a person who already has one under their old address, and neither older guard catches it: the duplicate notice needs two accounts sharing one email, and the handle check compares folded addresses. Observed on open.quiltdata.com, where robbyqbutler@pm.me would have created and mailed a second account for an existing robbyqbutler / robbyqbutler@protonmail.com. An address whose local part an existing account already uses is now a warning naming both records, with nothing created. Also corrects the roster docstring: resolution is username-first, then email, matching _resolve_configured_user.
Third commit:
|
There was a problem hiding this comment.
🟡 Changes recommended
Post-apply onboarding can attempt irreversible user creation against a managed role whose creation failed.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Releases QuiltX 0.22.0 with expanded ACL reconciliation and onboarding capabilities.
Changes:
- Adds email-based user resolution and explicit roster onboarding.
- Adds default policies and safer failure handling.
- Adds per-bucket preflight control and registration verification.
File summaries
| File | Description |
|---|---|
quiltx/acl.py |
Implements ACL behavior and validation. |
quiltx/tools/catalog/acl.py |
Integrates new CLI workflows. |
tests/test_acl.py |
Adds comprehensive ACL tests. |
README.md |
Documents user, policy, and bucket features. |
stack-acl.example.yaml |
Expands example configuration guidance. |
CHANGELOG.md |
Records the 0.22.0 release. |
AGENTS.md |
Adds developer implementation notes. |
pyproject.toml |
Bumps the package version. |
uv.lock |
Synchronizes the locked package version. |
Review details
- Files reviewed: 7/9 changed files
- Comments generated: 5
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
plan_user_creations mixed the desired role set into "available", which is right for --dry-run (nothing is applied yet, so a fresh catalog would otherwise flag every address) and wrong after apply_acl: a managed role whose create failed is still in state.role_updates, so the real run could call users.create naming a role the registry does not have. Whether the registry mails the welcome before rejecting is not something quiltx controls, so the attempt must not be made. include_planned_roles now selects between the two, defaulting to the strict server-only reading. Forgetting the flag costs a spurious dry-run warning; the reverse costs an irreversible creation attempt, so the safe reading is the default and _print_user_creation_dry_run opts in explicitly. Docs: the #105 changelog entry called the unmanaged-role note a warning, which contradicts the notice routing it actually got. README and stack-acl.example.yaml claimed quiltx never creates users, which --create-and-email-users contradicts; both are now scoped to the users: block, with deletion still absolute. The role-availability rule is restated in all three. Also dropped the unverifiable claim that the registry mails before rejecting a bad role.
|
Reviewed The rename guard holdsThe case: an account existed as At Across the full live roster it is 24 creations, 15 existing, 1 blocked. The one blocked is One note on the reasoning, because I got this wrong first: I had assumed local-part matching would contradict Residual 1 — the
|
|
Correction to my comment above: Residual 2 is already tracked as #117, which I had missed — it covers the So the follow-up list against this PR is:
Residual 1 stands as written. Everything else in my review above is unchanged: the guard in |
The roster path refuses an address whose local part an existing account already uses; the users: path had the same root cause and none of the reporting. A key whose account moved misses both the username and the email index, so compute_diff dropped the entry with a nonfatal notice while the roster path exited non-zero for the identical condition. _accounts_sharing_a_local_part is a free function over (address, users), so the signal was already available here. Candidates now produce a warning naming them; no candidates keeps the notice, so a captured config carrying an entry for a deleted account still does not fail a clean run. The message offers rekeying as well as set_email, since a users: key can be rewritten and a roster address cannot. That alone was not enough. _run's early return for an unchanged config skipped the exit-code decision, so the warning printed and the run still exited 0 -- and that is precisely the #119 case: when a pin is the only thing naming someone, there is nothing else to apply. Verified by probe before and after. The early return is now `return 1 if diff.warnings else 0`, which also removes a broader inconsistency where one warning failed a run with work to do and passed one without. Notices remain outside the exit code everywhere. Docs: README gains the set_email snippet the refusal recommends but no quiltx command performs, pointing at #117 for the durable fix.
|
Thanks for running it against the live catalog — the Both residuals addressed in Residual 1 (#119) — fixed, and it needed a second change to actually workImplemented your suggested shape: One deviation from the issue text, in the message rather than the mechanism. The roster message says "set the existing account's email", because a roster is keyed by email and cannot be rekeyed. A The interesting part: routing to Which is exactly the case your issue singles out — when the pin is the only thing naming the person, there is nothing else to apply, so that is the common path rather than a corner. The fix would have been inert without this. The early return is now Both halves pinned, and both verified non-vacuous by mutation:
Residual 2 — README snippet addedYou are right that the refusal recommended something the tool could not do, which is worse for a fatal error than for a warning. Added next to the refusal: from quilt3 import admin
admin.users.set_email("robbyqbutler", "robbyqbutler@pm.me")with a note that the first argument is the username the warning prints, that On the docstringGlad that paragraph earned its place. I very nearly cut it as over-explaining, and kept it precisely because I had talked myself into the same contradiction you did and wanted the resolution written down rather than rediscovered. Closing #119. |
Closes #104, closes #105, closes #96, closes #106.
Four independent ACL changes, one release. Each is described at length in
CHANGELOG.md; this is the short version.
#104 (bug) —
users:keys resolve only by usernameKeys were matched against
user.name, anduser.nameis not one thing: theregistry derives it from
email[:64]for SSO self-registrations, while anadmin-created account must satisfy
^[a-z][a-z0-9_]*$. So one captured ACLmixes both shapes with nothing marking which is which, and writing someone's
email when their account is under a handle produced a nonfatal notice and
silently skipped the grant.
Keys now resolve by exact username first, then by a unique case-insensitive
email match. Precedence decides, so a key that names an account still means
that account even when a different account holds it as an email — that clash
is a warning naming both, because making it fatal would reject this tool's own
--yamlcapture, which keys every entry byuser.name. Two things remainfatal: a key that names no account and is the email of two, and two keys
resolving to one account. Everything downstream (the SDK call, downgrade
analysis, verbose output) now uses the resolved server username.
#105 (feature) —
config.default_policy, a floor rather than a fallbackconfig.default_rolefires only when a user's SSO claims matched no mapping,so a specific grant cost a user the general one — one real config carried
twelve hand-maintained
extra_rolesentries expressing a single intent. Adefault policy composes into the roles a user already matched, so it grants the
same permissions without adding a role to anyone's set. Requires
config.synthesize: false; unmanaged roles are out of reach and thatcombination warns.
Because the floor becomes a dependency of every managed role, a default policy
that fails to create now names itself as the cause in a
!! DEFAULT POLICY MISSINGblock instead of leaving oneunknown policylineper role to explain it.
#96 (feature + fix) — per-bucket
config.no_preflight, and loud failuresA cross-account bucket already prepared owner-side cannot be preflighted by the
catalog admin applying the ACL, so the add failed and the bucket was never
registered (
sierra-generalon open.quiltdata.com needed two attempts and theglobal flag). The new top-level
buckets:block keeps that fact with thebucket;
--no-preflightremains a global override.Separately: a bucket that does not register is now a named
!! BUCKET REGISTRATION FAILEDblock, and the command re-reads the catalogafterwards and exits non-zero for any bucket it still does not hold — so an add
that returns cleanly without registering is caught too.
#106 (feature) —
--create-and-email-usersusers:only ever reaches accounts that exist, so an ACL file could not onboardanyone. Creation is driven from
sso.email(unambiguous identifiers, and therole is implied by nesting so it cannot drift from what the SSO mapping grants)
The admin API requires a username, so quiltx cannot use the registry's own
email[:64]derivation and instead folds the whole address into a handle thegrammar accepts:
alice@example.com->alice_example_com. The domain is foldedin rather than dropped, because a local-part handle maps
alice@example.comandalice@contractor.exampleonto one name and only one account can hold it. Thehandle is an administrative label, not the identity: first SSO login reconciles
against the pre-created account by email. Folding is not injective, so the handle
is checked against server usernames and against the rest of the roster, and any
clash refuses those addresses instead of picking a winner.
An address whose local part an existing account already uses is also refused.
quiltx never calls
set_email, so an address nobody holds may be a person whoseaddress changed, and creating there would split one person across two logins.
Role availability is read from the server on a real run, never from the plan, so a
role whose create failed cannot turn into a creation the registry will reject.
There is deliberately no config key. The registry mails a welcome and
password-reset link as part of creating an account, with no suppress flag, so a
config default would make the first apply the irreversible one. The flag is
named for that side effect, prints every address before asking, and refuses more
than
--max-created-users(default 10).Testing
651 passed, 1 skipped (was 548 on
main);./poe lint-checkclean. New testscover each feature plus the cross-feature seams: the
--yamlcapture/replayround trip under a username/email collision, the failed-default-policy cascade,
per-bucket vs global preflight mode in one apply, and the two-prompt ordering
where declining the apply prompt creates nobody.
A design-level review of the combined diff caught seven issues that are fixed
here rather than filed, including the capture/replay regression above and a
silent last-write-wins when two
users:keys addressed one account.Not addressed
The tool analyses access reductions only, so a default policy that broadens
every managed role shows as
~ role Xat default verbosity (--verboselists the composed policies). That asymmetry predates this branch. There is
also no per-role opt-out from a default policy; both are worth their own issues
if wanted.
Greptile Summary
The PR releases QuiltX 0.22.0 with four coordinated ACL enhancements and associated documentation and tests.
sso.emailusers after ACL reconciliation.Confidence Score: 5/5
The PR appears safe to merge, with no concrete changed-code defect identified.
The new ACL behaviors preserve the reconciliation sequence, explicitly handle ambiguous identities and partial failures, verify bucket registration against refreshed state, and gate irreversible user creation behind an explicit capped CLI operation.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Parse ACL YAML] --> B[Build desired policies, roles, SSO, users, and buckets] B --> C[Fetch current catalog state] C --> D[Compute ACL diff and downgrade warnings] D --> E{Dry run?} E -- Yes --> F[Print ACL and user-creation plans] E -- No --> G{ACL changes present?} G -- Yes --> H[Register buckets] H --> I[Reconcile policies and roles] I --> J[Update users, default role, and SSO] J --> K[Re-read state and recover policy drift] G -- No --> K K --> L{Create-and-email flag enabled?} L -- Yes --> M[Plan, confirm, cap, and create roster users] L -- No --> N[Report final status] M --> NReviews (1): Last reviewed commit: "0.22.0: four ACL fixes and features (#10..." | Re-trigger Greptile
Context used (3)