Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6819261
migration sql script
Neilk1021 Jul 28, 2026
ae02630
refactor(sql): swap sql tables to add auth_provider table
Neilk1021 Jul 28, 2026
8cf34ce
refactor(sql): migrate local and google auth to use new table def
Neilk1021 Jul 28, 2026
87adc7d
refactor(auth): Migrate admin UserResource to new jOOq def.
Neilk1021 Jul 29, 2026
913daaf
refactor(auth): Refactor `AdminUserResource.scala` to match jOOq tables.
Neilk1021 Jul 29, 2026
b3682dd
refactor(auth): Refactor `AdminUserResource.scala` to match jOOq tables.
Neilk1021 Jul 29, 2026
5bad80c
Merge branch 'apache:main' into task/migrateBackend
Neilk1021 Jul 29, 2026
94d2fa9
refactor(auth): Refactor `AdminUserResource.scala` to match jOOq tables.
Neilk1021 Jul 29, 2026
2766d9c
Merge branch 'task/migrateBackend' of https://github.com/Neilk1021/te…
Neilk1021 Jul 29, 2026
5ec16e0
fix(auth): fix spec tests for branch
Neilk1021 Jul 29, 2026
ebb758b
Merge branch 'main' into task/migrateBackend
Neilk1021 Jul 30, 2026
53ca02f
refactor(auth): readd migration after git merge
Neilk1021 Jul 30, 2026
33d70b6
Merge remote-tracking branch 'upstream/main' into task/migrateBackend
Neilk1021 Jul 30, 2026
70fb360
refactor(auth): update `AuthResource` with main.
Neilk1021 Jul 30, 2026
e417d9c
refactor(auth): update `AuthResource` with main.
Neilk1021 Jul 30, 2026
f8c67d7
fix(auth): fix spec tests
Neilk1021 Jul 30, 2026
a36b9db
Merge remote-tracking branch 'upstream/main' into task/migrateBackend
Neilk1021 Jul 30, 2026
784f6a4
fix(auth): fix coverage
Neilk1021 Jul 30, 2026
8edb710
fix(auth): fix coverage
Neilk1021 Jul 30, 2026
0f5e89d
Merge branch 'main' into task/migrateBackend
Neilk1021 Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ class AccessControlResourceSpec
user.setName("testuser")
user.setEmail("test@example.com")
user.setRole(UserRoleEnum.REGULAR)
user.setPassword("password")
user
}

Expand All @@ -76,7 +75,6 @@ class AccessControlResourceSpec
user.setName("testuser2")
user.setEmail("test2@example.com")
user.setRole(UserRoleEnum.REGULAR)
user.setPassword("password")
user
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ class LiteLLMProxyAuthSpec extends AnyFlatSpec with Matchers with BeforeAndAfter
u.setUid(1)
u.setName("test")
u.setEmail("test@example.com")
u.setGoogleId(null)
u.setRole(role)
JwtAuth.jwtToken(JwtAuth.jwtClaims(u, expireInDays = 1))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@

package org.apache.texera.web.resource.auth

import com.typesafe.scalalogging.Logger
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.Tables.USER
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.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.apache.texera.web.model.http.request.auth.{UserLoginRequest, UserRegistrationRequest}
import org.apache.texera.web.model.http.response.TokenIssueResponse
import org.apache.texera.web.resource.auth.AuthResource._
Expand All @@ -35,53 +36,103 @@ import javax.ws.rs._
import javax.ws.rs.core.MediaType

object AuthResource {
private val logger: Logger = Logger(classOf[AuthResource])

private def userDao =
new UserDao(
SqlServer
.getInstance()
.createDSLContext()
.configuration
private def context = SqlServer.getInstance().context
private def userDao = new UserDao(context.configuration)

private val passwordEncryptor = new StrongPasswordEncryptor

private def localHandleExists(handle: String): Boolean = {
context.fetchExists(
context
.selectFrom(AUTH_PROVIDER)
.where(AUTH_PROVIDER.PROVIDER_TYPE.eq(ProviderTypeEnum.LOCAL))
.and(AUTH_PROVIDER.PROVIDER_ID.eq(handle))
)
}

//TODO ASSERT THAT ALL USERS WERE MIGRATED CORRECTLY AND CHECK

/**
* Retrieve exactly one User from databases with the given username and password.
* The password is used to validate against the hashed password stored in the db.
*
* @param name String
* @param username String
* @param password String, plain text password
* @return
*/
def retrieveUserByUsernameAndPassword(name: String, password: String): Option[User] = {
if (password == null) return None
if (name == null) return None
Option(
SqlServer
.getInstance()
.createDSLContext()
.select()
.from(USER)
.where(USER.NAME.eq(name))
.fetchOneInto(classOf[User])
).filter(user => new StrongPasswordEncryptor().checkPassword(password, user.getPassword))
}
def retrieveUserByUsernameAndPassword(username: String, password: String): Option[User] = {
if (password == null || username == null) return None

def createAdminUser(): Unit = {
val adminUsername = UserSystemConfig.adminUsername
val adminPassword = UserSystemConfig.adminPassword
val record = context
.select()
.from(AUTH_PROVIDER)
.join(USER)
.on(USER.UID.eq(AUTH_PROVIDER.UID))
.where(AUTH_PROVIDER.PROVIDER_TYPE.eq(ProviderTypeEnum.LOCAL))
.and(AUTH_PROVIDER.PROVIDER_ID.eq(username))
.fetchOne()

if (adminUsername.trim.nonEmpty && adminPassword.trim.nonEmpty) {
val existingUser = userDao.fetchByName(adminUsername)
if (existingUser.isEmpty) {
val user = new User
user.setName(adminUsername)
user.setEmail(adminUsername)
user.setRole(UserRoleEnum.ADMIN)
user.setPassword(new StrongPasswordEncryptor().encryptPassword(adminPassword))
userDao.insert(user)
Option(record).flatMap(r => {
val encryptedPassword = r.get(AUTH_PROVIDER.PASSWORD)
if (passwordEncryptor.checkPassword(password, encryptedPassword)) {
Some(r.into(USER).into(classOf[User]))
} else {
None
}
})
}

/**
* Create a user together with the LOCAL credential it logs in with. The handle is passed
* explicitly rather than read off `user.getName`, so that identity is never re-derived
* from the mutable display name.
*/
private def insertLocalUser(user: User, handle: String, hashedPassword: String): Unit = {
SqlServer.withTransaction(SqlServer.getInstance().createDSLContext()) { ctx =>
val txUserDao = new UserDao(ctx.configuration())
val txAuthDao = new AuthProviderDao(ctx.configuration())

txUserDao.insert(user)

val auth = new AuthProvider
auth.setUid(user.getUid)
auth.setProviderType(ProviderTypeEnum.LOCAL)
auth.setProviderId(handle)
auth.setPassword(hashedPassword)
txAuthDao.insert(auth)
}
}

def createAdminUser(): Unit =
createAdminUser(UserSystemConfig.adminUsername.trim, UserSystemConfig.adminPassword.trim)

/**
* Bootstrap the configured admin account, doing nothing if it already exists. The credentials
* are parameters rather than reads of [[UserSystemConfig]] because those are object vals
* resolved once per JVM, which leaves the unconfigured case unreachable from a test.
*/
private[auth] def createAdminUser(adminUsername: String, adminPassword: String): Unit = {
if (adminUsername.isEmpty || adminPassword.isEmpty) return

if (localHandleExists(adminUsername)) return

if (userDao.fetchOneByEmail(adminUsername) != null) {
logger.warn(
s"Not creating the admin account: '$adminUsername' is already used as an email address " +
"by an account with no local credential. Grant that account the ADMIN role instead."
)
return
}

val user = new User
user.setName(adminUsername)
user.setEmail(adminUsername)
user.setRole(UserRoleEnum.ADMIN)

insertLocalUser(user, adminUsername, passwordEncryptor.encryptPassword(adminPassword))
}
}

@Path("/auth/")
Expand Down Expand Up @@ -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(
user,
username,
AuthResource.passwordEncryptor.encryptPassword(userpassword)
)
TokenIssueResponse(jwtToken(jwtClaims(user, TOKEN_EXPIRE_TIME_IN_MINUTES)))
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
case Some(existing) =>
existing.tap { user =>
if (refresh(user, profile)) txUserDao.update(user)
}
case None =>
new User().tap { user =>
user.setName(profile.name)
user.setEmail(profile.email)
profile.avatar.foreach(user.setAvatar)
user.setRole(UserRoleEnum.INACTIVE)
txUserDao.insert(user)
}
}

upsertProvider(ctx, txAuthDao, user, profile)
user
}
}

/**
* Mutate `user` in place to match `profile`, returning true iff anything changed
* (so the caller only issues an UPDATE when needed).
*/
private def refresh(user: User, profile: ExternalProfile): Boolean = {
var changed = false
if (user.getName != profile.name) {
user.setName(profile.name)
changed = true
}
if (user.getEmail != profile.email) {
user.setEmail(profile.email)
changed = true
}
profile.avatar.foreach { avatar =>
if (user.getAvatar != avatar) {
user.setAvatar(avatar)
changed = true
}
}
changed
}

private def upsertProvider(
ctx: DSLContext,
authDao: AuthProviderDao,
user: User,
profile: ExternalProfile
): Unit = {
val hasProvider = ctx.fetchExists(
ctx
.selectFrom(AUTH_PROVIDER)
.where(AUTH_PROVIDER.UID.eq(user.getUid))
.and(AUTH_PROVIDER.PROVIDER_TYPE.eq(profile.providerType))
)
if (hasProvider) {
ctx
.update(AUTH_PROVIDER)
.set(AUTH_PROVIDER.PROVIDER_ID, profile.providerId)
.where(AUTH_PROVIDER.UID.eq(user.getUid))
.and(AUTH_PROVIDER.PROVIDER_TYPE.eq(profile.providerType))
.execute()
} else {
authDao.insert(
new AuthProvider().tap { auth =>
auth.setUid(user.getUid)
auth.setProviderType(profile.providerType)
auth.setProviderId(profile.providerId)
auth.setCreatedAt(OffsetDateTime.now())
}
)
}
}
}
Loading
Loading