Skip to content

fix(reconcile): close the PlatformUser Rule 2 and Rule 5 gaps#1795

Merged
rohilsurana merged 3 commits into
mainfrom
fix/reconcile-platformuser-r2-r5
Jul 23, 2026
Merged

fix(reconcile): close the PlatformUser Rule 2 and Rule 5 gaps#1795
rohilsurana merged 3 commits into
mainfrom
fix/reconcile-platformuser-r2-r5

Conversation

@rohilsurana

@rohilsurana rohilsurana commented Jul 23, 2026

Copy link
Copy Markdown
Member

What

Closes the two remaining PlatformUser gaps on the rules-v2 audit: Rule 2 (an add for a missing user id reported phantom success) and Rule 5 (a stored display-name email broke the export round-trip). One commit per fix.

Rule 2: fail an unknown non-email Sudo ref (server-side)

user.Sudo returned nil for an id that is neither an existing user nor an email (the "skip" branch). It reported the platform grant as done while granting nothing. So a reconcile add for a missing user id reported success and re-planned the same add on every run, never converging. It now returns a not-found error, so the add fails loudly and names the bad ref. Only the not-found non-email path changes; an existing id and an email both behave as before. The serviceuser Sudo already fails loudly on a missing id, so it needs no change.

Rule 5: match and export platform-user emails by address (reconcile-side)

The server can store a user's email as a display-name form (Alice <a@x.com>; AddPlatformUser accepts it because isValidEmail uses mail.ParseAddress). Exporting that stored email verbatim produced a document the validation rejected, breaking the Rule 5 round-trip over a reachable state.

The fix compares emails by their parsed address instead of as exact strings:

  • Validate accepts a uuid or any address mail.ParseAddress parses, so a display-name ref is allowed.
  • Match compares the lowercased address of the ref and the stored email, so a display-name or differently-cased ref matches the address the server stored, and no spurious remove is planned.
  • Add normalizes an email ref to its plain address, so an add for an unmatched display-name ref grants the real user rather than creating a shadow user whose email is the literal display-name form.
  • Export emits the normalized address (lowercased, display name dropped), or the id when the stored value is not an email or does not re-parse. A quoted local part like "john smith"@x.com unquotes to an address mail.ParseAddress cannot read back, so it exports by id and still round-trips.

A uuid never parses as an email, so a uuid ref matches only by id, and a user whose email happens to be another user's id can neither be matched by that id's ref nor exported as that id (which would otherwise grant the other user on re-reconcile). The uuid-as-email case, the add normalization, and the quoted-local-part fallback were each found by independent review and are fixed here.

Testing

  • Test first for each. Rule 2: a Sudo call for an unknown non-email id now errors, where it used to skip silently. Rule 5: a display-name-email user round-trips, a display-name ref matches the stored principal, a uuid ref does not match a user whose email is that uuid, a user whose email is another user's id exports by its own id, an add for a new email ref uses the normalized address, and a user with a quoted-local-part email exports by id and round-trips.
  • go build, go vet, go test ./internal/reconcile/... ./core/user/..., gofmt, and golangci-lint all pass.

Notes

  • The AddPlatformUser handler maps a Sudo error to Internal today. That is enough for the reconciler (it stops on any error), but a not-found could map to a cleaner status code. Small follow-up if wanted.
  • Address comparison folds only the display name and case; it does not apply provider rules, so abc+1@x.com and abc+2@x.com stay distinct.
  • Two distinct users whose emails share one bare address (for example a@x.com and Alice <a@x.com>, which the server allows because email uniqueness is on the raw string) both export to that one address, so reconciling that export cross-grants their relations. It is an accepted edge; the alternative (a strict bare-email check with an id fallback) avoids it but rejects hand-written display-name refs.

Status

The documented PlatformUser Rule 2 gap (the Sudo phantom success) and the Rule 5 email round-trip are closed here. An independent review surfaced one more reachable Rule 2 gap this PR does not fix: a disabled platform admin is dropped by ListPlatformUsers (the store filters disabled users), so the reconciler never sees the grant, cannot show it in a plan, and cannot remove it by omission. Closing that needs either a server-side change to list disabled principals or a documented carve-out, so it is a follow-up. After this PR the audit has no Non-compliant cells left; the remaining Partial cells are that disabled-admin case and Permission's namespace-charset gap (Rules 2 and 4). Targets main.

Sudo returned nil for an id that is neither an existing user nor an email, reporting the platform grant as done while nothing changed. A reconcile add for a missing user id then reported success and re-planned the same add on every run, never converging (rule 4/rule 2). Return a not-found error so the add fails loudly and the operator sees the bad ref. Only the not-found non-email path changes; an existing id and an email both behave as before.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Unknown, non-email user identifiers now return a clear not-found error instead of silently succeeding.
    • User reconciliation now reliably recognizes email addresses regardless of capitalization or display names.
    • Invalid or ambiguous email formats safely fall back to using the user’s ID.
    • UUID references are matched strictly by user ID, preventing accidental matches with email values.
  • Improvements

    • Exported user references now use canonical, normalized email addresses for more consistent re-import and reconciliation.

Walkthrough

Changes

Sudo user lookup behavior

Layer / File(s) Summary
Fail unknown non-email sudo targets
core/user/service.go, core/user/service_test.go
Service.Sudo now returns a wrapped ErrNotExist for unknown non-email identifiers, with a regression test covering the failure path.

Platform-user email references

Layer / File(s) Summary
Normalize and match email references
internal/reconcile/platformuser.go, internal/reconcile/platformuser_test.go
Validation, canonicalization, and principal matching now use normalized email addresses while keeping UUID references ID-specific.
Export canonical refs and verify round trips
internal/reconcile/platformuser_reconciler.go, internal/reconcile/platformuser_reconciler_test.go
Exports use normalized email references when valid and fall back to IDs for ambiguous values, with round-trip coverage for edge cases.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: amangit07

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Jul 23, 2026 9:17am

@coveralls

coveralls commented Jul 23, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 29994649480

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.05%) to 46.674%

Details

  • Coverage increased (+0.05%) from the base build.
  • Patch coverage: 39 of 39 lines across 3 files are fully covered (100%).
  • 1 coverage regression across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
internal/reconcile/platformuser.go 1 97.44%

Coverage Stats

Coverage Status
Relevant Lines: 38921
Covered Lines: 18166
Line Coverage: 46.67%
Coverage Strength: 14.3 hits per line

💛 - Coveralls

The server can store a user's email as a display-name form ("Alice <a@x.com>"), so exporting the stored email verbatim produced a document the strict validation rejected, breaking the rule 5 round trip. Compare emails by their parsed address instead: validate accepts a uuid or any email, matching compares the lowercased address (so a display-name or differently-cased ref matches the stored address), and export emits the normalized address, or the id when the stored value is not an email. A uuid never parses as an email, so a uuid ref matches only by id and a user whose email is another user's id can neither be matched nor exported as that id. Closes the PlatformUser rule 5 gap over every reachable email form.

Uses mail.ParseAddress per review discussion; this does not fold plus-addressing (abc+1 and abc+2 stay distinct), only display name and case.
Two edges the address-based matching missed, found by an independent review. (1) An add for an unmatched email spec sent the ref verbatim, so a display-name ref like "Alice <a@x.com>" made the server create a shadow user and grant it, never the real user, and the drift never showed again. canonicalRef now normalizes an email ref to its plain address, so the add resolves to the real user. (2) A quoted local part ("john smith"@x.com) unquotes to an address mail.ParseAddress cannot read back, so exporting it emitted a ref that failed its own validation. emailAddress now requires the address to re-parse, so such stored emails fall back to the id on export and match, keeping the rule 5 round trip.
@rohilsurana
rohilsurana marked this pull request as ready for review July 23, 2026 09:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f0fb6148-e1a9-4435-a6b6-c4f202f9aa9e

📥 Commits

Reviewing files that changed from the base of the PR and between 9e063e3 and 49efe71.

📒 Files selected for processing (6)
  • core/user/service.go
  • core/user/service_test.go
  • internal/reconcile/platformuser.go
  • internal/reconcile/platformuser_reconciler.go
  • internal/reconcile/platformuser_reconciler_test.go
  • internal/reconcile/platformuser_test.go

Comment thread core/user/service_test.go
@rohilsurana
rohilsurana merged commit 152edd2 into main Jul 23, 2026
8 checks passed
@rohilsurana
rohilsurana deleted the fix/reconcile-platformuser-r2-r5 branch July 23, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants