Repository navigation
fix(server): hold a provisioning key's tags by ID - #7602
Merged
Merged
Conversation
otavio
added this pull request to stack #7604
October 17, 2026 23:51
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/provisioning-key-tag-ids
branch
from
October 18, 2026 20:03
82ff110 to
7db913c
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/provisioning-key-tag-ids
branch
from
October 18, 2026 20:20
7db913c to
773cbc8
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/provisioning-key-tag-ids
branch
from
October 18, 2026 20:53
773cbc8 to
135e8af
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/provisioning-key-tag-ids
branch
from
October 18, 2026 21:07
135e8af to
cc78a4c
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/provisioning-key-tag-ids
branch
from
October 18, 2026 21:23
cc78a4c to
c602735
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
otavio
force-pushed
the
fix/provisioning-key-tag-ids
branch
from
October 18, 2026 23:22
bfcf872 to
02c382d
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
… them A provisioning key stored the names of its tags, and enrollment created any name the namespace lacked. Renaming a tag left the key holding the old name, so the next device it enrolled brought the old tag back beside the renamed one. Deleting the tag answered 204 and the next enrollment re-created it. A deny access policy or firewall deny rule on the renamed tag then missed every device the key enrolled from that point on. Public keys, access policies and firewall rules already hold the tag itself and refuse its delete (#7437, #7537). Provisioning keys now do the same: - provisioning_key_tags joins a key to its tags. Migration 048 links every name a key held, creating a tag the namespace lacked, and drops the tags column. - Creating or updating a key resolves the names it is given, creating a missing tag as enrollment used to, so the API keeps taking names. The names resolve once every other field has validated, in the transaction that writes the key, so a rejected request creates no tag. - Enrollment applies the key's tag IDs and no longer creates tags. - DeleteTag answers 409 naming `provisioning key "x"` while a key holds the tag. A revoked key does not count: it enrolls nothing, cannot be edited and cannot be deleted, so counting it would pin the tag forever. Deleting the tag drops it from the revoked key through the cascade. Creating or updating a key locks each tag it names, the row lock DeleteTag takes, in the transaction that writes the key's links. Whichever runs second sees the other's result: a delete that wins leaves the name for the key to create afresh, and a key that wins makes the delete answer 409. Without the lock the link insert waited on the delete and then failed its foreign key as a 500. An update rewrites the key's links only when the request names tags. The service passes nil TagIDs otherwise, so an update that changes only the mode cannot put back a tag another request removed and a delete then dropped, which would fail the foreign key as a 500. tagSelectors becomes tagHolders: a provisioning key applies a tag rather than selecting devices by it. Refs: shellhub-io/team#243
The server now also refuses to delete a tag a provisioning key applies, and a provisioning key has no device filter. Refs: shellhub-io/team#243
ProvisioningKeyResolve found the key through resolveUnique and then selected it again by primary key only to attach its tags, because resolveUnique built a query no caller could add a relation to. resolveUnique now takes the relations to load, and the key and its tags come back from one Scan. bun still reads an m2m relation in a query of its own after the main one, so a lock option keeps covering only the key row, as it did before. Refs: shellhub-io/team#243
The resolve contracts named only ErrAmbiguous, and the two system resolves restated their signatures, so a caller learned of ErrNoDocuments, ErrInvalidScope, ErrResolverNotFound, ErrInternal and the bare ctx error fromSQLError passes through only by reading the pg store. They also now say the key comes back with its tags. Refs: shellhub-io/team#243
otavio
force-pushed
the
fix/provisioning-key-tag-ids
branch
from
October 19, 2026 00:06
13ea1c2 to
beba482
Compare
Member
Author
|
/review |
This comment has been minimized.
This comment has been minimized.
…turns The service contracts named only the tag failure and otherwise ended "an error, if any", so a caller learned of the forbidden, not-found, duplicate and invalid-field cases these methods have returned all along only by reading the service. A cancelled ctx comes back two ways, and the contracts say both: NewErrNamespaceNotFound wraps any namespace lookup error, so a cancel that stops that lookup reads as ErrNamespaceNotFound, while one landing after it returns ctx's error unwrapped. Refs: shellhub-io/team#243
otavio
force-pushed
the
fix/provisioning-key-tag-ids
branch
from
October 19, 2026 00:20
beba482 to
984a6bd
Compare
Member
Author
|
/review |
|
Claude finished @otavio's task in 3m 39s —— View job Code Review CompleteI reviewed all 26 files for Standards (repo rules, language conventions, tests, code smells), Spec (does the PR implement the issue?) and Correctness (security and runtime defects). I found no new issues, and the code is fine to merge as it stands. This PR closes no issue, and its commits cite shellhub-io/team#243 only through
Notes:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A provisioning key stored the names of its tags, and enrollment created any name the namespace lacked. After a rename, the key still held the old name, so the next device it enrolled brought the old tag back next to the renamed one. A delete answered 204 and the next enrollment re-created the tag. A deny access policy or firewall deny rule on the renamed tag then missed every device the key enrolled from that point on.
Keys now hold the tag itself, as public keys, access policies and firewall rules have since #7437 and #7537:
A revoked key does not count as a holder: it enrolls nothing, cannot be edited and has no delete route, so counting it would pin the tag forever. Deleting the tag drops it from the revoked key through the FK cascade.
The console's 409 message said
is used in the device filter of .... A provisioning key has no device filter, so it now saysis held by ....The e2e tests are in #7603, stacked on this one.
Evidence
TestTagsAppliedByProvisioningKey(stacked PR), with a key that applies one tag:cf7c08123, written against the expected behaviour first):Also:
TestProvisioningKeyTagsMigrationfailed withno migration numbered 048before the migration. It now passes: names become links, a missing tag is created, an existing one is reused, and the column is gone.TestPgStore/ProvisioningKeyStore/*: resolve and list report a renamed tag under its new name, and an update replaces the links.TestService_DeleteTag/fails_when_provisioning_keys_not_revoked_hold_the_tag, plusCreateProvisioningKeyandUpdateProvisioningKeycases for name resolution and tag creation.server/api/services,server/api/routes/...andserver/api/store/...pass. The fulltests/suite passes except the fourTestCloud*cases, which fail at setup here becauseSHELLHUB_STRIPE_PUBLISHABLE_KEYis unset.ManageTagsModal.test.tsxpasses. golangci-lint finds 0 issues inserver/apiandtests; eslint, tsc and knip pass.Merge Danger
Door: one-way
Migration 048 drops
provisioning_keys.tagsafter copying it into links. Its down migration rebuilds the array from the links, but under the tags' current names. Tags it created for names the namespace lacked stay after a rollback.Blast Radius: tags
Deleting a tag that an active provisioning key applies now answers 409 instead of 204. Creating or editing a key with a new tag name now creates that tag at once instead of at the first enrollment.
Refs shellhub-io/team#243