fix: prevent client grant loss and duplication in directory format exports - #1473
fix: prevent client grant loss and duplication in directory format exports#1473TheInfinity007 wants to merge 2 commits into
Conversation
A client grant's identity is (client_id, audience, subject_type) — see the `identifiers` list in src/tools/auth0/handlers/clientGrants.ts, which also contains dedicated DELETE + CREATE handling because subject_type is immutable. The directory dump encoded only client name and API name, so grants differing solely by subject type (e.g. `client` vs `user` on the same client and audience) resolved to the same filename and silently overwrote each other. One of the two grants was lost from the export with no error or warning, and would then be treated as a deletion candidate on the next import. Append subject_type to the filename when present. Grants without the field keep their existing filename, so exports from tenants that do not use subject_type are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The clientGrants dump never pruned files, unlike connections. Two consequences: 1. A grant deleted from the tenant kept its file, so the next import recreated the grant that had just been removed. 2. A grant whose filename changes is left behind under its old name while the new name is also written. `parse` then returns the same grant twice, and `calculateChanges` emits one create plus one update — the create targets a grant that already exists and the import fails. The second case is reachable from the preceding commit, which changes filenames for grants that carry a subject_type. Track written filenames and remove anything else in the folder, mirroring the existing cleanup in the connections handler. Filenames for excluded clients are recorded before those grants are filtered out, so excluding a client no longer deletes its previously dumped file. Name construction moves into a `nameFor` helper so the cleanup pass can derive the filenames of excluded grants. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The Cause
const AUTH0_CLIENT_ID = process.env['AUTH0_E2E_CLIENT_ID'] || '';The clients and clientGrants handlers exclude the Management API client from changes via The four failures are also all YAML-path tests, while this PR only touches the directory VerificationOn unmodified
That single variable flips it — no secret or domain needed, since I also ruled out dependency drift: installing with Possible fixes1. Default the client ID in lockdown mode (suggested) The value isn't a secret — const AUTH0_CLIENT_ID = shouldUseRecordings
? 'Vp0gMRF8PtMzekil38qWoj4Fjw2VjRZE'
: process.env['AUTH0_E2E_CLIENT_ID'] || '';One line, no CI changes, no credentials. Makes 2. Enable "Pass secrets to builds from forked pull requests" in CircleCI Works, but exposes every project secret to arbitrary fork builds — probably not a trade you 3. Re-run the job with the env available, or rely on internal CI post-merge Unblocks this PR but leaves the underlying issue for the next external contributor. Happy to open a separate PR for option 1 if that's useful — I'd keep it out of this branch so |
🔧 Changes
Two related bugs in the
clientGrantsdirectory-format handler, one per commit.1. Grant filenames omit
subject_type, so grants overwrite each othersrc/context/directory/handlers/clientGrants.tsbuilt the filename from client name +API name only. But a grant's identity is
(client_id, audience, subject_type)— per theidentifierslist insrc/tools/auth0/handlers/clientGrants.ts, which also containsdedicated DELETE + CREATE handling because
subject_typeis immutable (it is instripUpdateFields).So a client holding both a
clientand ausergrant on the same audience produced asingle file: the second write silently overwrote the first. No error or warning is emitted —
the dump only validates that the generated name is non-empty. The lost grant is absent from
the export and becomes a deletion candidate on the next import, removed outright when
AUTH0_ALLOW_DELETEis enabled.The directory serializer was strictly less expressive than the data model it serializes.
subject_typeis now appended to the filename when present. Grants without the field keeptheir existing filename.
2. Stale grant files are never removed
Unlike
connections, theclientGrantsdump never pruned its folder. Two consequences:that had just been removed.
while the new name is also written.
parsethen returns the same grant twice, andcalculateChangesemits one create plus one update — the create targets a grant thatalready exists, so the import fails.
The dump now tracks written filenames and removes anything else in the folder, mirroring the
existing cleanup in
connections.ts. Filenames for excluded clients are recorded beforethose grants are filtered out, so excluding a client no longer deletes its previously dumped
file.
Name construction moved into a
nameForhelper so the cleanup pass can derive the filenamesof excluded grants. No behaviour change beyond the above.
Tenants whose grants carry
subject_typewill see files renamed on their next export(
Client-API.json→Client-API-client.json). Commit 2 makes this safe — the old file isremoved rather than left to be parsed as a duplicate — but it will show up as a rename in
users' config repos.
If you'd prefer to avoid that churn, I'm happy to switch to appending
subject_typeonlywhere a collision actually exists. That limits renames to the tenants currently losing data,
at the cost of a filename that depends on sibling grants. Happy to go either way.
📚 References
Fixes #1472
🔬 Testing
Unit tests added to
test/context/directory/clientGrants.test.js— 11 passing in that file,1423 in the full suite (up from 1420):
subject_typedump to separate filessubject_typekeeps its legacy filename (backward compatibility)exactly one asset
The first test fails on unmodified
master, confirming the bug:The pre-existing dump tests assert exact filenames and pass unmodified, since their fixtures
omit
subject_type.To reproduce manually: give one client two grants on the same audience, one with
subject_type: clientand one withsubject_type: user, thena0deploy export -c config.json -f directory -o ./outand inspect./out/grants/. Only onefile exists for that client/audience pair. The same export with
-f yamlcorrectly containsboth, since
tenant.yamlstoresclientGrantsas a list.Two known gaps left out to keep this focused — happy to file follow-ups:
dump()returns early whenclientGrants.length === 0, before the cleanup runs, sodeleting every grant prunes nothing while deleting some prunes correctly. The early
return exists to avoid the clients/resourceServers API calls, which an existing test asserts.
default_forthird-party grants have noclient_idand produce filenames beginning withthe literal string
undefined.📝 Checklist