fix(sync): three ways a UserGroup sync loses a permission silently - #1582
Open
cplieger wants to merge 3 commits into
Open
fix(sync): three ways a UserGroup sync loses a permission silently#1582cplieger wants to merge 3 commits into
cplieger wants to merge 3 commits into
Conversation
A sync that creates a resource and a UserGroup permission on that same resource creates the group, adds its users, reports success, and applies no permission. The symptom appears later and elsewhere, as an API caller being denied a permission the TOML plainly declares. Running the sync a second time applies it, which is what makes it look like a fluke. Cause is ordering. user_groups::run_updates sat under the '// No deps' comment ahead of every resource phase, but a permission target names a resource, so it does have a dependency. get_updates_for_execution, which is where expand_user_group_permissions resolves targets against all_resources_cache, also ran before anything was created. That expansion keeps only the targets it can match, so a permission on a resource this same sync is about to create expands to nothing and is dropped silently. The apply block already documents this class of problem: 'The ordering these are executed does matter, since latter resources may depend on prior synced resources already being updated with the declared state.' Procedure is last with '// Depends on everything'. UserGroups belong there too. Moves the phase after Procedure and recomputes its deltas at that point, because the earlier computation expanded against the pre-sync cache. resource::create refreshes that cache, so the targets resolve by then. The earlier computation is kept solely to feed the existing no-changes early exit. The recompute deliberately does not abort the sync on error. Every resource phase has already been applied by that point, so a '?' there would skip the deploy cache and the sync's own last_sync_ts bookkeeping; the error is logged into the update instead, which matches how run_updates already reports.
A UserGroup permission target is a resource name or a regex over names. A target that matches nothing is dropped by `expand_user_group_permissions` and the sync still reports success, so a permission the TOML declares is never granted and nothing says so. Collect those targets during expansion and report them in the UserGroups log, named by user group and target. An empty target id is reported the same way, since it cannot match either. Reported rather than fatal: a target can match nothing because it names a resource that does not exist, or because a pattern legitimately matches nothing right now, and the sync cannot tell those apart. The report comes from the pass that runs after the resource phases, whose resource cache has been refreshed and so includes resources this sync just created. The no-changes gate consults the earlier pass instead, so a sync whose only finding is a dropped target does not exit "nothing to do" before the report can be written. System targets are excluded: they are not name matched, so a no-match result says nothing about them. The view path discards its list on purpose, since it is recomputed continuously and reporting there would be noise rather than news.
The export and the import disagree about System targets, and the diff resolves that disagreement by deleting data. `convert_user_groups` writes a UserGroup's System permissions into the file untouched, so an export produces TOML that declares them. `expand_user_group_permissions` then drops every System target on the way back in, so the incoming permission list never contains one. `original_permissions` is read from the database with System passed through, and `to_remove` sets every original permission missing from the incoming list to `PermissionLevel::None`. So syncing back a file that Komodo itself exported revokes the group's System permissions. The same asymmetry shows up in the view: `curr` comes from `convert_user_groups` and contains the System permission, the incoming side does not, so the sync displays a permission removal that no edit asked for and that reappears on every refresh until it is applied. Pass System targets through the expansion instead of dropping them. There is nothing to match them against, since a System target names no resource, so pass-through is the whole fix. Scope: this restores round trip fidelity, not access. `get_user_permission_on_target` returns `PermissionLevel::None` for System unconditionally, so a System permission on a group grants nothing today either way. What changes is that the sync stops deleting a permission row a user deliberately created, and stops showing a diff that cannot be settled.
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.
A sync that creates a resource and a UserGroup permission on that same resource creates the group, adds its users, reports success, and applies no permission. Hit this on a live deployment: a service user was denied
Executeon an Action that the TOML plainly grants it. Running the sync a second time applied it, which is what made it look like a fluke rather than a bug.Three commits. Tracing the first one turned up two more ways the same code path loses a permission without saying so, and they are small enough that splitting them across PRs seemed worse than one PR you can read commit by commit. Commit 3 is independent of the other two: drop it and they still stand.
Commit 1: apply UserGroups after resources, not before
Cause
Ordering. Two things both ran before any resource existed:
user_groups::run_updatessat in the apply block under// No deps, ahead of every resource phase.user_groups::get_updates_for_executionran in the pre-computation block. That is whereexpand_user_group_permissionsresolves each permission target throughMatcheragainstall_resources_cache(), keeping only the targets it can match. A target naming a resource the same sync is about to create expands to nothing and is dropped, with no error.The apply block already documents exactly this hazard:
Procedureis last with// Depends on everything. A UserGroup permission names a resource, so UserGroups belong there too.The change
Move the phase after
Procedure, and recompute its deltas there rather than reusing the pre-computation, since that one expanded against the pre-sync cache.resource::createcallsrefresh_all_resources_cache(), so by that point the targets resolve. The earlier computation is kept solely to feed the existing no-changes early exit, which is why the TOML is cloned.The recompute deliberately does not propagate its error. Every resource phase has already been applied by then, so a
?there would introduce a new mid-sync abort that skipsdeploy_from_cacheand thelast_sync_tsbookkeeping. It logs into the update instead, matching howrun_updatesalready reports problems.Cost
One extra
get_updates_for_executionper sync wheninclude_user_groupsis on: afind_collectover user_groups, one over users, and oneListUserTargetPermissionsper group. Happy to restructure if you would rather avoid the second pass, for example by having the expansion happen insiderun_updatesso the deltas are computed once at apply time. That is a larger change and I did not want to guess at your preference.Interactions I checked
delete = true. When a resource is removed during the resource phases,delete_all_permissions_on_resourcepurges its permission rows in the same phase, so the recompute'soriginal_permissionsno longer contains it and the expansion drops the target. Net no-op, same end state the old order reached by granting then deleting. I did not find a case where the recompute revokes a permission the old order preserved.Commit 2: report permission targets that match nothing
The first commit fixes the case where the target was resolvable and the ordering was wrong. The remaining case is a target that is not resolvable at all: a typo, a renamed resource, a pattern that matches nothing.
expand_user_group_permissionsdrops it,run_updatesreports success, and no line of output mentions it. A permission the TOML declares is simply never granted, and the sync is green.This is the diagnostic I mentioned as future work in the first draft of this PR. It is here now rather than deferred, and without the
warn!that made the draft noisy.The expansion now collects each target that expanded to nothing and
run_updatesprints oneWARNline per unique target into the UserGroups log the user already reads, naming the group and the target. An empty target id is reported the same way, since an id is a name or a regex over names and an empty one cannot match either.Reported rather than fatal, deliberately: a target can match nothing because it names a resource that does not exist, or because a pattern legitimately matches nothing right now, and the sync cannot tell those apart. Hard-failing would break the second case, which is supported configuration.
Four details worth your attention:
RunSyncno-changes gate, so a sync whose only finding is a dropped target does not exit "nothing to do" before the report can be written. I stopped short of claiming that cache is authoritative:refresh_all_resources_cachelogs and swallows a load failure, so a stale cache can in principle produce a false report.last_sync_ts/last_sync_hash. Previously it returned early. I convinced myself this is the more accurate of the two, since everything applicable really has been applied, and I checked that displayed sync state and the pending-updates alert are both derived from view-path data and so are unaffected. Flagging it because it is the one behavior change beyond the log line.ResourceSyncInfothat does not feedhas_updates, Pending state, or alerts. That is a UI change I did not want to make uninvited, so say the word if you want it.Shape note
get_updates_for_executionnow returns a 4-tuple. AUserGroupDeltasstruct would read better and there is precedent inSyncDeltas. I kept the tuple to hold the diff to the bug; happy to convert it if you would rather that landed here.Commit 3: stop the sync revoking System permissions it exported itself
Independent of the first two, and the only one that deletes data rather than hiding information.
The export and the import disagree about System targets, and the diff resolves the disagreement by deleting:
convert_user_groupswrites a group's System permissions into the file untouched, so an export produces TOML that declares them.expand_user_group_permissionsdrops every System target on the way back in, so the incoming list never contains one.original_permissionsis read from the database with System passed through, andto_removesets every original permission missing from the incoming list toPermissionLevel::None.So syncing back a file that Komodo itself exported revokes the group's System permissions. The same asymmetry is visible in the view:
currcomes fromconvert_user_groupsand holds the System permission, the incoming side does not, so the sync displays a permission removal nobody asked for, and it reappears on every refresh until it is applied.The fix is pass-through. A System target names no resource, so there is nothing to match it against and nothing to expand.
On severity, so you can weigh it honestly: this restores round trip fidelity, not access.
get_user_permission_on_targetreturnsNonefor System unconditionally, so a System permission on a group grants nothing today either way. What changes is that the sync stops deleting a permission row a user deliberately created, and stops rendering a diff that cannot be settled.If you would rather System permissions simply not be syncable, the symmetric alternative is to filter them out of
convert_user_groupsinstead. That alone does not fix the revocation, sinceto_removecompares against the database rather than the exported file, so it would needto_removeto skip System as well. I picked pass-through because it is one branch and keeps export and import agreeing, but say the word and I will invert it.Verification
cargo check -p komodo_coreandcargo check -p komodo_core --testsclean with no new warnings,cargo fmt --checkclean. Reproduced the original failure and the fix against a live Core (a sync creating an Action plus a group grantingExecuteon it: permission missing before, applied after). No test added:bin/corehas no test module today, and these paths need a database and the resource cache.WARNwithColor::Redfollows the existing precedent insync/file.rs;formatting::Colorhas only Red, Green and Blue.