aglinxinyuan commented on code in PR #7055:
URL: https://github.com/apache/texera/pull/7055#discussion_r3694711824


##########
sql/texera_ddl.sql:
##########
@@ -78,6 +78,7 @@ DROP TABLE IF EXISTS computing_unit_user_access CASCADE;
 DROP TABLE IF EXISTS notebook CASCADE;
 DROP TABLE IF EXISTS workflow_notebook_mapping CASCADE;
 DROP TABLE IF EXISTS virtual_environments CASCADE;
+DROP TYPE IF EXISTS provider_type_enum CASCADE;

Review Comment:
   This drops the *type* but nothing drops the *table* — `auth_provider` is 
missing from the DROP TABLE block above. Re-run the DDL on a DB that already 
has it and you get: `DROP TYPE ... CASCADE` takes the `provider_type` column 
off `auth_provider`, the table itself survives (`DROP TABLE "user" CASCADE` 
only takes the FK), and `CREATE TABLE IF NOT EXISTS auth_provider` below is 
then a no-op. You're left with a column-less, constraint-less table full of 
stale rows.
   
   Add `DROP TABLE IF EXISTS auth_provider CASCADE;` before the `"user"` drop, 
and move this line down into the enum group at 87-89 where the other DROP TYPEs 
live.
   



##########
amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala:
##########
@@ -128,9 +179,11 @@ class AuthResource {
         user.setName(username)
         user.setEmail(useremail)
         user.setRole(UserRoleEnum.RESTRICTED)
-        // hash the plain text password
-        user.setPassword(new 
StrongPasswordEncryptor().encryptPassword(userpassword))
-        userDao.insert(user)
+        insertLocalUser(

Review Comment:
   The guard above this (line 169) is still `userDao.fetchByName(username)` — 
it checks the display name, but the handle now lives in 
`auth_provider.provider_id`, and `insertLocalUser`'s own scaladoc says identity 
is never re-derived from the mutable name.
   
   `ExternalAuthProvisioner.refresh` rewrites `user.name` on every Google 
login, so: alice registers locally (`name` = `provider_id` = "alice"), later 
signs in with Google as "Alice Smith" and `user.name` moves, then bob registers 
as "alice" — `fetchByName` finds nothing, and this insert hits 
`uq_provider_identity`. That's a raw `DataAccessException` → 500, not the 406 
you wanted. The mirror case rejects a handle that's actually free, because some 
Google user happens to display that name.
   
   `localHandleExists` is already defined at line 46 — that's the check 
`register` wants. Worth also mapping 23505 → 409 here the way 
`AdminUserResource.createLocalAccount` does; right now the same collision 
returns two different statuses depending on the entry point.
   



##########
amber/src/main/scala/org/apache/texera/web/resource/auth/GoogleAuthResource.scala:
##########
@@ -19,30 +19,40 @@
 
 package org.apache.texera.web.resource.auth
 
-import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
+import com.google.api.client.googleapis.auth.oauth2.{GoogleIdToken, 
GoogleIdTokenVerifier}
 import com.google.api.client.http.javanet.NetHttpTransport
 import com.google.api.client.json.gson.GsonFactory
 import org.apache.texera.auth.JwtAuth.{TOKEN_EXPIRE_TIME_IN_MINUTES, 
jwtClaims, jwtToken}
 import org.apache.texera.common.config.UserSystemConfig
-import org.apache.texera.dao.SqlServer
-import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum
-import org.apache.texera.dao.jooq.generated.tables.daos.UserDao
-import org.apache.texera.dao.jooq.generated.tables.pojos.User
+import org.apache.texera.dao.jooq.generated.enums.ProviderTypeEnum
 import org.apache.texera.web.model.http.response.TokenIssueResponse
-import org.apache.texera.web.resource.auth.GoogleAuthResource.userDao
 
 import java.util.Collections
 import javax.ws.rs._
 import javax.ws.rs.core.MediaType
 
 object GoogleAuthResource {
-  private def userDao =
-    new UserDao(
-      SqlServer
-        .getInstance()
-        .createDSLContext()
-        .configuration
+
+  /**
+    * Reduce a verified Google id-token payload to the fields we persist. 
Google omits `name`
+    * for accounts with no profile name, and the provisioner writes `name` 
straight to a NOT
+    * NULL column, so the address stands in for it. Only the last path segment 
of `picture` is
+    * kept — the frontend rebuilds the full `lh3.googleusercontent.com` URL 
around it.
+    */
+  private[auth] def profileOf(payload: GoogleIdToken.Payload): ExternalProfile 
= {
+    val googleEmail = payload.getEmail
+    ExternalProfile(
+      ProviderTypeEnum.GOOGLE,
+      payload.getSubject,
+      
Option(payload.get("name").asInstanceOf[String]).filter(_.nonEmpty).getOrElse(googleEmail),
+      googleEmail,
+      Some(

Review Comment:
   `ExternalProfile.avatar`'s scaladoc says `None` means "the provider supplies 
no avatar, so the user's existing avatar column is left untouched rather than 
overwritten" — but this wraps unconditionally in `Some(...)` with a `""` 
fallback, so `None` is unreachable and a token with no `picture` claim writes 
`""` over whatever was stored. Same as the old behavior, so not a regression, 
but it isn't what the new doc promises. Dropping the `Some(` and letting the 
`Option` through makes the documented path real.
   



##########
amber/src/main/scala/org/apache/texera/web/resource/auth/ExternalAuthProvisioner.scala:
##########
@@ -0,0 +1,147 @@
+package org.apache.texera.web.resource.auth

Review Comment:
   License header goes above the `package` line — this is the only `.scala` 
file in the repo with it the other way round. RAT passes either way, so nothing 
catches it.
   



##########
amber/src/main/scala/org/apache/texera/web/resource/auth/ExternalAuthProvisioner.scala:
##########
@@ -0,0 +1,147 @@
+package org.apache.texera.web.resource.auth
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.texera.dao.SqlServer
+import org.apache.texera.dao.jooq.generated.Tables.{AUTH_PROVIDER, USER}
+import org.apache.texera.dao.jooq.generated.enums.{ProviderTypeEnum, 
UserRoleEnum}
+import org.apache.texera.dao.jooq.generated.tables.daos.{AuthProviderDao, 
UserDao}
+import org.apache.texera.dao.jooq.generated.tables.pojos.{AuthProvider, User}
+import org.jooq.DSLContext
+
+import java.time.OffsetDateTime
+import scala.util.chaining.scalaUtilChainingOps
+
+/**
+  * A verified external identity (Google, Facebook, ...) reduced to the fields 
we
+  * persist. `avatar` is optional: `None` means the provider supplies no 
avatar, so
+  * the user's existing avatar column is left untouched rather than 
overwritten.
+  */
+final case class ExternalProfile(
+    providerType: ProviderTypeEnum,
+    providerId: String,
+    name: String,
+    email: String,
+    avatar: Option[String] = None
+)
+
+object ExternalAuthProvisioner {
+
+  /**
+    * Resolve the user behind an external identity, creating one if necessary, 
and
+    * ensure its auth-provider row is present and up to date. Runs in a single
+    * transaction and returns the (possibly newly created) user.
+    */
+  def loginOrProvision(profile: ExternalProfile): User =
+    SqlServer.withTransaction(SqlServer.getInstance().createDSLContext()) { 
ctx =>
+      val txUserDao = new UserDao(ctx.configuration())
+      val txAuthDao = new AuthProviderDao(ctx.configuration())
+
+      Option(
+        ctx
+          .select()
+          .from(USER)
+          .join(AUTH_PROVIDER)
+          .on(USER.UID.eq(AUTH_PROVIDER.UID))
+          .where(AUTH_PROVIDER.PROVIDER_TYPE.eq(profile.providerType))
+          .and(AUTH_PROVIDER.PROVIDER_ID.eq(profile.providerId))
+          .fetchOne()
+      ) match {
+        case Some(record) =>
+          // known identity: refresh the profile fields if they drifted
+          txUserDao.fetchOneByUid(record.get(USER.UID)).tap { user =>
+            if (refresh(user, profile)) txUserDao.update(user)
+          }
+
+        case None =>
+          val user = Option(txUserDao.fetchOneByEmail(profile.email)) match {

Review Comment:
   This links a verified external identity onto an existing account purely on 
an email match, with no `email_verified` check. Inherited from the old 
`GoogleAuthResource`, so not a regression — but this class is explicitly the 
generic entry point for "github, IEEE accounts, etc.", and the first provider 
that doesn't verify email turns this into account takeover. Worth an 
`emailVerified: Boolean` on `ExternalProfile` (Google gives you 
`email_verified` in the payload) before the second provider lands.
   



##########
sql/texera_ddl.sql:
##########
@@ -102,16 +104,25 @@ CREATE TABLE IF NOT EXISTS "user"
     uid                     SERIAL PRIMARY KEY,
     name                    VARCHAR(256) NOT NULL,
     email                   VARCHAR(256) UNIQUE,
-    password                VARCHAR(256),
-    google_id               VARCHAR(256) UNIQUE,
-    google_avatar           VARCHAR(100),
+    avatar           VARCHAR(100),
     role                    user_role_enum NOT NULL DEFAULT 'INACTIVE',
     comment                 TEXT,
     account_creation_time   TIMESTAMPTZ NOT NULL DEFAULT now(),
     affiliation             VARCHAR(128),
-    joining_reason          VARCHAR(500),
-    -- check that either password or google_id is not null
-    CONSTRAINT ck_nulltest CHECK ((password IS NOT NULL) OR (google_id IS NOT 
NULL))
+    joining_reason          VARCHAR(500)
+    );
+
+CREATE TABLE IF NOT EXISTS auth_provider
+(
+    uid               INT                 NOT NULL,
+    provider_type     provider_type_enum  NOT NULL,
+    provider_id       VARCHAR(256)        NOT NULL,
+    password          VARCHAR(256),
+    created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
+    PRIMARY KEY (uid, provider_type),
+    FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE,
+    CONSTRAINT uq_provider_identity UNIQUE (provider_type, provider_id),
+    CONSTRAINT ck_provider_credential CHECK ((provider_type = 'LOCAL') = 
(password IS NOT NULL))

Review Comment:
   Worth noting explicitly: the old `ck_nulltest` guaranteed every user had at 
least one credential, and nothing replaces it — a `user` row with zero 
`auth_provider` rows is now legal and simply can't log in. `30.sql` only `RAISE 
NOTICE`s those. Probably the right call since it can't be a row-level check 
anymore, just want it to be a deliberate decision rather than a side effect.
   



##########
amber/src/test/scala/org/apache/texera/web/auth/ExternalAuthProvisionerSpec.scala:
##########
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.web.resource.auth

Review Comment:
   Package is `...web.resource.auth` but the file sits in `web/auth/`. 
`AuthResourceSpec` and `GoogleAuthResourceSpec` are both under 
`web/resource/auth/` — move this one next to them.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to