bobbai00 commented on code in PR #3598: URL: https://github.com/apache/texera/pull/3598#discussion_r2277505180
########## core/access-control-service/build.sbt: ########## @@ -0,0 +1,84 @@ +// 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 scala.collection.Seq + +name := "access-control-service" +organization := "edu.uci.ics" Review Comment: Rename the service, folders and files as `auth-service`, as we have an `auth` module ########## core/access-control-service/src/main/scala/edu/uci/ics/texera/service/resource/AccessControlResource.scala: ########## @@ -0,0 +1,93 @@ +package edu.uci.ics.texera.service.resource + +import com.typesafe.scalalogging.LazyLogging +import edu.uci.ics.texera.auth.JwtParser.parseToken +import edu.uci.ics.texera.auth.SessionUser +import edu.uci.ics.texera.dao.SqlServer +import edu.uci.ics.texera.dao.jooq.generated.enums.PrivilegeEnum +import edu.uci.ics.texera.service.util.ComputingUnit +import jakarta.ws.rs.{GET, POST, Path, PathParam, Produces} +import jakarta.ws.rs.core.{Context, HttpHeaders, MediaType, Response, UriInfo} + +import java.util.Optional +import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsScala} + +@Produces(Array(MediaType.APPLICATION_JSON)) +@Path("/authorize") +class AccessControlResource extends LazyLogging { + + private val computingUnit: ComputingUnit = new ComputingUnit() + + private def performAuth( + uriInfo: UriInfo, + headers: HttpHeaders + ): Response = { + val queryParams: Map[String, String] = uriInfo + .getQueryParameters() + .asScala + .view + .mapValues(values => values.asScala.headOption.getOrElse("")) + .toMap + + logger.info(s"Request URI: ${uriInfo.getRequestUri} and headers: ${headers.getRequestHeaders.asScala} and queryParams: $queryParams") + + val token = queryParams.getOrElse( + "access-token", Review Comment: Instead of parsing token manually, you can register the JWTAuthFilter in `AccessControlService`: ``` // Register JWT authentication filter environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) ``` And simply use ` @Auth user: SessionUser` at the below request handler. For details, check examples like `FileService.scala` and `DatasetResource` ########## core/build.sbt: ########## @@ -27,6 +27,14 @@ lazy val ConfigService = (project in file("config-service")) "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.17.0" ) ) +lazy val PermissionService = (project in file("access-control-service")) + .dependsOn(Auth, Config) Review Comment: Rename to `AuthService` ########## core/access-control-service/src/main/scala/edu/uci/ics/texera/service/resource/AccessControlResource.scala: ########## @@ -0,0 +1,93 @@ +package edu.uci.ics.texera.service.resource + +import com.typesafe.scalalogging.LazyLogging +import edu.uci.ics.texera.auth.JwtParser.parseToken +import edu.uci.ics.texera.auth.SessionUser +import edu.uci.ics.texera.dao.SqlServer +import edu.uci.ics.texera.dao.jooq.generated.enums.PrivilegeEnum +import edu.uci.ics.texera.service.util.ComputingUnit +import jakarta.ws.rs.{GET, POST, Path, PathParam, Produces} +import jakarta.ws.rs.core.{Context, HttpHeaders, MediaType, Response, UriInfo} + +import java.util.Optional +import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsScala} + +@Produces(Array(MediaType.APPLICATION_JSON)) +@Path("/authorize") +class AccessControlResource extends LazyLogging { Review Comment: rename the api to `/auth` ########## core/access-control-service/src/main/scala/edu/uci/ics/texera/service/util/ComputingUnit.scala: ########## @@ -0,0 +1,39 @@ +package edu.uci.ics.texera.service.util + +import edu.uci.ics.texera.dao.SqlServer +import edu.uci.ics.texera.dao.jooq.generated.enums.PrivilegeEnum +import edu.uci.ics.texera.dao.jooq.generated.tables.daos.{ComputingUnitUserAccessDao, WorkflowComputingUnitDao} +import edu.uci.ics.texera.service.util.ComputingUnit._ +import org.jooq.DSLContext + +import scala.jdk.CollectionConverters._ + + +object ComputingUnit { + private lazy val context: DSLContext = SqlServer + .getInstance() + .createDSLContext() +} + +class ComputingUnit { Review Comment: move this to `core/auth` module, rename to `ComputingUnitAccess` ########## core/amber/src/main/scala/edu/uci/ics/texera/web/ServletAwareConfigurator.scala: ########## @@ -29,46 +30,76 @@ import java.nio.charset.Charset import javax.websocket.HandshakeResponse import javax.websocket.server.{HandshakeRequest, ServerEndpointConfig} import scala.jdk.CollectionConverters.ListHasAsScala +import scala.jdk.CollectionConverters._ /** - * This configurator extracts HTTPSession and associates it to ServerEndpointConfig, - * allow it to be accessed by Websocket connections. - * <pre> - * See <a href="https://stackoverflow.com/questions/17936440/accessing-httpsession- - * from-httpservletrequest-in-a-web-socket-serverendpoint"></a> - * </pre> - */ + * This configurator extracts HTTPSession and associates it to ServerEndpointConfig, Review Comment: this changes are redundant ########## core/access-control-service/build.sbt: ########## @@ -0,0 +1,84 @@ +// 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 scala.collection.Seq + +name := "access-control-service" +organization := "edu.uci.ics" +version := "1.0.0" + +scalaVersion := "2.13.12" + +enablePlugins(JavaAppPackaging) + +// Enable semanticdb for Scalafix +ThisBuild / semanticdbEnabled := true +ThisBuild / semanticdbVersion := scalafixSemanticdb.revision + +// Manage dependency conflicts by always using the latest revision +ThisBuild / conflictManager := ConflictManager.latestRevision + +// Restrict parallel execution of tests to avoid conflicts +Global / concurrentRestrictions += Tags.limit(Tags.Test, 1) + +///////////////////////////////////////////////////////////////////////////// +// Compiler Options +///////////////////////////////////////////////////////////////////////////// + +// Scala compiler options +Compile / scalacOptions ++= Seq( + "-Xelide-below", "WARNING", // Turn on optimizations with "WARNING" as the threshold + "-feature", // Check feature warnings + "-deprecation", // Check deprecation warnings + "-Ywarn-unused:imports" // Check for unused imports +) + +///////////////////////////////////////////////////////////////////////////// +// Version Variables +///////////////////////////////////////////////////////////////////////////// + +val dropwizardVersion = "4.0.7" +val mockitoVersion = "5.4.0" +val assertjVersion = "3.24.2" + +///////////////////////////////////////////////////////////////////////////// +// Test-related Dependencies +///////////////////////////////////////////////////////////////////////////// + +libraryDependencies ++= Seq( + "org.scalamock" %% "scalamock" % "5.2.0" % Test, // ScalaMock + "org.scalatest" %% "scalatest" % "3.2.17" % Test, // ScalaTest + "io.dropwizard" % "dropwizard-testing" % dropwizardVersion % Test, // Dropwizard Testing + "org.mockito" % "mockito-core" % mockitoVersion % Test, // Mockito for mocking + "org.assertj" % "assertj-core" % assertjVersion % Test, // AssertJ for assertions + "com.novocode" % "junit-interface" % "0.11" % Test // SBT interface for JUnit +) + +///////////////////////////////////////////////////////////////////////////// +// Dependencies +///////////////////////////////////////////////////////////////////////////// + +// Core Dependencies +libraryDependencies ++= Seq( + "io.dropwizard" % "dropwizard-core" % dropwizardVersion, + "io.dropwizard" % "dropwizard-auth" % dropwizardVersion, // Dropwizard Authentication module + "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.15.2", + "jakarta.ws.rs" % "jakarta.ws.rs-api" % "3.1.0", // Ensure Jakarta JAX-RS API is available + "org.bitbucket.b_c" % "jose4j" % "0.9.6", + "org.playframework" %% "play-json" % "3.1.0-M1", + "com.typesafe" % "config" % "1.4.2" // For configuration management Review Comment: most of these dependencies should be redundant as this module should depend on `core/auth`, which already import these dependencies ########## core/access-control-service/src/main/scala/edu/uci/ics/texera/service/resource/AccessControlResource.scala: ########## @@ -0,0 +1,93 @@ +package edu.uci.ics.texera.service.resource + +import com.typesafe.scalalogging.LazyLogging +import edu.uci.ics.texera.auth.JwtParser.parseToken +import edu.uci.ics.texera.auth.SessionUser +import edu.uci.ics.texera.dao.SqlServer +import edu.uci.ics.texera.dao.jooq.generated.enums.PrivilegeEnum +import edu.uci.ics.texera.service.util.ComputingUnit +import jakarta.ws.rs.{GET, POST, Path, PathParam, Produces} +import jakarta.ws.rs.core.{Context, HttpHeaders, MediaType, Response, UriInfo} + +import java.util.Optional +import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsScala} + +@Produces(Array(MediaType.APPLICATION_JSON)) +@Path("/authorize") +class AccessControlResource extends LazyLogging { + + private val computingUnit: ComputingUnit = new ComputingUnit() + + private def performAuth( + uriInfo: UriInfo, + headers: HttpHeaders + ): Response = { + val queryParams: Map[String, String] = uriInfo + .getQueryParameters() + .asScala + .view + .mapValues(values => values.asScala.headOption.getOrElse("")) + .toMap + + logger.info(s"Request URI: ${uriInfo.getRequestUri} and headers: ${headers.getRequestHeaders.asScala} and queryParams: $queryParams") + + val token = queryParams.getOrElse( + "access-token", + headers + .getRequestHeader("Authorization") + .asScala + .headOption + .getOrElse("") + .replace("Bearer ", "") + ) + val cuid = queryParams.getOrElse("cuid", "") + val cuidInt = try { + cuid.toInt + } catch { + case _: NumberFormatException => + return Response.status(Response.Status.FORBIDDEN).build() + } + + var cuAccess: PrivilegeEnum = PrivilegeEnum.NONE + var userSession: Optional[SessionUser] = Optional.empty() + try { + userSession = parseToken(token) + if (userSession.isEmpty) + return Response.status(Response.Status.FORBIDDEN).build() + + val uid = userSession.get().getUid + cuAccess = computingUnit.getComputingUnitAccess(cuidInt, uid) + if (cuAccess == PrivilegeEnum.NONE) + return Response.status(Response.Status.FORBIDDEN).build() + } catch { + case e: Exception => + return Response.status(Response.Status.FORBIDDEN).build() + } + + Response + .ok() + .header("x-user-cu-access", cuAccess.toString) + .header("x-user-id", userSession.get().getUid.toString) + .header("x-user-name", userSession.get().getName) + .header("x-user-email", userSession.get().getEmail) + .build() + } + + @GET + @Path("/{path:.*}") + def authorizeGet( + @Context uriInfo: UriInfo, + @Context headers: HttpHeaders + ): Response = { Review Comment: As stated above, use the `@Auth user: SessionUser` here. ########## core/amber/src/main/scala/edu/uci/ics/texera/web/ServletAwareConfigurator.scala: ########## @@ -29,46 +30,76 @@ import java.nio.charset.Charset import javax.websocket.HandshakeResponse import javax.websocket.server.{HandshakeRequest, ServerEndpointConfig} import scala.jdk.CollectionConverters.ListHasAsScala +import scala.jdk.CollectionConverters._ /** - * This configurator extracts HTTPSession and associates it to ServerEndpointConfig, - * allow it to be accessed by Websocket connections. - * <pre> - * See <a href="https://stackoverflow.com/questions/17936440/accessing-httpsession- - * from-httpservletrequest-in-a-web-socket-serverendpoint"></a> - * </pre> - */ + * This configurator extracts HTTPSession and associates it to ServerEndpointConfig, + * allow it to be accessed by Websocket connections. + * <pre> + * See <a href="https://stackoverflow.com/questions/17936440/accessing-httpsession- + * from-httpservletrequest-in-a-web-socket-serverendpoint"></a> + * </pre> + */ class ServletAwareConfigurator extends ServerEndpointConfig.Configurator with LazyLogging { override def modifyHandshake( - config: ServerEndpointConfig, - request: HandshakeRequest, - response: HandshakeResponse - ): Unit = { + config: ServerEndpointConfig, + request: HandshakeRequest, + response: HandshakeResponse + ): Unit = { try { - val params = - URLEncodedUtils.parse(new URI("?" + request.getQueryString), Charset.defaultCharset()) - params.asScala - .map(pair => pair.getName -> pair.getValue) - .toMap - .get("access-token") - .map(token => { - val claims = jwtConsumer.process(token).getJwtClaims - config.getUserProperties.put( - classOf[User].getName, - new User( - claims.getClaimValue("userId").asInstanceOf[Long].toInt, - claims.getSubject, - String.valueOf(claims.getClaimValue("email").asInstanceOf[String]), - null, - null, - null, - null, - null - ) + if (KubernetesConfig.kubernetesComputingUnitEnabled) { + // KUBERNETES MODE: Construct the User object from trusted headers + // coming from envoy and generated by permission service. + val headers = request.getHeaders.asScala.view.mapValues(_.asScala.headOption).toMap + + val userId = headers.get("x-user-id").flatten.map(_.toInt).get Review Comment: Is this differentiation really needed? -- 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]
