refactor(common/auth): move login credentials to auth_provider - #7055
refactor(common/auth): move login credentials to auth_provider#7055Neilk1021 wants to merge 58 commits into
Conversation
…xera into task/migrateBackend
Automated Reviewer SuggestionsBased on the
|
# Conflicts: # sql/changelog.xml # sql/updates/29.sql
# Conflicts: # amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #7055 +/- ##
============================================
+ Coverage 85.51% 86.04% +0.52%
- Complexity 4150 4225 +75
============================================
Files 1169 1172 +3
Lines 46740 46789 +49
Branches 5202 5206 +4
============================================
+ Hits 39971 40259 +288
+ Misses 5055 4790 -265
- Partials 1714 1740 +26
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🔴 | bs=10 sw=10 sl=64 | 425 | 0.26 | 23,260/32,185/32,185 us | 🔴 +113.9% / 🔴 +93.7% |
| 🔴 | bs=100 sw=10 sl=64 | 945 | 0.577 | 102,739/137,353/137,353 us | 🔴 +35.7% / 🔴 +21.3% |
| ⚪ | bs=1000 sw=10 sl=64 | 1,094 | 0.668 | 908,779/1,019,844/1,019,844 us | ⚪ within ±5% / 🟢 +13.3% |
Baseline details
Latest main 42d08a3 from 2026-08-10T13:03:59.622Z
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 425 tuples/sec | 818.75 tuples/sec | 740.3 tuples/sec | -48.1% | -42.6% |
| bs=10 sw=10 sl=64 | MB/s | 0.26 MB/s | 0.5 MB/s | 0.452 MB/s | -48.0% | -42.5% |
| bs=10 sw=10 sl=64 | p50 | 23,260 us | 11,857 us | 13,173 us | +96.2% | +76.6% |
| bs=10 sw=10 sl=64 | p95 | 32,185 us | 15,045 us | 16,615 us | +113.9% | +93.7% |
| bs=10 sw=10 sl=64 | p99 | 32,185 us | 17,739 us | 19,272 us | +81.4% | +67.0% |
| bs=100 sw=10 sl=64 | throughput | 945 tuples/sec | 1,057 tuples/sec | 939.86 tuples/sec | -10.6% | +0.5% |
| bs=100 sw=10 sl=64 | MB/s | 0.577 MB/s | 0.645 MB/s | 0.574 MB/s | -10.6% | +0.6% |
| bs=100 sw=10 sl=64 | p50 | 102,739 us | 93,404 us | 106,062 us | +10.0% | -3.1% |
| bs=100 sw=10 sl=64 | p95 | 137,353 us | 101,214 us | 113,212 us | +35.7% | +21.3% |
| bs=100 sw=10 sl=64 | p99 | 137,353 us | 112,746 us | 123,853 us | +21.8% | +10.9% |
| bs=1000 sw=10 sl=64 | throughput | 1,094 tuples/sec | 1,081 tuples/sec | 966.11 tuples/sec | +1.2% | +13.2% |
| bs=1000 sw=10 sl=64 | MB/s | 0.668 MB/s | 0.66 MB/s | 0.59 MB/s | +1.2% | +13.3% |
| bs=1000 sw=10 sl=64 | p50 | 908,779 us | 923,474 us | 1,038,733 us | -1.6% | -12.5% |
| bs=1000 sw=10 sl=64 | p95 | 1,019,844 us | 983,118 us | 1,085,289 us | +3.7% | -6.0% |
| bs=1000 sw=10 sl=64 | p99 | 1,019,844 us | 1,015,764 us | 1,115,555 us | +0.4% | -8.6% |
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,470.12,200,128000,425,0.260,23259.56,32185.26,32185.26
1,100,10,64,20,2115.64,2000,1280000,945,0.577,102739.48,137353.04,137353.04
2,1000,10,64,20,18284.96,20000,12800000,1094,0.668,908778.93,1019843.88,1019843.88# Conflicts: # access-control-service/src/test/scala/org/apache/texera/service/activity/UserActivityEventListenerSpec.scala # amber/src/main/scala/org/apache/texera/web/ServletAwareConfigurator.scala # amber/src/main/scala/org/apache/texera/web/auth/GuestAuthFilter.scala # common/auth/src/main/scala/org/apache/texera/auth/JwtParser.scala
AdminComputingUnitResourceSpec arrived with apache#6854 while this branch was open and seeds its fixture with setPassword and setGoogleAvatar, neither of which the User pojo still has. Nothing conflicts textually -- the file is new and this branch never touched it -- so the merge is clean and only the compile catches it. Dropped the password (credentials live in auth_provider, and this spec exercises the listing rather than login) and moved the avatar to the renamed column. The value is preserved because the suite asserts ownerGoogleAvatar downstream.
One textual conflict, in AuthResource.register: main changed the role assigned to
a new local signup while this branch changed how the credential is written, on
adjacent lines. Took one line from each side rather than picking a side.
- Role: took main's INACTIVE. RESTRICTED was not a decision this branch made --
it dates to 95f1bb1 -- whereas main changed it deliberately in 5b2e866
(apache#7309) to align local signup with the other account-creation paths. It also
makes this branch self-consistent, since ExternalAuthProvisioner already
seeds new accounts INACTIVE.
- Credential: kept LocalAuthProvisioner.createLocalAccount. Main's
setPassword + userDao.insert would write the password back onto the user row
and undo this branch's migration.
Also ported ComputingUnitManagingResourceSpec, which arrived from main seeding a
fixture with setPassword. Nothing conflicts textually -- this branch never
touched the file -- so only the compile catches it, the same way
AdminComputingUnitResourceSpec did in b0b411f. Dropped the password: the spec
exercises computing-unit ownership rather than login.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Yicong-Huang
left a comment
There was a problem hiding this comment.
🔴 10 resolved · 0 open · 5 new (5 new — the newly-introduced vs. late-catch split is unavailable this round: the prior review's commit 1abdc70 was force-pushed away)
1 must-fix · 1 advisory · 3 polish. Everything from round 2 is resolved, and resolved the right way. The email-casing regression is fixed with two tests that genuinely fail on the old code, and the four behavior changes were withdrawn rather than argued for — which is what makes refactor(auth) an honest type now. What is left is the same case-sensitivity defect in a guard round 2 did not reach, plus three comments the withdrawal left stale.
Correctness (1)
AuthResource.scala:115— the new admin-bootstrap email guard is case-sensitive, so a case-variant admin username slips it and a duplicate account is created (must-fix, see inline)
Simplifications (1)
JwtAuth.scala:68—expireInDaysis never read and names days for a minutes value; this signature is already being rewritten (advisory, see inline)
Polish: 3 comments left stale by the round-2 withdrawal (see inline comments).
Verification trace
I re-traced the four behavior changes I asked to be split out, since the refactor type now rests on their withdrawal. All four are genuinely gone rather than relocated. (1) Avatar representation: profileOf is back to the last-path-segment form (GoogleAuthResource.scala:44-46), matching pre-PR :96-98. The part that matters is that the migration moved with it — 32.sql carries no value-rewrite, and says so at :24-26. Code and data agree. (2) No unverified-email gate: ExternalProfile (ExternalAuthProvisioner.scala:39-45) has no emailVerified field. (3) verifiedPayload (GoogleAuthResource.scala:75-76) has no try/catch, so a malformed credential behaves as pre-PR. (4) The display-name refresh is restored at :152-155.
What still changes observably is inherent to the schema move. register asks handleExists (:170) because the handle now lives in auth_provider, which restores the pre-PR meaning rather than altering it. The 23505 to 409 mapping (LocalAuthProvisioner.scala:98) is the error contract the new two-row insert needs.
I also confirmed the casing fix rather than taking it on report. Both sites route through the tx-scoped userByEmailIgnoreCase (:63-69, called at :100 and :124), and the new tests at ExternalAuthProvisionerSpec.scala:194 and :208 seed and query different casings — so unlike the old :374 assertion, they can observe a duplicate.
Three conflicts, all in sql/. sql/updates/32.sql was an add/add: main added the user_warehouse registration table (apache#6870) while this branch added the auth_provider split, and both claimed the same sequence number. Renumbered this branch's to 33.sql and kept main's as 32. Renumbering main's instead would break every database that has already applied it, whereas this branch's has not been merged anywhere. The two are independent -- main's only adds a table with a uid foreign key, and this one does not touch "user".uid -- so 32-then-33 needs no further ordering care. sql/changelog.xml followed: main's changeSet 32 kept as-is, this branch's re-registered as 33 pointing at 33.sql. sql/texera_ddl.sql was a union: both sides added a CREATE TYPE next to each other. Verified the merged file against both parents -- diffing it against main shows only the auth_provider changes, and against this branch only the user_warehouse ones -- so nothing was dropped from either side. Also ported UserWarehouseSpec, which arrived from main seeding its fixture with USER.PASSWORD. Nothing conflicts textually, so only the compile catches it, as with ComputingUnitManagingResourceSpec before it. The column moved to auth_provider and the ck_nulltest constraint that required it is gone, so the warehouse owner is inserted with a name alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🔴 5 resolved · 0 open · 6 new (6 new — the newly-introduced vs. late-catch split is unavailable this round: the prior review's commit 38db65d was force-pushed away)
1 must-fix · 3 advisory · 2 polish. All five round-3 items are resolved, and the refactor claim still holds — the four withdrawn behavior changes are genuinely absent rather than relocated. The blocker is not a new mistake so much as an unverified fix: the race handler applied from Copilot's autofix sits inside the transaction it means to recover from.
Correctness (1)
ExternalAuthProvisioner.scala:115— the 23505 recovery runs inside the already-aborted transaction, so it cannot recover and the request still 500s (must-fix, see inline)
Design & architecture (1)
AuthResource.scala:110— two seams extracted this round "so the path is reachable from a test" have no test caller, leaving this round's own fix unguarded (advisory, see inline)
Simplifications (1)
LocalAuthProvisioner.scala:114—claimWithLocalCredentialduplicatescreateLocalAccountbut for one line (advisory, see inline)
Conventions (1)
- Description: names migration
32.sql(the added file is33.sql;32.sqlis the unrelateduser_warehousemigration) and calls the tableauth_providers, notauth_provider. (advisory)
Polish: 2 quick touch-ups (see inline comments).
Verification trace
The must-fix rests on one claim, so I checked its premises rather than assuming Postgres semantics. SqlServer.withTransaction (SqlServer.scala:96) is a bare dsl.transaction(...), and SqlServer installs no custom jOOQ Settings or TransactionProvider — so this is one JDBC transaction, with savepoints only for nested transaction() calls, never per statement. That leaves the insert's 23505 aborting the transaction outright, which is why the handler's own SELECT cannot run. The decisive part is the in-repo counter-example: LocalAuthProvisioner does the same 23505 to 409 mapping but puts its try outside withTransaction, so its handler runs after rollback.
I also re-traced the refactor claim, since it rests on the round-2 withdrawals. All four are still absent rather than relocated: no sanitizedAvatar anywhere in the tree, ExternalProfile has no emailVerified, verifiedPayload (GoogleAuthResource.scala:68-69) has no try/catch, and the display-name refresh lives in refresh (ExternalAuthProvisioner.scala:144-147). The claim contract holds too: JwtAuth.scala:71-75 still writes googleAvatar and writes googleId whenever one is in hand, auth.service.ts:167-168 and flarum.service.ts:39,48 both still resolve, and a grep for getGoogleId|getGoogleAvatar|GOOGLE_AVATAR|GOOGLE_ID across all Scala returns nothing. The two jwtClaims sites that omit googleId are service-to-service — an internal upload header (ResultExportService.scala:553) and a computing-unit token (ComputingUnitManagingResource.scala:383) — neither reaching the browser, as the new scaladoc claims.
Yicong-Huang
left a comment
There was a problem hiding this comment.
🔴 3 resolved · 3 open · 6 new (6 new = 1 newly introduced · 5 late catches — my misses from earlier rounds)
1 must-fix · 4 advisory · 2 polish. The round-4 blocker is genuinely fixed: the 23505 catch now sits outside the transaction it recovers from. What the fix did not do is look at its sibling — LocalAuthProvisioner's handler has the mirror-image problem, right place but wrong message. Both still-open code items are the ones deferred to a follow-up issue.
Correctness (1)
LocalAuthProvisioner.scala:100— one 23505 handler covers two unique constraints, so an email collision is reported as a taken handle (must-fix, see inline)
Design & architecture (1)
ExternalAuthProvisioner.scala:80— bare"23505"besideLocalAuthProvisioner.UNIQUE_VIOLATIONin the same package (advisory, see inline)
Simplifications (2)
AuthResource.scala:45—userDaolost its last caller in this diff, and no Scala-Wunusedpass will flag it (advisory, see inline)JwtAuthSpec.scala:71— the expiry test'sexpireInDaysparameter is unread, so both call sites assert the same thing (advisory, see inline)
Conventions (1)
- Description: the added migration is
sql/updates/33.sql, not32.sql(that one isuser_warehouse), and the table isauth_provider, notauth_providers. Carried over from round 4. (advisory)
Polish: 2 doc lines the delta made inaccurate (see inline comments).
Verification trace
Two claims were load-bearing this round, so I traced both rather than reading the reply.
That the round-4 recovery now actually runs: SqlServer.withTransaction (SqlServer.scala:96) is a bare dsl.transaction, so the rollback completes before the new catch at ExternalAuthProvisioner.scala:79-82 executes — the shape LocalAuthProvisioner:83-104 already uses. Re-running provision resolves either violation it can raise: losing user_email_key finds the winner through the lower(email) lookup (:107), and losing uq_provider_identity finds it through the provider join (:91-99).
That createLocalAccount's transaction really has two constraints inside it: txUserDao.insert (:88) writes "user", whose email is UNIQUE (texera_ddl.sql:108), and txAuthDao.insert (:95) writes uq_provider_identity (:129). Both raise 23505, and both land on the single handler at :98.
I also re-checked the frozen wire contract, since that is what keeps this a refactor: the googleAvatar and googleId claim names are unchanged (JwtAuth.scala:74-75), a repo-wide grep for getGoogleId|getGoogleAvatar|GOOGLE_ID|GOOGLE_AVATAR across Scala and Java returns nothing, and no google_id/google_avatar survives in TS, SQL, YAML, Python or shell outside sql/updates/. The DDL and migration 33 converge on the same end state, and AdminUserResource.list still projects 12 columns onto 12 UserInfo fields in matching order.
…AuthProvisioner.scala Co-authored-by: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Signed-off-by: Neil Ketteringham <53205839+Neilk1021@users.noreply.github.com>
Co-authored-by: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Signed-off-by: Neil Ketteringham <53205839+Neilk1021@users.noreply.github.com>
…nalAuthProvisioner.scala Co-authored-by: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Signed-off-by: Neil Ketteringham <53205839+Neilk1021@users.noreply.github.com>
Yicong-Huang
left a comment
There was a problem hiding this comment.
🔴 3 addressed · 4 open · 4 new (4 new = 1 newly introduced · 3 late catches — my misses from earlier rounds)
1 must-fix · 1 advisory · 2 polish. All three suggestion blocks from round 5 were applied verbatim and verified in the tree. The one must-fix is the sibling handler that fix skipped. The schema, the migration and the frozen wire names all re-verified clean.
Correctness (1)
LocalAuthProvisioner.scala:130— the sibling handler still names the handle for a collision that is actually onPRIMARY KEY (uid, provider_type)(must-fix, see inline)
Conventions (1)
- Title scope:
authnames a feature, not a module.CONTRIBUTING.mdasks for the module carrying the change — hereamber. Possibly meant ascommon/auth, so worth confirming (advisory)
Polish: 2 scaladoc lines, one of them wording I supplied last round (see inline comments).
Still open from earlier rounds: the bare "23505" at ExternalAuthProvisioner.scala:81, the unreferenced userDao at AuthResource.scala:45, the unread expireInDays at JwtAuthSpec.scala:71, and the description's auth_providers / 32.sql (the table is auth_provider, the migration is 33.sql).
Verification trace
Two claims were load-bearing this round, so I traced both rather than reading the commit messages.
That the three suggestions really landed: LocalAuthProvisioner.scala:98-102, ExternalAuthProvisioner.scala:70-74 and JwtAuth.scala:65-67 now carry the suggested text verbatim, and SqlServer.withTransaction (SqlServer.scala:96) is still a bare dsl.transaction, so every catch continues to run after the rollback it recovers from.
Which constraint the remaining handler can actually meet: claimWithLocalCredential updates an existing user row and then inserts (uid, LOCAL, handle). The email is not changed on that path, so user_email_key cannot fire; PRIMARY KEY (uid, provider_type) (texera_ddl.sql:125) is what a second claim of the same placeholder hits, not uq_provider_identity. That is why re-checking handleExists alone is not the whole answer here.
I also checked whether the retry at :82 could 500 permanently on a user_email_key violation raised by refresh + update (:102-104), which would need two accounts whose emails differ only in case. It cannot arise from anything this PR touches: DatasetResource.resolveContributorUid (:212-217) looks up lower(email) before inserting a placeholder, and both register and provision match case-insensitively. The one exact-match check left is AdminUserResource.updateUser (:116), which this PR does not change.
…nalAuthProvisioner.scala Co-authored-by: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Signed-off-by: Neil Ketteringham <53205839+Neilk1021@users.noreply.github.com>
…eAuthResource.scala Co-authored-by: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Signed-off-by: Neil Ketteringham <53205839+Neilk1021@users.noreply.github.com>
…AuthProvisioner.scala Co-authored-by: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Signed-off-by: Neil Ketteringham <53205839+Neilk1021@users.noreply.github.com>
…o be referenced by other files in Auth.
…stant The constant added last round sat in `AuthResource` as `private[auth]`, which left the one other file in that package — `ExternalAuthProvisioner` — still spelling `"23505"` inline, and put a property of the database inside an HTTP resource. It now lives beside `SqlServer` in `common/dao`, where every caller that catches the code can reach it regardless of package. Also drops the scaladoc the move orphaned in `LocalAuthProvisioner` (it had come to document the password encryptor), gives `ExternalAuthProvisioner`'s retry comment a referent for what happens on the second failure, and completes a sentence in `WorkflowExecutionsResourceSpec`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both `createLocalAccount` and `claimWithLocalCredential` map a unique violation onto a 409 whose text names which constraint fired, and neither branch had a test. Three review rounds each found a defect inside these two blocks — the handler running in an already-aborted transaction, the sibling method the fix skipped, and the wrong constraint being named — so the branches were being corrected by inspection with nothing pinning the result. Each case drives the real constraint against the real DDL under embedded Postgres: `uq_provider_identity` and `user_email_key` for the insert path, `PRIMARY KEY (uid, provider_type)` and `uq_provider_identity` for the claim path. The assertions cover the cause named as well as the causes *not* named, since telling a user their free handle is taken was the last defect here, and they also pin the transaction boundary: a lost race leaves behind neither a credential-less account nor a placeholder flipped to claimed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What changes were proposed in this PR?
As discussed in #6716 we'd like to split authentication handling into its own table
auth_provider.For future support of multiple sources of authentication (e.g. github, IEEE accounts, etc.). This PR migrate the Schema from the image on the left to the one on the right.This PR:
auth_providertable in texera_ddl.33.sqlto migrate old account data to the new schema.GoogleAuthServiceto a genericExternalAuthProvisionerto be used in future PRs adding new external logins.Any related issues, documentation, discussions?
Closes #7048
How was this PR tested?
Compiled and ran all tests as well as deployed locally to verify functionality.
Was this PR authored or co-authored using generative AI tooling?
Co-authored with Claude Opus 4.8