feat: add optional activeAccount to UserValidation - #123
Merged
Conversation
UserValidation only proved that a user exists in Entra ID, so a disabled (for example offboarded) account passed validation exactly like an active one. Add an optional activeAccount flag to the Input. When set, UserValidation additionally requires the accountEnabled attribute to be true and omits disabled users from the result written to the target. Microsoft Graph returns accountEnabled only when it is explicitly selected, so it is added to the $select projection and filtered client side. Filtering server side via "and accountEnabled eq true" would make "disabled" and "does not exist" indistinguishable (both come back as an empty collection) and would leave accountEnabled either constantly true or absent in the output. accountEnabled is reported for every validated user regardless of the flag, so the account state stays visible to downstream consumers. A user for which Graph returns no accountEnabled value is treated as disabled: when the state cannot be confirmed, activeAccount excludes the user. Excluded users are logged at debug level, consistent with how a non-existent user is silently omitted today. The result-map building is extracted into buildUserResults so the added branch stays under the gocyclo ceiling and is unit testable without a Graph client, following the existing processMember precedent. Notable for adopters: activeAccount filters the result of a query, it does not purge a target that is no longer refreshed, so enabling it alongside skipQueryWhenTargetHasData leaves already-stored disabled users in place until the target is cleared once. Both READMEs document this. Signed-off-by: Yury Tsarev <yury@upbound.io>
golangci-lint v2.12.2 (used in CI) reported two goconst findings that the locally installed v2.11.4 did not: fn_test.go:990: string `user-id-1` has 7 occurrences, make it a constant fn_test.go:1599: string `Test User` has 8 occurrences, make it a constant The activeAccount test fixtures and cases pushed both literals over the configured min-occurrences of 5. Extract them as testUserID1 and testUserDisplay alongside the existing fixture constants, and use them at every Go call site. Occurrences inside the raw-string JSON request bodies are part of a single larger literal and are not counted by goconst, so they are left as they are. No behavior change. Verified with golangci-lint v2.12.2 (0 issues) and go test -count=1 ./... (all pass). Signed-off-by: Yury Tsarev <yury@upbound.io>
Adds cluster manifests for the activeAccount filter alongside the existing queryInterval scenario: two Compositions identical except for the flag, and one XR each, so the difference is observable in a single cluster. The baseline shows a disabled account passing validation, which is the behaviour the flag exists to change. The e2e README is restructured around the two scenarios. function.yaml gains a DeploymentRuntimeConfig enabling --debug, so the per-user exclusions and the queryInterval skips are visible in the pod logs, and pins the tag to v0.8.0-rc2. The Compositions use placeholder UPNs deliberately: substitute one enabled and one disabled account from your own directory before running. Verified against a kind cluster running v0.8.0-rc2: both XRs reach SYNCED/READY with FunctionSuccess, the baseline stores the disabled user with accountEnabled false, the activeAccount XR stores only the enabled user, and the exclusion is logged. All manifests pass kubectl apply --dry-run=server. Signed-off-by: Yury Tsarev <yury@upbound.io>
turkenf
approved these changes
Aug 13, 2026
|
This looks good to me too but I had Claude take a look too. Is this OK? Seems like possible drift loop. |
Member
Author
|
@jboero thanks for the double checking. My understanding is that the target field will have to be forcefully invalidated /cleaned up to propagate updated structure and that's ok for one off operation |
|
OK then if that's a prerequisite fine by me. Well done. |
jboero
approved these changes
Aug 13, 2026
jboero
left a comment
There was a problem hiding this comment.
Looks good if we agree the drift loop should be avoided.
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.
Description of your changes
UserValidationonly proved that a user exists in Entra ID, so a disabled (forexample offboarded) account passed validation like an active one. This adds an
opt-in check that the account is also enabled.
activeAccount: trueadditionally requires theaccountEnabledattribute to betrue and omits disabled users from the result written to the target:
accountEnabledis added to the$selectprojection, because Microsoft Graphreturns it only when explicitly selected, and is filtered client side. Filtering
server side via
and accountEnabled eq truewould make "disabled" and "does notexist" indistinguishable, since both come back as an empty collection, and would
leave
accountEnabledeither constantly true or absent in the output.The value is reported for every validated user regardless of the flag, so the
account state stays visible to downstream consumers. A user for which Graph
returns no
accountEnabledvalue is treated as disabled: when the state cannotbe confirmed,
activeAccountexcludes the user.Notes for reviewers
accountEnabledeven when the flag is unset. InOperation mode this registers as a one-time drift on the first reconcile after
upgrade (
hasQueryResultDriftedFromTargetcompares withreflect.DeepEqual),and consumers whose XRD constrains the target with a structural item schema
need to add the field or the API server prunes it.
by
skipQueryWhenTargetHasDataorqueryInterval, so adopters with anexisting target must clear it once. Documented in
README.mdandexample/e2e/README.md.pass a gating check.
UserValidationonly.GroupMembershipuser members are unchanged.buildUserResults, which keepsvalidateUsersunder thegocycloceiling and makes the filter unit testablewithout a Graph client, following the existing
processMemberprecedent.Testing
TestBuildUserResultscovers the filter, including the fail-closedunconfirmed-state case and the empty/nil edges, plus a new
TestRunFunctioncase proving the field is parsed off the manifest and reaches the query.
disabled account, via
crossplane renderand end-to-end on a kind clusterrunning the packaged build: without the flag the disabled account passes
validation and is reported as
accountEnabled: false, with the flag it isexcluded, and the filtering holds across forced reconciles.
example/e2e/gains anactiveAccountscenario (two Compositions identicalexcept for the flag) so the difference is observable in one cluster.
I have: