From 0c3af5e620fa3b6ad684ab311da5da6cd7ee859a Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Wed, 27 May 2026 20:45:22 -0700 Subject: [PATCH 01/30] added backend notebook-migration-service related files --- build.sbt | 12 +- common/config/src/main/resources/storage.conf | 7 + .../texera/amber/config/StorageConfig.scala | 3 + frontend/proxy.config.json | 5 + notebook-migration-service/build.sbt | 77 ++++ .../project/build.properties | 18 + .../src/main/resources/logback.xml | 55 +++ ...notebook-migration-service-web-config.yaml | 36 ++ .../service/NotebookMigrationService.scala | 80 ++++ ...otebookMigrationServiceConfiguration.scala | 22 ++ .../resource/NotebookMigrationResource.scala | 358 ++++++++++++++++++ 11 files changed, 672 insertions(+), 1 deletion(-) create mode 100644 notebook-migration-service/build.sbt create mode 100644 notebook-migration-service/project/build.properties create mode 100644 notebook-migration-service/src/main/resources/logback.xml create mode 100644 notebook-migration-service/src/main/resources/notebook-migration-service-web-config.yaml create mode 100644 notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala create mode 100644 notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationServiceConfiguration.scala create mode 100644 notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala diff --git a/build.sbt b/build.sbt index 3767e7a6c21..4c4c2790104 100644 --- a/build.sbt +++ b/build.sbt @@ -167,6 +167,15 @@ lazy val WorkflowExecutionService = (project in file("amber")) ) .configs(Test) .dependsOn(DAO % "test->test", Auth % "test->test") // test scope dependency +lazy val NotebookMigrationService = (project in file("notebook-migration-service")) + .dependsOn(Auth, Config, DAO) + .settings(asfLicensingSettings) + .settings( + dependencyOverrides ++= Seq( + // override it as io.dropwizard 4 require 2.16.1 or higher + "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.17.0" + ) + ) // root project definition lazy val TexeraProject = (project in file(".")) @@ -184,7 +193,8 @@ lazy val TexeraProject = (project in file(".")) ConfigService, FileService, WorkflowCompilingService, - WorkflowExecutionService + WorkflowExecutionService, + NotebookMigrationService ) .settings( name := "texera", diff --git a/common/config/src/main/resources/storage.conf b/common/config/src/main/resources/storage.conf index 0682109f19e..4fdcf0c1694 100644 --- a/common/config/src/main/resources/storage.conf +++ b/common/config/src/main/resources/storage.conf @@ -146,4 +146,11 @@ storage { password = "postgres" password = ${?STORAGE_JDBC_PASSWORD} } + + # Configurations of the JupyterLab service + # Default values are provided for each field, which you don't need to change if you deployed Jupyter via docker-compose.yml in notebook-migration-service/src/main/resources/docker-compose.yml + jupyter { + url = "http://localhost:9100" + url = ${?STORAGE_JUPYTER_URL} + } } diff --git a/common/config/src/main/scala/org/apache/texera/amber/config/StorageConfig.scala b/common/config/src/main/scala/org/apache/texera/amber/config/StorageConfig.scala index 07447cfdbee..bbbc9ffbaee 100644 --- a/common/config/src/main/scala/org/apache/texera/amber/config/StorageConfig.scala +++ b/common/config/src/main/scala/org/apache/texera/amber/config/StorageConfig.scala @@ -132,4 +132,7 @@ object StorageConfig { val ENV_S3_REGION = "STORAGE_S3_REGION" val ENV_S3_AUTH_USERNAME = "STORAGE_S3_AUTH_USERNAME" val ENV_S3_AUTH_PASSWORD = "STORAGE_S3_AUTH_PASSWORD" + + // Jupyter + val jupyterURL: String = conf.getString("storage.jupyter.url") } diff --git a/frontend/proxy.config.json b/frontend/proxy.config.json index f68602e0714..7d68961d9b6 100755 --- a/frontend/proxy.config.json +++ b/frontend/proxy.config.json @@ -10,6 +10,11 @@ "secure": false, "changeOrigin": true }, + "/api/notebook-migration": { + "target": "http://localhost:9098", + "secure": false, + "changeOrigin": true + }, "/api/models": { "target": "http://localhost:9096", "secure": false, diff --git a/notebook-migration-service/build.sbt b/notebook-migration-service/build.sbt new file mode 100644 index 00000000000..f744653c63c --- /dev/null +++ b/notebook-migration-service/build.sbt @@ -0,0 +1,77 @@ +// 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 := "notebook-migration-service" + + +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.18.6" +) \ No newline at end of file diff --git a/notebook-migration-service/project/build.properties b/notebook-migration-service/project/build.properties new file mode 100644 index 00000000000..d0ba9f1421a --- /dev/null +++ b/notebook-migration-service/project/build.properties @@ -0,0 +1,18 @@ +# 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. + +sbt.version = 1.12.9 \ No newline at end of file diff --git a/notebook-migration-service/src/main/resources/logback.xml b/notebook-migration-service/src/main/resources/logback.xml new file mode 100644 index 00000000000..ffcc406a752 --- /dev/null +++ b/notebook-migration-service/src/main/resources/logback.xml @@ -0,0 +1,55 @@ + + + + + + + [%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n + + + + + + + logs/notebook-migration-service.log + true + + logs/notebook-migration-service-%d{yyyy-MM-dd}.log.gz + + + [%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n + + + + + 8192 + true + + + + + + + + + + + \ No newline at end of file diff --git a/notebook-migration-service/src/main/resources/notebook-migration-service-web-config.yaml b/notebook-migration-service/src/main/resources/notebook-migration-service-web-config.yaml new file mode 100644 index 00000000000..947dd59c4f5 --- /dev/null +++ b/notebook-migration-service/src/main/resources/notebook-migration-service-web-config.yaml @@ -0,0 +1,36 @@ +# 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. + +server: + applicationConnectors: + - type: http + port: 9098 + adminConnectors: [] + requestLog: + type: classic + appenders: [] + +logging: + level: ${TEXERA_SERVICE_LOG_LEVEL:-INFO} + appenders: + - type: console + threshold: ${TEXERA_SERVICE_LOG_LEVEL:-INFO} + - type: file + currentLogFilename: logs/notebook-migration-service.log + archive: true + archivedLogFilenamePattern: logs/notebook-migration-service-%d.log.gz + archivedFileCount: 5 \ No newline at end of file diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala new file mode 100644 index 00000000000..9effd07436e --- /dev/null +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala @@ -0,0 +1,80 @@ +// 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.service + +import com.fasterxml.jackson.module.scala.DefaultScalaModule +import com.typesafe.scalalogging.LazyLogging +import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider} +import io.dropwizard.core.Application +import io.dropwizard.core.setup.{Bootstrap, Environment} +import org.apache.texera.amber.config.StorageConfig +import org.apache.texera.auth.RequestLoggingFilter +import org.apache.texera.dao.SqlServer +import java.nio.file.Path +import org.apache.texera.service.resource.NotebookMigrationResource + +class NotebookMigrationService + extends Application[NotebookMigrationServiceConfiguration] + with LazyLogging { + override def initialize(bootstrap: Bootstrap[NotebookMigrationServiceConfiguration]): Unit = { + // enable environment variable substitution in YAML config + bootstrap.setConfigurationSourceProvider( + new SubstitutingSourceProvider( + bootstrap.getConfigurationSourceProvider, + new EnvironmentVariableSubstitutor(false) + ) + ) + // Register Scala module to Dropwizard default object mapper + bootstrap.getObjectMapper.registerModule(DefaultScalaModule) + + SqlServer.initConnection( + StorageConfig.jdbcUrl, + StorageConfig.jdbcUsername, + StorageConfig.jdbcPassword + ) + } + + override def run( + configuration: NotebookMigrationServiceConfiguration, + environment: Environment + ): Unit = { + // Serve backend at /api + environment.jersey.setUrlPattern("/api/*") + + environment.jersey.register(classOf[NotebookMigrationResource]) + + // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL + RequestLoggingFilter.register(environment.getApplicationContext) + } +} +object NotebookMigrationService { + def main(args: Array[String]): Unit = { + val notebookMigrationPath = Path + .of(sys.env.getOrElse("TEXERA_HOME", ".")) + .resolve("notebook-migration-service") + .resolve("src") + .resolve("main") + .resolve("resources") + .resolve("notebook-migration-service-web-config.yaml") + .toAbsolutePath + .toString + + // Start the Dropwizard application + new NotebookMigrationService().run("server", notebookMigrationPath) + } +} diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationServiceConfiguration.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationServiceConfiguration.scala new file mode 100644 index 00000000000..dc908e7565e --- /dev/null +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationServiceConfiguration.scala @@ -0,0 +1,22 @@ +// 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.service + +import io.dropwizard.core.Configuration + +class NotebookMigrationServiceConfiguration extends Configuration {} diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala new file mode 100644 index 00000000000..6b06d883619 --- /dev/null +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -0,0 +1,358 @@ +// 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.service.resource + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.scala.DefaultScalaModule +import com.typesafe.scalalogging.LazyLogging +import jakarta.ws.rs._ +import jakarta.ws.rs.core._ +import org.apache.texera.dao.SqlServer +import org.jooq.JSONB +import org.apache.texera.dao.jooq.generated.tables.Notebook +import org.apache.texera.dao.jooq.generated.tables.WorkflowNotebookMapping +import java.net.{HttpURLConnection, URL} +import java.nio.charset.StandardCharsets +import scala.util.control.NonFatal +import org.apache.texera.amber.config.StorageConfig + +object NotebookMigrationResource extends LazyLogging { + + private val mapper: ObjectMapper = new ObjectMapper().registerModule(DefaultScalaModule) + private val jupyterUrl = StorageConfig.jupyterURL + private var jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb" + + private def isJupyterAvailable(jupyterUrl: String): Boolean = { + try { + val conn = new java.net.URL(s"$jupyterUrl/api") + .openConnection() + .asInstanceOf[java.net.HttpURLConnection] + + conn.setRequestMethod("GET") + conn.setConnectTimeout(2000) + conn.setReadTimeout(2000) + + val status = conn.getResponseCode + + status == 200 || status == 403 + } catch { + case _: Exception => false + } + } + + // Returns the Jupyter iframe reference URL + def getJupyterIframeURL(): Response = { + if (!isJupyterAvailable(jupyterUrl)) { + return Response + .status(500) + .entity( + """ + { + "success": false, + "message": "Cannot connect to Jupyter server" + } + """ + ) + .build() + } + + Response + .ok( + s""" + { + "success": true, + "url": "$jupyterIframeURL" + } + """ + ) + .build() + } + + // Returns the URL of Jupyter + def getJupyterURL(): Response = { + if (!isJupyterAvailable(jupyterUrl)) { + return Response + .status(500) + .entity( + """ + { + "success": false, + "message": "Cannot connect to Jupyter server" + } + """ + ) + .build() + } + + Response + .ok( + s""" + { + "success": true, + "url": "$jupyterUrl" + } + """ + ) + .build() + } + + // Set the notebook in Jupyter + def setNotebook(body: String): Response = { + if (!isJupyterAvailable(jupyterUrl)) { + return Response + .status(500) + .entity( + """ + { + "success": false, + "message": "Cannot connect to Jupyter server" + } + """ + ) + .build() + } + + try { + val json = mapper.readTree(body) + + val notebookName = json.get("notebookName").asText() + val notebookData = json.get("notebookData") + + // Construct Jupyter API URL + val apiUrl = s"$jupyterUrl/api/contents/work/$notebookName" + + val url = new URL(apiUrl) + val conn = url.openConnection().asInstanceOf[HttpURLConnection] + + conn.setRequestMethod("PUT") + conn.setDoOutput(true) + conn.setRequestProperty("Content-Type", "application/json") + + val requestBody = + s""" + { + "type": "notebook", + "content": $notebookData + } + """ + + val os = conn.getOutputStream + os.write(requestBody.getBytes(StandardCharsets.UTF_8)) + os.flush() + os.close() + + val status = conn.getResponseCode + + if (status != 200 && status != 201) { + return Response + .status(500) + .entity( + s""" + { + "success": false, + "message": "Failed to upload notebook to Jupyter (status $status)" + } + """ + ) + .build() + } + + jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb" + + Response + .ok( + s""" + { + "success": true, + "message": "Notebook successfully sent to Jupyter." + } + """ + ) + .build() + + } catch { + case NonFatal(e) => + logger.error("Error sending notebook to Jupyter", e) + Response + .status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(s"""{"error":"${e.getMessage}"}""") + .build() + } + } + + // Store notebook + mapping in database + def storeNotebookAndMapping(body: String): Response = { + try { + val json = mapper.readTree(body) + + val wid: java.lang.Integer = json.get("wid").asInt() + val vid: java.lang.Integer = json.get("vid").asInt() + val mappingNode = json.get("mapping") + val notebookNode = json.get("notebook") + + val dsl = SqlServer.getInstance().createDSLContext() + + val nid: java.lang.Integer = SqlServer.withTransaction(dsl) { ctx => + // Insert notebook + val notebookRecord = ctx + .insertInto(Notebook.NOTEBOOK) + .set(Notebook.NOTEBOOK.WID, wid) + .set(Notebook.NOTEBOOK.NOTEBOOK_, JSONB.valueOf(notebookNode.toString)) + .returning(Notebook.NOTEBOOK.NID) + .fetchOne() + + val nidInside: java.lang.Integer = notebookRecord.getValue(Notebook.NOTEBOOK.NID) + + // Insert workflow-notebook mapping + ctx + .insertInto(WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING) + .set(WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING.WID, wid) + .set(WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING.VID, vid) + .set(WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING.NID, nidInside) + .set( + WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING.MAPPING, + JSONB.valueOf(mappingNode.toString) + ) + .execute() + + nidInside + } + + Response + .ok( + s""" + { + "success": true, + "message": "Notebook and mapping successfully stored. wid: $wid, vid: $vid, nid: $nid" + } + """ + ) + .build() + + } catch { + case NonFatal(e) => + logger.error("Error storing mapping and workflow", e) + Response + .status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(s"""{"error":"${e.getMessage}"}""") + .build() + } + } + + // Fetch notebook + mapping + def fetchNotebookAndMapping(body: String): Response = { + try { + val json = mapper.readTree(body) + + val wid: java.lang.Integer = json.get("wid").asInt() + val vid: java.lang.Integer = json.get("vid").asInt() + + val dsl = SqlServer.getInstance().createDSLContext() + + // Fetch the most recent notebook (highest nid) for this workflow version + val result = dsl + .select( + Notebook.NOTEBOOK.NID, + Notebook.NOTEBOOK.NOTEBOOK_, + WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING.MAPPING + ) + .from(Notebook.NOTEBOOK) + .join(WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING) + .on(Notebook.NOTEBOOK.WID.eq(WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING.WID)) + .and(Notebook.NOTEBOOK.NID.eq(WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING.NID)) + .where(Notebook.NOTEBOOK.WID.eq(wid)) + .and(WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING.VID.eq(vid)) + .orderBy(Notebook.NOTEBOOK.NID.desc()) // most recent nid first + .limit(1) // only take the latest + .fetchOne() + + if (result == null) { + Response.ok("""{"exists": false}""").build() + } else { + val nid: Int = result.getValue(Notebook.NOTEBOOK.NID) + val notebookJson: String = + result.get(Notebook.NOTEBOOK.NOTEBOOK_).asInstanceOf[JSONB].data() + val mappingJson: String = result + .get(WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING.MAPPING) + .asInstanceOf[JSONB] + .data() + + Response + .ok( + s""" + { + "exists": true, + "notebook": $notebookJson, + "mapping": $mappingJson + } + """ + ) + .build() + } + + } catch { + case NonFatal(e) => + logger.error("Database error retrieving mapping", e) + Response + .status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(s"""{"error":"${e.getMessage}"}""") + .build() + } + } +} + +@Path("/notebook-migration") +@Produces(Array(MediaType.APPLICATION_JSON)) +@Consumes(Array(MediaType.APPLICATION_JSON)) +class NotebookMigrationResource extends LazyLogging { + + @GET + @Path("/get-jupyter-iframe-url") + def getJupyterIframeURL: Response = { + logger.info("Getting Jupyter iframe URL") + NotebookMigrationResource.getJupyterIframeURL() + } + + @GET + @Path("/get-jupyter-url") + def getJupyterURL: Response = { + logger.info("Getting Jupyter API URL") + NotebookMigrationResource.getJupyterURL() + } + + @POST + @Path("/set-notebook") + def setNotebook(body: String): Response = { + logger.info("Setting notebook, request body: " + body) + NotebookMigrationResource.setNotebook(body) + } + + @POST + @Path("/store-notebook-and-mapping") + def storeNotebookAndMapping(body: String): Response = { + logger.info("Storing notebook and mapping, request body: " + body) + NotebookMigrationResource.storeNotebookAndMapping(body) + } + + @POST + @Path("/fetch-notebook-and-mapping") + def fetchNotebookAndMapping(body: String): Response = { + logger.info("Fetching notebook and mapping, request body: " + body) + NotebookMigrationResource.fetchNotebookAndMapping(body) + } +} From baea89ab883434e1c6da9770b2f54180396f0e4f Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Wed, 27 May 2026 21:18:05 -0700 Subject: [PATCH 02/30] added test cases --- build.sbt | 1 + .../NotebookMigrationResourceSpec.scala | 204 ++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala diff --git a/build.sbt b/build.sbt index 4c4c2790104..27e02e1deeb 100644 --- a/build.sbt +++ b/build.sbt @@ -176,6 +176,7 @@ lazy val NotebookMigrationService = (project in file("notebook-migration-service "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.17.0" ) ) + .dependsOn(DAO % "test->test") // test scope dependency // root project definition lazy val TexeraProject = (project in file(".")) diff --git a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala new file mode 100644 index 00000000000..61544bf65d2 --- /dev/null +++ b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala @@ -0,0 +1,204 @@ +/* + * 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.service.resource + +import jakarta.ws.rs.core.Response +import org.apache.texera.dao.MockTexeraDB +import org.apache.texera.dao.jooq.generated.tables.Notebook.NOTEBOOK +import org.apache.texera.dao.jooq.generated.tables.Workflow.WORKFLOW +import org.apache.texera.dao.jooq.generated.tables.WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING +import org.apache.texera.dao.jooq.generated.tables.WorkflowVersion.WORKFLOW_VERSION +import org.apache.texera.dao.jooq.generated.tables.daos.{WorkflowDao, WorkflowVersionDao} +import org.apache.texera.dao.jooq.generated.tables.pojos.{Workflow, WorkflowVersion} +import org.jooq.JSONB +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} + +import java.sql.Timestamp +import java.util.UUID + +class NotebookMigrationResourceSpec + extends AnyFlatSpec + with Matchers + with BeforeAndAfterAll + with BeforeAndAfterEach + with MockTexeraDB { + + // Randomise the seeded wid so a parallel run of unrelated specs that happen + // to seed the same id wouldn't collide on the embedded postgres. + private val testWid = 9000 + scala.util.Random.nextInt(1000) + + private var workflowDao: WorkflowDao = _ + private var workflowVersionDao: WorkflowVersionDao = _ + private var seededVid: Integer = _ + + private val sampleNotebook = + """{"cells":[{"cell_type":"code","metadata":{},"source":"print(1)"}]}""" + private val sampleMapping = + """{"operator_to_cell":{},"cell_to_operator":{}}""" + + override protected def beforeAll(): Unit = initializeDBAndReplaceDSLContext() + override protected def afterAll(): Unit = shutdownDB() + + override protected def beforeEach(): Unit = { + val cfg = getDSLContext.configuration() + workflowDao = new WorkflowDao(cfg) + workflowVersionDao = new WorkflowVersionDao(cfg) + cleanup() + + val workflow = new Workflow + workflow.setWid(testWid) + workflow.setName(s"wf_${UUID.randomUUID().toString.substring(0, 8)}") + workflow.setContent("{}") + workflow.setDescription("") + workflow.setCreationTime(new Timestamp(System.currentTimeMillis())) + workflow.setLastModifiedTime(new Timestamp(System.currentTimeMillis())) + workflowDao.insert(workflow) + + val version = new WorkflowVersion + version.setWid(testWid) + version.setContent("{}") + version.setCreationTime(new Timestamp(System.currentTimeMillis())) + workflowVersionDao.insert(version) + seededVid = version.getVid + } + + override protected def afterEach(): Unit = cleanup() + + private def cleanup(): Unit = { + // notebook and workflow_notebook_mapping cascade on workflow/version delete, + // but explicit deletes here keep state observable across tests and avoid + // depending on cascade ordering. + getDSLContext.deleteFrom(WORKFLOW_NOTEBOOK_MAPPING).execute() + getDSLContext.deleteFrom(NOTEBOOK).execute() + getDSLContext + .deleteFrom(WORKFLOW_VERSION) + .where(WORKFLOW_VERSION.WID.eq(testWid)) + .execute() + getDSLContext.deleteFrom(WORKFLOW).where(WORKFLOW.WID.eq(testWid)).execute() + } + + private def storePayload( + notebook: String = sampleNotebook, + mapping: String = sampleMapping, + vid: Integer = seededVid + ): String = + s"""{"wid": $testWid, "vid": $vid, "notebook": $notebook, "mapping": $mapping}""" + + private def fetchPayload(vid: Integer = seededVid): String = + s"""{"wid": $testWid, "vid": $vid}""" + + // -- storeNotebookAndMapping ------------------------------------------------ + + "storeNotebookAndMapping" should "insert one notebook and one mapping tied to the workflow version" in { + val response = NotebookMigrationResource.storeNotebookAndMapping(storePayload()) + response.getStatus shouldBe Response.Status.OK.getStatusCode + + getDSLContext.fetchCount(NOTEBOOK) shouldBe 1 + getDSLContext.fetchCount(WORKFLOW_NOTEBOOK_MAPPING) shouldBe 1 + + val notebookRow = getDSLContext.selectFrom(NOTEBOOK).fetchOne() + notebookRow.get(NOTEBOOK.WID) shouldBe testWid + + val mappingRow = getDSLContext.selectFrom(WORKFLOW_NOTEBOOK_MAPPING).fetchOne() + mappingRow.get(WORKFLOW_NOTEBOOK_MAPPING.WID) shouldBe testWid + mappingRow.get(WORKFLOW_NOTEBOOK_MAPPING.VID) shouldBe seededVid + // The mapping row must reference the just-inserted notebook by its returned nid. + mappingRow.get(WORKFLOW_NOTEBOOK_MAPPING.NID) shouldBe notebookRow.get(NOTEBOOK.NID) + } + + it should "round-trip notebook and mapping JSON content through the JSONB columns" in { + val notebook = + """{"cells":[{"cell_type":"code","metadata":{"uuid":"abc-123"},"source":"x = 1"}]}""" + val mapping = + """{"operator_to_cell":{"op1":["cell1"]},"cell_to_operator":{"cell1":["op1"]}}""" + + NotebookMigrationResource.storeNotebookAndMapping(storePayload(notebook, mapping)) + + val storedNotebookJson = + getDSLContext.selectFrom(NOTEBOOK).fetchOne().get(NOTEBOOK.NOTEBOOK_).asInstanceOf[JSONB].data() + val storedMappingJson = + getDSLContext + .selectFrom(WORKFLOW_NOTEBOOK_MAPPING) + .fetchOne() + .get(WORKFLOW_NOTEBOOK_MAPPING.MAPPING) + .asInstanceOf[JSONB] + .data() + + // Use whitespace-agnostic substring checks — postgres canonicalises JSONB + // text on the way out, so exact-string compare against the input would be + // fragile across postgres versions. + storedNotebookJson should include("\"abc-123\"") + storedNotebookJson should include("x = 1") + storedMappingJson should include("\"op1\"") + storedMappingJson should include("\"cell1\"") + } + + it should "roll back the notebook insert when the mapping insert fails its FK constraint" in { + // workflow_notebook_mapping.vid has FK -> workflow_version(vid). Passing an + // unknown vid trips the mapping insert; because both inserts share a single + // SqlServer.withTransaction block, the notebook insert must roll back too. + // Without this guarantee, orphaned notebook rows would accumulate on every + // failed store. + val unknownVid: Integer = -1 + val response = NotebookMigrationResource.storeNotebookAndMapping( + storePayload(vid = unknownVid) + ) + response.getStatus shouldBe Response.Status.INTERNAL_SERVER_ERROR.getStatusCode + getDSLContext.fetchCount(NOTEBOOK) shouldBe 0 + getDSLContext.fetchCount(WORKFLOW_NOTEBOOK_MAPPING) shouldBe 0 + } + + // -- fetchNotebookAndMapping ------------------------------------------------ + + "fetchNotebookAndMapping" should "return exists=false when no notebook is stored for the (wid, vid)" in { + val response = NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()) + response.getStatus shouldBe Response.Status.OK.getStatusCode + response.getEntity.toString should include("\"exists\": false") + } + + it should "return exists=true with the stored notebook and mapping when a row exists" in { + NotebookMigrationResource.storeNotebookAndMapping(storePayload()) + + val entity = NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()).getEntity.toString + entity should include("\"exists\": true") + entity should include("\"notebook\":") + entity should include("\"mapping\":") + } + + it should "return the most recent notebook (highest nid) when multiple are stored for the same (wid, vid)" in { + // The endpoint uses orderBy(NID.desc).limit(1) to surface the latest + // notebook. Pin that ordering — a refactor that drops the limit or flips + // to ASC would silently regress workflow-reopen UX (the panel would + // restore from a stale notebook). + val first = + """{"cells":[{"cell_type":"code","metadata":{},"source":"v1"}]}""" + val second = + """{"cells":[{"cell_type":"code","metadata":{},"source":"v2"}]}""" + + NotebookMigrationResource.storeNotebookAndMapping(storePayload(first, sampleMapping)) + NotebookMigrationResource.storeNotebookAndMapping(storePayload(second, sampleMapping)) + + val entity = NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()).getEntity.toString + entity should include("\"v2\"") + entity should not include "\"v1\"" + } +} From 5e1cae47df8e5ff67afeed3b00095c18edf80f26 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Fri, 5 Jun 2026 15:48:47 -0700 Subject: [PATCH 03/30] fixed formatting for spec test file --- .../resource/NotebookMigrationResourceSpec.scala | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala index 61544bf65d2..246fb3230b9 100644 --- a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala +++ b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala @@ -134,7 +134,12 @@ class NotebookMigrationResourceSpec NotebookMigrationResource.storeNotebookAndMapping(storePayload(notebook, mapping)) val storedNotebookJson = - getDSLContext.selectFrom(NOTEBOOK).fetchOne().get(NOTEBOOK.NOTEBOOK_).asInstanceOf[JSONB].data() + getDSLContext + .selectFrom(NOTEBOOK) + .fetchOne() + .get(NOTEBOOK.NOTEBOOK_) + .asInstanceOf[JSONB] + .data() val storedMappingJson = getDSLContext .selectFrom(WORKFLOW_NOTEBOOK_MAPPING) @@ -178,7 +183,8 @@ class NotebookMigrationResourceSpec it should "return exists=true with the stored notebook and mapping when a row exists" in { NotebookMigrationResource.storeNotebookAndMapping(storePayload()) - val entity = NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()).getEntity.toString + val entity = + NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()).getEntity.toString entity should include("\"exists\": true") entity should include("\"notebook\":") entity should include("\"mapping\":") @@ -197,7 +203,8 @@ class NotebookMigrationResourceSpec NotebookMigrationResource.storeNotebookAndMapping(storePayload(first, sampleMapping)) NotebookMigrationResource.storeNotebookAndMapping(storePayload(second, sampleMapping)) - val entity = NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()).getEntity.toString + val entity = + NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()).getEntity.toString entity should include("\"v2\"") entity should not include "\"v1\"" } From af14652daa27056c5d7874d9f41576de2c264579 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 14:25:54 -0700 Subject: [PATCH 04/30] added weak default token for Jupyter auth --- common/config/src/main/resources/storage.conf | 4 ++++ .../org/apache/texera/common/config/StorageConfig.scala | 1 + .../service/resource/NotebookMigrationResource.scala | 8 ++++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/common/config/src/main/resources/storage.conf b/common/config/src/main/resources/storage.conf index c58c337e9a3..03c74b5aac6 100644 --- a/common/config/src/main/resources/storage.conf +++ b/common/config/src/main/resources/storage.conf @@ -156,5 +156,9 @@ storage { jupyter { url = "http://localhost:9100" url = ${?STORAGE_JUPYTER_URL} + # Must match JUPYTER_TOKEN in notebook-migration-service/src/main/resources/docker-compose.yml. + # The backend sends this token when loading the iframe and uploading notebooks. + token = "texera" + token = ${?STORAGE_JUPYTER_TOKEN} } } diff --git a/common/config/src/main/scala/org/apache/texera/common/config/StorageConfig.scala b/common/config/src/main/scala/org/apache/texera/common/config/StorageConfig.scala index 55a32c958fb..2ff39093c82 100644 --- a/common/config/src/main/scala/org/apache/texera/common/config/StorageConfig.scala +++ b/common/config/src/main/scala/org/apache/texera/common/config/StorageConfig.scala @@ -136,4 +136,5 @@ object StorageConfig { // Jupyter val jupyterURL: String = conf.getString("storage.jupyter.url") + val jupyterToken: String = conf.getString("storage.jupyter.token") } diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 6b06d883619..ac678c120d5 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -35,7 +35,9 @@ object NotebookMigrationResource extends LazyLogging { private val mapper: ObjectMapper = new ObjectMapper().registerModule(DefaultScalaModule) private val jupyterUrl = StorageConfig.jupyterURL - private var jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb" + private val jupyterToken = StorageConfig.jupyterToken + // The token is passed as a URL param so the browser iframe can authenticate when loading the notebook. + private var jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb?token=$jupyterToken" private def isJupyterAvailable(jupyterUrl: String): Boolean = { try { @@ -142,6 +144,8 @@ object NotebookMigrationResource extends LazyLogging { conn.setRequestMethod("PUT") conn.setDoOutput(true) conn.setRequestProperty("Content-Type", "application/json") + // The Jupyter Contents API requires authentication; send the configured token. + conn.setRequestProperty("Authorization", s"token $jupyterToken") val requestBody = s""" @@ -172,7 +176,7 @@ object NotebookMigrationResource extends LazyLogging { .build() } - jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb" + jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb?token=$jupyterToken" Response .ok( From 0502cfb99a14b8b5ed3a1f3c83689b0ec6d57b33 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 14:37:41 -0700 Subject: [PATCH 05/30] fix stale StorageConfig import after move to common.config package --- .../org/apache/texera/service/NotebookMigrationService.scala | 2 +- .../texera/service/resource/NotebookMigrationResource.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala index 9effd07436e..7ed1cea0b30 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala @@ -22,7 +22,7 @@ import com.typesafe.scalalogging.LazyLogging import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider} import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} -import org.apache.texera.amber.config.StorageConfig +import org.apache.texera.common.config.StorageConfig import org.apache.texera.auth.RequestLoggingFilter import org.apache.texera.dao.SqlServer import java.nio.file.Path diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index ac678c120d5..6008a11cc66 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -29,7 +29,7 @@ import org.apache.texera.dao.jooq.generated.tables.WorkflowNotebookMapping import java.net.{HttpURLConnection, URL} import java.nio.charset.StandardCharsets import scala.util.control.NonFatal -import org.apache.texera.amber.config.StorageConfig +import org.apache.texera.common.config.StorageConfig object NotebookMigrationResource extends LazyLogging { From 1ffef65d3c996ff2fdb305404b31dc74356ad8aa Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 15:03:00 -0700 Subject: [PATCH 06/30] disconnect HttpURLConnection in finally to avoid socket leak --- .../service/resource/NotebookMigrationResource.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 6008a11cc66..1513fa8f6cc 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -40,8 +40,9 @@ object NotebookMigrationResource extends LazyLogging { private var jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb?token=$jupyterToken" private def isJupyterAvailable(jupyterUrl: String): Boolean = { + var conn: java.net.HttpURLConnection = null try { - val conn = new java.net.URL(s"$jupyterUrl/api") + conn = new java.net.URL(s"$jupyterUrl/api") .openConnection() .asInstanceOf[java.net.HttpURLConnection] @@ -54,6 +55,8 @@ object NotebookMigrationResource extends LazyLogging { status == 200 || status == 403 } catch { case _: Exception => false + } finally { + if (conn != null) conn.disconnect() } } @@ -129,6 +132,7 @@ object NotebookMigrationResource extends LazyLogging { .build() } + var conn: HttpURLConnection = null try { val json = mapper.readTree(body) @@ -139,7 +143,7 @@ object NotebookMigrationResource extends LazyLogging { val apiUrl = s"$jupyterUrl/api/contents/work/$notebookName" val url = new URL(apiUrl) - val conn = url.openConnection().asInstanceOf[HttpURLConnection] + conn = url.openConnection().asInstanceOf[HttpURLConnection] conn.setRequestMethod("PUT") conn.setDoOutput(true) @@ -196,6 +200,8 @@ object NotebookMigrationResource extends LazyLogging { .status(Response.Status.INTERNAL_SERVER_ERROR) .entity(s"""{"error":"${e.getMessage}"}""") .build() + } finally { + if (conn != null) conn.disconnect() } } From cc80b1a21f22034c346212923a67c464161b98f3 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 15:06:52 -0700 Subject: [PATCH 07/30] derive jupyter iframe url from uploaded notebook name --- .../texera/service/resource/NotebookMigrationResource.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 1513fa8f6cc..1f63ac44d66 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -180,7 +180,7 @@ object NotebookMigrationResource extends LazyLogging { .build() } - jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb?token=$jupyterToken" + jupyterIframeURL = s"$jupyterUrl/notebooks/work/$notebookName?token=$jupyterToken" Response .ok( From 7468ab1323d09c57823b8ffb71b3418d9bd83141 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 15:12:44 -0700 Subject: [PATCH 08/30] document per-user-pod assumption for jupyterIframeURL global state --- .../texera/service/resource/NotebookMigrationResource.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 1f63ac44d66..2e8b61c2de0 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -37,6 +37,10 @@ object NotebookMigrationResource extends LazyLogging { private val jupyterUrl = StorageConfig.jupyterURL private val jupyterToken = StorageConfig.jupyterToken // The token is passed as a URL param so the browser iframe can authenticate when loading the notebook. + // jupyterIframeURL is process-global state. This is safe ONLY because each user runs their own pod + // (own notebook-migration-service JVM + own Jupyter) in the k8s deployment, so this singleton is + // effectively per-user. Do NOT deploy this service as a shared multi-user instance without adding + // per-user keying here, or one user's upload would overwrite another's iframe URL. private var jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb?token=$jupyterToken" private def isJupyterAvailable(jupyterUrl: String): Boolean = { From f0cf26172ff2f85923f0c0c4464e4d6d1fcba5f5 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 15:33:06 -0700 Subject: [PATCH 09/30] build error responses via mapper to avoid malformed JSON --- .../service/resource/NotebookMigrationResource.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 2e8b61c2de0..6e2fbd4087c 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -34,6 +34,12 @@ import org.apache.texera.common.config.StorageConfig object NotebookMigrationResource extends LazyLogging { private val mapper: ObjectMapper = new ObjectMapper().registerModule(DefaultScalaModule) + + // Build an error response body via the mapper so the message is JSON-escaped; interpolating + // e.getMessage directly produces malformed JSON when it contains quotes, backslashes, or newlines. + private def errorJson(message: String): String = + mapper.writeValueAsString(mapper.createObjectNode().put("error", message)) + private val jupyterUrl = StorageConfig.jupyterURL private val jupyterToken = StorageConfig.jupyterToken // The token is passed as a URL param so the browser iframe can authenticate when loading the notebook. @@ -202,7 +208,7 @@ object NotebookMigrationResource extends LazyLogging { logger.error("Error sending notebook to Jupyter", e) Response .status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(s"""{"error":"${e.getMessage}"}""") + .entity(errorJson(e.getMessage)) .build() } finally { if (conn != null) conn.disconnect() @@ -263,7 +269,7 @@ object NotebookMigrationResource extends LazyLogging { logger.error("Error storing mapping and workflow", e) Response .status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(s"""{"error":"${e.getMessage}"}""") + .entity(errorJson(e.getMessage)) .build() } } @@ -324,7 +330,7 @@ object NotebookMigrationResource extends LazyLogging { logger.error("Database error retrieving mapping", e) Response .status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(s"""{"error":"${e.getMessage}"}""") + .entity(errorJson(e.getMessage)) .build() } } From c4df430098ffcb617b75ba7171b5a209fe2ec231 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 15:36:28 -0700 Subject: [PATCH 10/30] stop logging full request bodies in notebook-migration endpoints --- .../texera/service/resource/NotebookMigrationResource.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 6e2fbd4087c..624af2b15a3 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -358,21 +358,21 @@ class NotebookMigrationResource extends LazyLogging { @POST @Path("/set-notebook") def setNotebook(body: String): Response = { - logger.info("Setting notebook, request body: " + body) + logger.info("Setting notebook") NotebookMigrationResource.setNotebook(body) } @POST @Path("/store-notebook-and-mapping") def storeNotebookAndMapping(body: String): Response = { - logger.info("Storing notebook and mapping, request body: " + body) + logger.info("Storing notebook and mapping") NotebookMigrationResource.storeNotebookAndMapping(body) } @POST @Path("/fetch-notebook-and-mapping") def fetchNotebookAndMapping(body: String): Response = { - logger.info("Fetching notebook and mapping, request body: " + body) + logger.info("Fetching notebook and mapping") NotebookMigrationResource.fetchNotebookAndMapping(body) } } From 391e2bbb23a9749169b29ba4dd7c99f7074a59e9 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 15:49:12 -0700 Subject: [PATCH 11/30] fix notebook fetch test to respect unique wid constraint --- .../NotebookMigrationResourceSpec.scala | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala index 246fb3230b9..6f805ec1949 100644 --- a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala +++ b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala @@ -190,22 +190,17 @@ class NotebookMigrationResourceSpec entity should include("\"mapping\":") } - it should "return the most recent notebook (highest nid) when multiple are stored for the same (wid, vid)" in { - // The endpoint uses orderBy(NID.desc).limit(1) to surface the latest - // notebook. Pin that ordering — a refactor that drops the limit or flips - // to ASC would silently regress workflow-reopen UX (the panel would - // restore from a stale notebook). - val first = + it should "return the stored notebook content for a (wid, vid) on fetch" in { + // notebook.wid is UNIQUE — one notebook per workflow — so the endpoint's + // orderBy(NID.desc).limit(1) resolves to that single row. This pins the + // workflow-reopen path: after a store, fetch must return that notebook's content. + val notebook = """{"cells":[{"cell_type":"code","metadata":{},"source":"v1"}]}""" - val second = - """{"cells":[{"cell_type":"code","metadata":{},"source":"v2"}]}""" - NotebookMigrationResource.storeNotebookAndMapping(storePayload(first, sampleMapping)) - NotebookMigrationResource.storeNotebookAndMapping(storePayload(second, sampleMapping)) + NotebookMigrationResource.storeNotebookAndMapping(storePayload(notebook, sampleMapping)) val entity = NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()).getEntity.toString - entity should include("\"v2\"") - entity should not include "\"v1\"" + entity should include("\"v1\"") } } From c05c35b004580649f3f97fdd5dbde8ff60c78a6e Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Wed, 17 Jun 2026 13:18:15 -0700 Subject: [PATCH 12/30] remove redundant logback.xml in favor of dropwizard yaml logging --- .../src/main/resources/logback.xml | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 notebook-migration-service/src/main/resources/logback.xml diff --git a/notebook-migration-service/src/main/resources/logback.xml b/notebook-migration-service/src/main/resources/logback.xml deleted file mode 100644 index ffcc406a752..00000000000 --- a/notebook-migration-service/src/main/resources/logback.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - [%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n - - - - - - - logs/notebook-migration-service.log - true - - logs/notebook-migration-service-%d{yyyy-MM-dd}.log.gz - - - [%date{ISO8601}] [%level] [%logger] [%thread] - %msg %n - - - - - 8192 - true - - - - - - - - - - - \ No newline at end of file From ce69dfd8b5f1dafecdd59356e8ef010f65f0a4c8 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Wed, 17 Jun 2026 13:23:08 -0700 Subject: [PATCH 13/30] remove "Default values..." comment from jupyter config --- common/config/src/main/resources/storage.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/common/config/src/main/resources/storage.conf b/common/config/src/main/resources/storage.conf index 03c74b5aac6..ac7d1f5a71a 100644 --- a/common/config/src/main/resources/storage.conf +++ b/common/config/src/main/resources/storage.conf @@ -152,7 +152,6 @@ storage { } # Configurations of the JupyterLab service - # Default values are provided for each field, which you don't need to change if you deployed Jupyter via docker-compose.yml in notebook-migration-service/src/main/resources/docker-compose.yml jupyter { url = "http://localhost:9100" url = ${?STORAGE_JUPYTER_URL} From 733dcfcee3dabb1bf318e8b1030519cdfee6baad Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Wed, 17 Jun 2026 13:39:15 -0700 Subject: [PATCH 14/30] read jupyter token from shared JUPYTER_TOKEN env var --- common/config/src/main/resources/storage.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/config/src/main/resources/storage.conf b/common/config/src/main/resources/storage.conf index ac7d1f5a71a..81e13a777f0 100644 --- a/common/config/src/main/resources/storage.conf +++ b/common/config/src/main/resources/storage.conf @@ -155,9 +155,9 @@ storage { jupyter { url = "http://localhost:9100" url = ${?STORAGE_JUPYTER_URL} - # Must match JUPYTER_TOKEN in notebook-migration-service/src/main/resources/docker-compose.yml. - # The backend sends this token when loading the iframe and uploading notebooks. + # Read from the same JUPYTER_TOKEN env var as the Jupyter container + # (notebook-migration-service/src/main/resources/docker-compose.yml) token = "texera" - token = ${?STORAGE_JUPYTER_TOKEN} + token = ${?JUPYTER_TOKEN} } } From 6f69e919632cdf219501700a7de7da0766a1eb3d Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Wed, 17 Jun 2026 13:48:01 -0700 Subject: [PATCH 15/30] use shared jacksonVersion for notebook-migration-service override --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 2d8dc5c9d72..77525f9e472 100644 --- a/build.sbt +++ b/build.sbt @@ -175,7 +175,7 @@ lazy val NotebookMigrationService = (project in file("notebook-migration-service .settings( dependencyOverrides ++= Seq( // override it as io.dropwizard 4 require 2.16.1 or higher - "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.17.0" + "com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion ) ) .dependsOn(DAO % "test->test") // test scope dependency From dc085344dc15d773c38c0e094c3c55c53479e835 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Wed, 17 Jun 2026 14:10:48 -0700 Subject: [PATCH 16/30] return 409 when storing a second notebook for a workflow --- .../resource/NotebookMigrationResource.scala | 13 +++++++++++++ .../resource/NotebookMigrationResourceSpec.scala | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 624af2b15a3..66df19c4a9a 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -227,6 +227,19 @@ object NotebookMigrationResource extends LazyLogging { val dsl = SqlServer.getInstance().createDSLContext() + // notebook.wid is UNIQUE: a workflow has at most one notebook. If one already + // exists, reject the re-store with a 409 rather than letting the INSERT trip the + // constraint and surface as a 500. + val alreadyStored = dsl.fetchExists( + dsl.selectFrom(Notebook.NOTEBOOK).where(Notebook.NOTEBOOK.WID.eq(wid)) + ) + if (alreadyStored) { + return Response + .status(Response.Status.CONFLICT) + .entity(errorJson(s"A notebook is already stored for workflow $wid")) + .build() + } + val nid: java.lang.Integer = SqlServer.withTransaction(dsl) { ctx => // Insert notebook val notebookRecord = ctx diff --git a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala index 6f805ec1949..222e0bb1781 100644 --- a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala +++ b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala @@ -172,6 +172,20 @@ class NotebookMigrationResourceSpec getDSLContext.fetchCount(WORKFLOW_NOTEBOOK_MAPPING) shouldBe 0 } + it should "return 409 Conflict on a second store for a workflow that already has a notebook" in { + // notebook.wid is UNIQUE — one notebook per workflow. The second store must be + // rejected with an explicit 409 (not a 500 from the constraint violation), and + // must not add a second notebook or mapping row. + val first = NotebookMigrationResource.storeNotebookAndMapping(storePayload()) + first.getStatus shouldBe Response.Status.OK.getStatusCode + + val second = NotebookMigrationResource.storeNotebookAndMapping(storePayload()) + second.getStatus shouldBe Response.Status.CONFLICT.getStatusCode + + getDSLContext.fetchCount(NOTEBOOK) shouldBe 1 + getDSLContext.fetchCount(WORKFLOW_NOTEBOOK_MAPPING) shouldBe 1 + } + // -- fetchNotebookAndMapping ------------------------------------------------ "fetchNotebookAndMapping" should "return exists=false when no notebook is stored for the (wid, vid)" in { From 01b731bedc4990bf503ecafa86d6bc0c716f1b83 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Wed, 17 Jun 2026 14:51:19 -0700 Subject: [PATCH 17/30] ship binary license files in notebook-migration-service dist --- notebook-migration-service/LICENSE-binary | 400 ++++++ notebook-migration-service/NOTICE-binary | 1419 +++++++++++++++++++++ notebook-migration-service/build.sbt | 10 + 3 files changed, 1829 insertions(+) create mode 100644 notebook-migration-service/LICENSE-binary create mode 100644 notebook-migration-service/NOTICE-binary diff --git a/notebook-migration-service/LICENSE-binary b/notebook-migration-service/LICENSE-binary new file mode 100644 index 00000000000..3abc86ea44c --- /dev/null +++ b/notebook-migration-service/LICENSE-binary @@ -0,0 +1,400 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for describing the origin of the Work and + reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Support. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or support. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + +================================================================================ +THIRD-PARTY COMPONENTS +================================================================================ + +Apache Texera's binary distribution of this service includes the following third-party components, grouped by license. Each section references licenses/ for the full text of the applicable license. Components under the Apache License, Version 2.0 are governed by the same license terms as Apache Texera itself and are listed for completeness. + +Locations within the distribution: + + - Scala/Java jars listed below ship under the lib/ directory of + this service's Universal zip. + + - Source files derived from third-party projects are compiled into + the bundled jars under lib/ and live at the listed paths in the + Apache Texera source tree. + +-------------------------------------------------------------------------------- +Dependencies under the Apache License, Version 2.0 +-------------------------------------------------------------------------------- + +Scala/Java jars: + - com.fasterxml.classmate-1.7.0.jar + - com.fasterxml.jackson.core.jackson-annotations-2.18.6.jar + - com.fasterxml.jackson.core.jackson-core-2.18.6.jar + - com.fasterxml.jackson.core.jackson-databind-2.18.6.jar + - com.fasterxml.jackson.dataformat.jackson-dataformat-yaml-2.16.1.jar + - com.fasterxml.jackson.datatype.jackson-datatype-guava-2.16.1.jar + - com.fasterxml.jackson.datatype.jackson-datatype-jdk8-2.16.1.jar + - com.fasterxml.jackson.datatype.jackson-datatype-jsr310-2.16.1.jar + - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-base-2.16.1.jar + - com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-json-provider-2.16.1.jar + - com.fasterxml.jackson.module.jackson-module-blackbird-2.16.1.jar + - com.fasterxml.jackson.module.jackson-module-jakarta-xmlbind-annotations-2.16.1.jar + - com.fasterxml.jackson.module.jackson-module-parameter-names-2.16.1.jar + - com.fasterxml.jackson.module.jackson-module-scala_2.13-2.18.6.jar + - com.github.ben-manes.caffeine.caffeine-3.1.8.jar + - com.google.code.findbugs.jsr305-3.0.2.jar + - com.google.errorprone.error_prone_annotations-2.25.0.jar + - com.google.guava.failureaccess-1.0.2.jar + - com.google.guava.guava-33.0.0-jre.jar + - com.google.guava.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar + - com.google.j2objc.j2objc-annotations-2.8.jar + - com.helger.profiler-1.1.1.jar + - com.thesamet.scalapb.lenses_2.13-0.11.20.jar + - com.thesamet.scalapb.scalapb-json4s_2.13-0.12.0.jar + - com.thesamet.scalapb.scalapb-runtime_2.13-0.11.20.jar + - com.typesafe.config-1.4.6.jar + - com.typesafe.scala-logging.scala-logging_2.13-3.9.5.jar + - com.zaxxer.HikariCP-5.1.0.jar + - io.dropwizard.dropwizard-auth-4.0.7.jar + - io.dropwizard.dropwizard-configuration-4.0.7.jar + - io.dropwizard.dropwizard-core-4.0.7.jar + - io.dropwizard.dropwizard-health-4.0.7.jar + - io.dropwizard.dropwizard-jackson-4.0.7.jar + - io.dropwizard.dropwizard-jersey-4.0.7.jar + - io.dropwizard.dropwizard-jetty-4.0.7.jar + - io.dropwizard.dropwizard-lifecycle-4.0.7.jar + - io.dropwizard.dropwizard-logging-4.0.7.jar + - io.dropwizard.dropwizard-metrics-4.0.7.jar + - io.dropwizard.dropwizard-request-logging-4.0.7.jar + - io.dropwizard.dropwizard-servlets-4.0.7.jar + - io.dropwizard.dropwizard-util-4.0.7.jar + - io.dropwizard.dropwizard-validation-4.0.7.jar + - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar + - io.dropwizard.metrics.metrics-annotation-4.2.25.jar + - io.dropwizard.metrics.metrics-caffeine-4.2.25.jar + - io.dropwizard.metrics.metrics-core-4.2.25.jar + - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar + - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar + - io.dropwizard.metrics.metrics-jersey3-4.2.25.jar + - io.dropwizard.metrics.metrics-jetty11-4.2.25.jar + - io.dropwizard.metrics.metrics-jmx-4.2.25.jar + - io.dropwizard.metrics.metrics-json-4.2.25.jar + - io.dropwizard.metrics.metrics-jvm-4.2.25.jar + - io.dropwizard.metrics.metrics-logback-4.2.25.jar + - io.r2dbc.r2dbc-spi-0.9.0.RELEASE.jar + - jakarta.inject.jakarta.inject-api-2.0.1.jar + - jakarta.validation.jakarta.validation-api-3.0.2.jar + - org.apache.commons.commons-lang3-3.13.0.jar + - org.apache.commons.commons-text-1.11.0.jar + - org.bitbucket.b_c.jose4j-0.9.6.jar + - org.eclipse.jetty.jetty-http-11.0.20.jar + - org.eclipse.jetty.jetty-io-11.0.20.jar + - org.eclipse.jetty.jetty-security-11.0.20.jar + - org.eclipse.jetty.jetty-server-11.0.20.jar + - org.eclipse.jetty.jetty-servlet-11.0.20.jar + - org.eclipse.jetty.jetty-servlets-11.0.20.jar + - org.eclipse.jetty.jetty-util-11.0.20.jar + - org.eclipse.jetty.toolchain.jetty-jakarta-servlet-api-5.0.2.jar + - org.eclipse.jetty.toolchain.setuid.jetty-setuid-java-1.0.4.jar + - org.hibernate.validator.hibernate-validator-7.0.5.Final.jar + - org.javassist.javassist-3.30.2-GA.jar + - org.jboss.logging.jboss-logging-3.5.3.Final.jar + - org.jooq.jooq-3.16.23.jar + - org.json4s.json4s-ast_2.13-4.0.1.jar + - org.json4s.json4s-jackson-core_2.13-4.0.1.jar + - org.scala-lang.modules.scala-collection-compat_2.13-2.13.0.jar + - org.scala-lang.scala-library-2.13.18.jar + - org.scala-lang.scala-reflect-2.13.18.jar + - org.slf4j.jcl-over-slf4j-2.0.12.jar + - org.slf4j.log4j-over-slf4j-2.0.12.jar + - org.yaml.snakeyaml-2.2.jar + +-------------------------------------------------------------------------------- +Dependencies under the MIT License +-------------------------------------------------------------------------------- + +Source files derived from third-party MIT-licensed projects: + - mbknor-jackson-jsonschema + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaDraft.java + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/JsonSchemaGenerator.scala + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaArrayWithUniqueItems.java + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaBool.java + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDefault.java + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaDescription.java + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaExamples.java + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaFormat.java + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInject.java + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaInt.java + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaOptions.java + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaString.java + common/workflow-operator/src/main/scala/com/kjetland/jackson/jsonSchema/annotations/JsonSchemaTitle.java + https://github.com/mbknor/mbknor-jackson-jsonschema + +Scala/Java jars: + - net.sourceforge.argparse4j.argparse4j-0.9.0.jar + - org.checkerframework.checker-qual-3.52.0.jar + - org.slf4j.jul-to-slf4j-2.0.12.jar + - org.slf4j.slf4j-api-2.0.12.jar + +-------------------------------------------------------------------------------- +Dependencies under the BSD 3-Clause License +-------------------------------------------------------------------------------- + +Scala/Java jars: + - com.google.protobuf.protobuf-java-3.25.8.jar + - com.thoughtworks.paranamer.paranamer-2.8.jar + +-------------------------------------------------------------------------------- +Dependencies under the BSD 2-Clause License +-------------------------------------------------------------------------------- + +Scala/Java jars: + - org.postgresql.postgresql-42.7.10.jar + +-------------------------------------------------------------------------------- +Dependencies under the Eclipse Public License, Version 2.0 (some are dual +licensed with GPL-2.0 with Classpath Exception) +-------------------------------------------------------------------------------- + +Scala/Java jars: + - jakarta.annotation.jakarta.annotation-api-2.1.1.jar + - jakarta.el.jakarta.el-api-4.0.0.jar + - jakarta.servlet.jakarta.servlet-api-5.0.0.jar + - jakarta.ws.rs.jakarta.ws.rs-api-3.0.0.jar + - org.glassfish.hk2.external.aopalliance-repackaged-3.0.6.jar + - org.glassfish.hk2.hk2-api-3.0.6.jar + - org.glassfish.hk2.hk2-locator-3.0.3.jar + - org.glassfish.hk2.hk2-utils-3.0.6.jar + - org.glassfish.hk2.osgi-resource-locator-1.0.3.jar + - org.glassfish.jakarta.el-4.0.2.jar + - org.glassfish.jersey.containers.jersey-container-servlet-3.0.12.jar + - org.glassfish.jersey.containers.jersey-container-servlet-core-3.0.12.jar + - org.glassfish.jersey.core.jersey-client-3.0.12.jar + - org.glassfish.jersey.core.jersey-common-3.0.12.jar + - org.glassfish.jersey.core.jersey-server-3.0.12.jar + - org.glassfish.jersey.ext.jersey-bean-validation-3.0.12.jar + - org.glassfish.jersey.ext.jersey-metainf-services-3.0.12.jar + - org.glassfish.jersey.inject.jersey-hk2-3.0.12.jar + +-------------------------------------------------------------------------------- +Dependencies under the Eclipse Public License, Version 1.0 (Logback is dual +licensed with LGPL-2.1) +-------------------------------------------------------------------------------- + +Scala/Java jars: + - ch.qos.logback.logback-access-1.4.14.jar + - ch.qos.logback.logback-classic-1.4.14.jar + - ch.qos.logback.logback-core-1.4.14.jar + +-------------------------------------------------------------------------------- +Dependencies under the Eclipse Distribution License, Version 1.0 +-------------------------------------------------------------------------------- + +Scala/Java jars: + - com.sun.activation.jakarta.activation-2.0.1.jar + - jakarta.activation.jakarta.activation-api-2.1.0.jar + - jakarta.xml.bind.jakarta.xml.bind-api-3.0.1.jar + +-------------------------------------------------------------------------------- +Dependencies in the Public Domain (CC0) +-------------------------------------------------------------------------------- + +Scala/Java jars: + - org.reactivestreams.reactive-streams-1.0.3.jar + +Individual jars may contain their own META-INF/LICENSE and META-INF/NOTICE +files that apply to their specific contents; those files continue to govern +the use of those components. diff --git a/notebook-migration-service/NOTICE-binary b/notebook-migration-service/NOTICE-binary new file mode 100644 index 00000000000..c194ff42fb3 --- /dev/null +++ b/notebook-migration-service/NOTICE-binary @@ -0,0 +1,1419 @@ +Apache Texera (Incubating) +Copyright 2025 and onwards The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +-------------------------------------------------------------------------------- +org.eclipse.jetty +-------------------------------------------------------------------------------- + +Notices for Eclipse Jetty +========================= +This content is produced and maintained by the Eclipse Jetty project. + +Project home: https://eclipse.dev/jetty/ + +Trademarks +---------- +Eclipse Jetty, and Jetty are trademarks of the Eclipse Foundation. + +Copyright +--------- +All contributions are the property of the respective authors or of +entities to which copyright has been assigned by the authors (eg. employer). + +Declared Project Licenses +------------------------- +This artifacts of this project are made available under the terms of: + + * the Eclipse Public License v2.0 + https://www.eclipse.org/legal/epl-2.0 + SPDX-License-Identifier: EPL-2.0 + + or + + * the Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + SPDX-License-Identifier: Apache-2.0 + +The following dependencies are EPL. + * org.eclipse.jetty.orbit:org.eclipse.jdt.core + +The following dependencies are EPL and ASL2. + * org.eclipse.jetty.orbit:javax.security.auth.message + +The following dependencies are EPL and CDDL 1.0. + * org.eclipse.jetty.orbit:javax.mail.glassfish + +The following dependencies are CDDL + GPLv2 with classpath exception. +https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html + + * jakarta.servlet:jakarta.servlet-api + * javax.annotation:javax.annotation-api + * javax.transaction:javax.transaction-api + * javax.websocket:javax.websocket-api + +The following dependencies are licensed by the OW2 Foundation according to the +terms of http://asm.ow2.org/license.html + + * org.ow2.asm:asm-commons + * org.ow2.asm:asm + +The following dependencies are ASL2 licensed. + + * org.apache.taglibs:taglibs-standard-spec + * org.apache.taglibs:taglibs-standard-impl + +The following dependencies are ASL2 licensed. Based on selected classes from +following Apache Tomcat jars, all ASL2 licensed. + + * org.mortbay.jasper:apache-jsp + * org.apache.tomcat:tomcat-jasper + * org.apache.tomcat:tomcat-juli + * org.apache.tomcat:tomcat-jsp-api + * org.apache.tomcat:tomcat-el-api + * org.apache.tomcat:tomcat-jasper-el + * org.apache.tomcat:tomcat-api + * org.apache.tomcat:tomcat-util-scan + * org.apache.tomcat:tomcat-util + * org.mortbay.jasper:apache-el + * org.apache.tomcat:tomcat-jasper-el + * org.apache.tomcat:tomcat-el-api + +The following artifacts are CDDL + GPLv2 with classpath exception. +https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html + + * org.eclipse.jetty.toolchain:jetty-schemas + +Cryptography +------------ +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. + +The UnixCrypt.java code implements the one way cryptography used by +Unix systems for simple password protection. Copyright 1996 Aki Yoshida, +modified April 2001 by Iris Van den Broeke, Daniel Deville. +Permission to use, copy, modify and distribute UnixCrypt +for non-commercial or commercial purposes and without fee is +granted provided that the copyright notice appears in all copies. + +-------------------------------------------------------------------------------- +org.glassfish.jersey +-------------------------------------------------------------------------------- + +# Notice for Jersey +This content is produced and maintained by the Eclipse Jersey project. + +* Project home: https://projects.eclipse.org/projects/ee4j.jersey + +## Trademarks +Eclipse Jersey is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jersey + +## Third-party Content + +Angular JS, v1.6.6 +* License MIT (http://www.opensource.org/licenses/mit-license.php) +* Project: http://angularjs.org +* Coyright: (c) 2010-2017 Google, Inc. + +aopalliance Version 1 +* License: all the source code provided by AOP Alliance is Public Domain. +* Project: http://aopalliance.sourceforge.net +* Copyright: Material in the public domain is not protected by copyright + +Bean Validation API 3.0.2 +* License: Apache License, 2.0 +* Project: http://beanvalidation.org/1.1/ +* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors +* by the @authors tag. + +Hibernate Validator CDI, 7.0.5.Final +* License: Apache License, 2.0 +* Project: https://beanvalidation.org/ +* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate + +Bootstrap v3.3.7 +* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) +* Project: http://getbootstrap.com +* Copyright: 2011-2016 Twitter, Inc + +Google Guava Version 18.0 +* License: Apache License, 2.0 +* Copyright (C) 2009 The Guava Authors + +jakarta.inject Version: 1 +* License: Apache License, 2.0 +* Copyright (C) 2009 The JSR-330 Expert Group + +Javassist Version 3.29.2-GA +* License: Apache License, 2.0 +* Project: http://www.javassist.org/ +* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. + +Jackson JAX-RS Providers Version 2.15.3 +* License: Apache License, 2.0 +* Project: https://github.com/FasterXML/jackson-jaxrs-providers +* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. + +jQuery v1.12.4 +* License: jquery.org/license +* Project: jquery.org +* Copyright: (c) jQuery Foundation + +jQuery Barcode plugin 0.3 +* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) +* Project: http://www.pasella.it/projects/jQuery/barcode +* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com + +JSR-166 Extension - JEP 266 +* License: CC0 +* No copyright +* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ + +KineticJS, v4.7.1 +* License: MIT license (http://www.opensource.org/licenses/mit-license.php) +* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS +* Copyright: Eric Rowell + +org.objectweb.asm Version 9.6 +* License: Modified BSD (https://asm.ow2.io/license.html) +* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. + +org.osgi.core version 6.0.0 +* License: Apache License, 2.0 +* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. + +org.glassfish.jersey.server.internal.monitoring.core +* License: Apache License, 2.0 +* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. +* Copyright 2010-2013 Coda Hale and Yammer, Inc. + +W3.org documents +* License: W3C License +* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ + +-------------------------------------------------------------------------------- +org.glassfish.hk2 +-------------------------------------------------------------------------------- + +# Notices for Eclipse GlassFish + +This content is produced and maintained by the Eclipse GlassFish project. + +* Project home: https://projects.eclipse.org/projects/ee4j.glassfish + +## Trademarks + +Eclipse GlassFish, and GlassFish are trademarks of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code + +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/glassfish-ha-api +* https://github.com/eclipse-ee4j/glassfish-logging-annotation-processor +* https://github.com/eclipse-ee4j/glassfish-shoal +* https://github.com/eclipse-ee4j/glassfish-cdi-porting-tck +* https://github.com/eclipse-ee4j/glassfish-jsftemplating +* https://github.com/eclipse-ee4j/glassfish-hk2-extra +* https://github.com/eclipse-ee4j/glassfish-hk2 +* https://github.com/eclipse-ee4j/glassfish-fighterfish + +## Third-party Content + +This project leverages the following third party content. + +None + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. + +-------------------------------------------------------------------------------- +com.fasterxml.jackson +-------------------------------------------------------------------------------- + +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Licensing + +Jackson components are licensed under Apache (Software) License, version 2.0, +as per accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +-------------------------------------------------------------------------------- +jakarta.el.jakarta.el-api +-------------------------------------------------------------------------------- + +# Notices for Jakarta Expression Language + +This content is produced and maintained by the Jakarta Expression Language project. + +* Project home: https://projects.eclipse.org/projects/ee4j.el + +## Trademarks + +Jakarta Expression Language is a trademark of the Eclipse +Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code + +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/el-ri + +## Third-party Content + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. + +-------------------------------------------------------------------------------- +com.sun.activation.jakarta.activation +-------------------------------------------------------------------------------- + +# Notices for Jakarta Activation + +This content is produced and maintained by Jakarta Activation project. + +* Project home: https://projects.eclipse.org/projects/ee4j.jaf + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Distribution License v. 1.0, +which is available at http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: BSD-3-Clause + +## Source Code + +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jaf + +-------------------------------------------------------------------------------- +com.fasterxml.jackson.core +-------------------------------------------------------------------------------- + +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + +## Licensing + +Jackson 2.x core and extension components are licensed under Apache License 2.0 +To find the details that apply to this artifact see the accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS(-2.x) file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +-------------------------------------------------------------------------------- +org.scala-lang +-------------------------------------------------------------------------------- + +Scala +Copyright (c) 2002-2025 EPFL +Copyright (c) 2011-2025 Lightbend, Inc. dba Akka + +Scala includes software developed at +LAMP/EPFL (https://lamp.epfl.ch/) and +Akka (https://akka.io/). + +Licensed under the Apache License, Version 2.0 (the "License"). +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. + +This software includes projects with other licenses -- see `doc/LICENSE.md`. + +-------------------------------------------------------------------------------- +com.fasterxml.jackson.core.jackson-core +-------------------------------------------------------------------------------- + +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + +## Licensing + +Jackson 2.x core and extension components are licensed under Apache License 2.0 +To find the details that apply to this artifact see the accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS(-2.x) file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +## FastDoubleParser + +jackson-core bundles a shaded copy of FastDoubleParser . +That code is available under an MIT license +under the following copyright. + +Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. + +See FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser +and the licenses and copyrights that apply to that code. + +# FastDoubleParser + +This is a Java port of Daniel Lemire's fast_float project. +This project provides parsers for double, float, BigDecimal and BigInteger values. + +## Copyright + +Copyright © 2024 Werner Randelshofer, Switzerland. + +## Licensing + +This code is licensed under MIT License. +https://github.com/wrandelshofer/FastDoubleParser/blob/522be16e145f43308c43b23094e31d5efcaa580e/LICENSE +(The file 'LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +Some portions of the code have been derived from other projects. +All these projects require that we include a copyright notice, and some require that we also include some text of their +license file. + +fast_double_parser, Copyright (c) 2022 Daniel Lemire. BSL License. +https://github.com/lemire/fast_double_parser +https://github.com/lemire/fast_double_parser/blob/07d9189a8fb815fe800cb15ca022e7a07093236e/LICENSE.BSL +(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +fast_float, Copyright (c) 2021 The fast_float authors. MIT License. +https://github.com/fastfloat/fast_float +https://github.com/fastfloat/fast_float/blob/cc1e01e9eee74128e48d51488a6b1df4a767a810/LICENSE-MIT +(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +bigint, Copyright 2020 Tim Buktu. 2-clause BSD License. +https://github.com/tbuktu/bigint/tree/floatfft +https://github.com/tbuktu/bigint/blob/617c8cd8a7c5e4fb4d919c6a4d11e2586107f029/LICENSE +https://github.com/wrandelshofer/FastDoubleParser/blob/39e123b15b71f29a38a087d16a0bc620fc879aa6/bigint-LICENSE +(We only use those portions of the bigint project that can be licensed under 2-clause BSD License.) +(The file 'thirdparty-LICENSE' is included in the sources and classes Jar files that are released by this project +- as is required by that license.) + +-------------------------------------------------------------------------------- +org.apache.commons.commons-lang3 +-------------------------------------------------------------------------------- + +Apache Commons Lang +Copyright 2001-2023 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). + +-------------------------------------------------------------------------------- +org.eclipse.jetty.toolchain.jetty-jakarta-servlet-api +-------------------------------------------------------------------------------- + +# Notices for Eclipse Project for Servlet + +This content is produced and maintained by the Eclipse Project for Servlet +project. + +* Project home: https://projects.eclipse.org/projects/ee4j.servlet + + +## Trademarks + +Eclipse Project for Servlet is a trademark of the Eclipse Foundation. + + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + + +## Source Code + +The project maintains the following source code repositories: + + * https://github.com/eclipse-ee4j/servlet-api + * https://github.com/eclipse/jetty.toolchain + + +## Third-party Content + +## Jakarta + +The following artifacts are EPL 2.0 + GPLv2 with classpath exception. +https://projects.eclipse.org/projects/ee4j.servlet + + * jakarta.servlet:jakarta.servlet-api + + +## GlassFish + +The following artifacts are CDDL + GPLv2 with classpath exception. +https://glassfish.dev.java.net/nonav/public/CDDL+GPL.html + + * org.eclipse.jetty.toolchain:jetty-schemas + +-------------------------------------------------------------------------------- +org.glassfish.jersey.core.jersey-server +-------------------------------------------------------------------------------- + +# Notice for Jersey Core Server module +This content is produced and maintained by the Eclipse Jersey project. + +* https://projects.eclipse.org/projects/ee4j.jersey + +## Trademarks +Eclipse Jersey is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jersey + +## Third-party Content + +org.glassfish.jersey.server.internal.monitoring.core +* License: Apache License, 2.0 +* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. +* Copyright 2010-2013 Coda Hale and Yammer, Inc. + +org.objectweb.asm Version 9.6 +* License: Modified BSD (https://asm.ow2.io/license.html) +* Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved. + +W3.org documents +* License: W3C License +* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ + +# Notice for Jersey +This content is produced and maintained by the Eclipse Jersey project. + +* Project home: https://projects.eclipse.org/projects/ee4j.jersey + +## Trademarks +Eclipse Jersey is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jersey + +## Third-party Content + +Angular JS, v1.6.6 +* License MIT (http://www.opensource.org/licenses/mit-license.php) +* Project: http://angularjs.org +* Coyright: (c) 2010-2017 Google, Inc. + +aopalliance Version 1 +* License: all the source code provided by AOP Alliance is Public Domain. +* Project: http://aopalliance.sourceforge.net +* Copyright: Material in the public domain is not protected by copyright + +Bean Validation API 3.0.2 +* License: Apache License, 2.0 +* Project: http://beanvalidation.org/1.1/ +* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors +* by the @authors tag. + +Hibernate Validator CDI, 7.0.5.Final +* License: Apache License, 2.0 +* Project: https://beanvalidation.org/ +* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate + +Bootstrap v3.3.7 +* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) +* Project: http://getbootstrap.com +* Copyright: 2011-2016 Twitter, Inc + +Google Guava Version 18.0 +* License: Apache License, 2.0 +* Copyright (C) 2009 The Guava Authors + +jakarta.inject Version: 1 +* License: Apache License, 2.0 +* Copyright (C) 2009 The JSR-330 Expert Group + +Javassist Version 3.29.2-GA +* License: Apache License, 2.0 +* Project: http://www.javassist.org/ +* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. + +Jackson JAX-RS Providers Version 2.15.3 +* License: Apache License, 2.0 +* Project: https://github.com/FasterXML/jackson-jaxrs-providers +* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. + +jQuery v1.12.4 +* License: jquery.org/license +* Project: jquery.org +* Copyright: (c) jQuery Foundation + +jQuery Barcode plugin 0.3 +* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) +* Project: http://www.pasella.it/projects/jQuery/barcode +* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com + +JSR-166 Extension - JEP 266 +* License: CC0 +* No copyright +* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ + +KineticJS, v4.7.1 +* License: MIT license (http://www.opensource.org/licenses/mit-license.php) +* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS +* Copyright: Eric Rowell + +org.objectweb.asm Version 9.6 +* License: Modified BSD (https://asm.ow2.io/license.html) +* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. + +org.osgi.core version 6.0.0 +* License: Apache License, 2.0 +* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. + +org.glassfish.jersey.server.internal.monitoring.core +* License: Apache License, 2.0 +* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. +* Copyright 2010-2013 Coda Hale and Yammer, Inc. + +W3.org documents +* License: W3C License +* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ + +-------------------------------------------------------------------------------- +org.glassfish.jersey.ext.jersey-bean-validation +-------------------------------------------------------------------------------- + +# Notice for Jersey Bean Validation module +This content is produced and maintained by the Eclipse Jersey project. + +* https://projects.eclipse.org/projects/ee4j.jersey + +## Trademarks +Eclipse Jersey is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jersey + +## Third-party Content + +Hibernate Validator CDI, 7.0.5.Final +* License: Apache License, 2.0 +* Project: https://beanvalidation.org/ +* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate + +# Notice for Jersey +This content is produced and maintained by the Eclipse Jersey project. + +* Project home: https://projects.eclipse.org/projects/ee4j.jersey + +## Trademarks +Eclipse Jersey is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jersey + +## Third-party Content + +Angular JS, v1.6.6 +* License MIT (http://www.opensource.org/licenses/mit-license.php) +* Project: http://angularjs.org +* Coyright: (c) 2010-2017 Google, Inc. + +aopalliance Version 1 +* License: all the source code provided by AOP Alliance is Public Domain. +* Project: http://aopalliance.sourceforge.net +* Copyright: Material in the public domain is not protected by copyright + +Bean Validation API 3.0.2 +* License: Apache License, 2.0 +* Project: http://beanvalidation.org/1.1/ +* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors +* by the @authors tag. + +Hibernate Validator CDI, 7.0.5.Final +* License: Apache License, 2.0 +* Project: https://beanvalidation.org/ +* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate + +Bootstrap v3.3.7 +* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) +* Project: http://getbootstrap.com +* Copyright: 2011-2016 Twitter, Inc + +Google Guava Version 18.0 +* License: Apache License, 2.0 +* Copyright (C) 2009 The Guava Authors + +jakarta.inject Version: 1 +* License: Apache License, 2.0 +* Copyright (C) 2009 The JSR-330 Expert Group + +Javassist Version 3.29.2-GA +* License: Apache License, 2.0 +* Project: http://www.javassist.org/ +* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. + +Jackson JAX-RS Providers Version 2.15.3 +* License: Apache License, 2.0 +* Project: https://github.com/FasterXML/jackson-jaxrs-providers +* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. + +jQuery v1.12.4 +* License: jquery.org/license +* Project: jquery.org +* Copyright: (c) jQuery Foundation + +jQuery Barcode plugin 0.3 +* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) +* Project: http://www.pasella.it/projects/jQuery/barcode +* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com + +JSR-166 Extension - JEP 266 +* License: CC0 +* No copyright +* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ + +KineticJS, v4.7.1 +* License: MIT license (http://www.opensource.org/licenses/mit-license.php) +* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS +* Copyright: Eric Rowell + +org.objectweb.asm Version 9.6 +* License: Modified BSD (https://asm.ow2.io/license.html) +* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. + +org.osgi.core version 6.0.0 +* License: Apache License, 2.0 +* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. + +org.glassfish.jersey.server.internal.monitoring.core +* License: Apache License, 2.0 +* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. +* Copyright 2010-2013 Coda Hale and Yammer, Inc. + +W3.org documents +* License: W3C License +* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ + +-------------------------------------------------------------------------------- +com.fasterxml.jackson.dataformat.jackson-dataformat-yaml +-------------------------------------------------------------------------------- + +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + +## Licensing + +Jackson components are licensed under Apache (Software) License, version 2.0, +as per accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +-------------------------------------------------------------------------------- +jakarta.inject.jakarta.inject-api +-------------------------------------------------------------------------------- + +# Notices for Eclipse Jakarta Dependency Injection + +This content is produced and maintained by the Eclipse Jakarta Dependency Injection project. + +* Project home: https://projects.eclipse.org/projects/cdi.batch + +## Trademarks + +Jakarta Dependency Injection is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Apache License, Version 2.0 which is available at +https://www.apache.org/licenses/LICENSE-2.0. + +SPDX-License-Identifier: Apache-2.0 + +## Source Code + +The project maintains the following source code repositories: + +https://github.com/eclipse-ee4j/injection-api +https://github.com/eclipse-ee4j/injection-spec +https://github.com/eclipse-ee4j/injection-tck + +## Third-party Content + +This project leverages the following third party content. + +None + +## Cryptography + +None + +-------------------------------------------------------------------------------- +jakarta.xml.bind.jakarta.xml.bind-api +-------------------------------------------------------------------------------- + +[//]: # " Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. " +[//]: # " " +[//]: # " This program and the accompanying materials are made available under the " +[//]: # " terms of the Eclipse Distribution License v. 1.0, which is available at " +[//]: # " http://www.eclipse.org/org/documents/edl-v10.php. " +[//]: # " " +[//]: # " SPDX-License-Identifier: BSD-3-Clause " + +# Notices for Jakarta XML Binding + +This content is produced and maintained by the Jakarta XML Binding +project. + +* Project home: https://projects.eclipse.org/projects/ee4j.jaxb + +## Trademarks + +Jakarta XML Binding is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Distribution License v. 1.0 which is available at +http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: BSD-3-Clause + +## Source Code + +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jaxb-api +* https://github.com/eclipse-ee4j/jaxb-tck + +## Third-party Content + +This project leverages the following third party content. + +Apache River (3.0.0) + +* License: Apache-2.0 AND BSD-3-Clause + +ASM 7 (n/a) + +* License: BSD-3-Clause +* Project: https://asm.ow2.io/ +* Source: + https://repository.ow2.org/nexus/#nexus-search;gav~org.ow2.asm~asm-commons~~~~kw,versionexpand + +JTHarness (5.0) + +* License: (GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0) +* Project: https://wiki.openjdk.java.net/display/CodeTools/JT+Harness +* Source: http://hg.openjdk.java.net/code-tools/jtharness/ + +normalize.css (3.0.2) + +* License: MIT + +SigTest (n/a) + +* License: GPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. + +-------------------------------------------------------------------------------- +jakarta.ws.rs.jakarta.ws.rs-api +-------------------------------------------------------------------------------- + +# Notices for Jakarta RESTful Web Services + +This content is produced and maintained by the **Jakarta RESTful Web Services** +project. + +* Project home: https://projects.eclipse.org/projects/ee4j.jaxrs + +## Trademarks + +**Jakarta RESTful Web Services** is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code + +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jaxrs-api + +## Third-party Content + +This project leverages the following third party content. + +javaee-api (7.0) + +* License: Apache-2.0 AND W3C + +JUnit (4.11) + +* License: Common Public License 1.0 + +Mockito (2.16.0) + +* Project: http://site.mockito.org +* Source: https://github.com/mockito/mockito/releases/tag/v2.16.0 + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. + +-------------------------------------------------------------------------------- +io.r2dbc.r2dbc-spi +-------------------------------------------------------------------------------- + +Reactive Relational Database Connectivity + +Copyright 2017-2021 the original author or authors. + +Licensed 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 + + https://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. + +-------------------------------------------------------------------------------- +com.fasterxml.jackson.jakarta.rs.jackson-jakarta-rs-json-provider +-------------------------------------------------------------------------------- + +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers, as well as supported +commercially by FasterXML.com. + +## Licensing + +Jackson core and extension components may be licensed under different licenses. +To find the details that apply to this artifact see the accompanying LICENSE file. +For more information, including possible other licensing options, contact +FasterXML.com (http://fasterxml.com). + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +-------------------------------------------------------------------------------- +com.fasterxml.jackson.module.jackson-module-blackbird +-------------------------------------------------------------------------------- + +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers, as well as supported +commercially by FasterXML.com. + +## Licensing + +Jackson core and extension components (as well their dependencies) may be licensed under +different licenses. +To find the details that apply to this artifact see the accompanying LICENSE file. +For more information, including possible other licensing options, contact +FasterXML.com (http://fasterxml.com). + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +-------------------------------------------------------------------------------- +com.fasterxml.classmate +-------------------------------------------------------------------------------- + +Java ClassMate library was originally written by Tatu Saloranta (tatu.saloranta@iki.fi) + +Other developers who have contributed code are: + +* Brian Langel + +## Copyright + +Copyright 2007-, Tatu Saloranta (tatu.saloranta@iki.fi) + +-------------------------------------------------------------------------------- +org.glassfish.jersey.core.jersey-common +-------------------------------------------------------------------------------- + +# Notice for Jersey Core Common module +This content is produced and maintained by the Eclipse Jersey project. + + +* https://projects.eclipse.org/projects/ee4j.jersey + +## Trademarks +Eclipse Jersey is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jersey + +## Third-party Content + +Google Guava Version 18.0 +* License: Apache License, 2.0 +* Copyright: (C) 2009 The Guava Authors + +JSR-166 Extension - JEP 266 +* License: Creative Commons 1.0 (CC0) +* No copyright +* Written by Doug Lea with assistance from members of JCP JSR-166 +* Expert Group and released to the public domain, as explained at +* http://creativecommons.org/publicdomain/zero/1.0/ + +# Notice for Jersey +This content is produced and maintained by the Eclipse Jersey project. + +* Project home: https://projects.eclipse.org/projects/ee4j.jersey + +## Trademarks +Eclipse Jersey is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code +The project maintains the following source code repositories: + +* https://github.com/eclipse-ee4j/jersey + +## Third-party Content + +Angular JS, v1.6.6 +* License MIT (http://www.opensource.org/licenses/mit-license.php) +* Project: http://angularjs.org +* Coyright: (c) 2010-2017 Google, Inc. + +aopalliance Version 1 +* License: all the source code provided by AOP Alliance is Public Domain. +* Project: http://aopalliance.sourceforge.net +* Copyright: Material in the public domain is not protected by copyright + +Bean Validation API 3.0.2 +* License: Apache License, 2.0 +* Project: http://beanvalidation.org/1.1/ +* Copyright: 2009, Red Hat, Inc. and/or its affiliates, and individual contributors +* by the @authors tag. + +Hibernate Validator CDI, 7.0.5.Final +* License: Apache License, 2.0 +* Project: https://beanvalidation.org/ +* Repackaged in org.glassfish.jersey.server.validation.internal.hibernate + +Bootstrap v3.3.7 +* License: MIT license (https://github.com/twbs/bootstrap/blob/master/LICENSE) +* Project: http://getbootstrap.com +* Copyright: 2011-2016 Twitter, Inc + +Google Guava Version 18.0 +* License: Apache License, 2.0 +* Copyright (C) 2009 The Guava Authors + +jakarta.inject Version: 1 +* License: Apache License, 2.0 +* Copyright (C) 2009 The JSR-330 Expert Group + +Javassist Version 3.29.2-GA +* License: Apache License, 2.0 +* Project: http://www.javassist.org/ +* Copyright (C) 1999- Shigeru Chiba. All Rights Reserved. + +Jackson JAX-RS Providers Version 2.15.3 +* License: Apache License, 2.0 +* Project: https://github.com/FasterXML/jackson-jaxrs-providers +* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated. + +jQuery v1.12.4 +* License: jquery.org/license +* Project: jquery.org +* Copyright: (c) jQuery Foundation + +jQuery Barcode plugin 0.3 +* License: MIT & GPL (http://www.opensource.org/licenses/mit-license.php & http://www.gnu.org/licenses/gpl.html) +* Project: http://www.pasella.it/projects/jQuery/barcode +* Copyright: (c) 2009 Antonello Pasella antonello.pasella@gmail.com + +JSR-166 Extension - JEP 266 +* License: CC0 +* No copyright +* Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the public domain, as explained at http://creativecommons.org/publicdomain/zero/1.0/ + +KineticJS, v4.7.1 +* License: MIT license (http://www.opensource.org/licenses/mit-license.php) +* Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS +* Copyright: Eric Rowell + +org.objectweb.asm Version 9.6 +* License: Modified BSD (https://asm.ow2.io/license.html) +* Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved. + +org.osgi.core version 6.0.0 +* License: Apache License, 2.0 +* Copyright (c) OSGi Alliance (2005, 2008). All Rights Reserved. + +org.glassfish.jersey.server.internal.monitoring.core +* License: Apache License, 2.0 +* Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved. +* Copyright 2010-2013 Coda Hale and Yammer, Inc. + +W3.org documents +* License: W3C License +* Copyright: Copyright (c) 1994-2001 World Wide Web Consortium, (Massachusetts Institute of Technology, Institut National de Recherche en Informatique et en Automatique, Keio University). All Rights Reserved. http://www.w3.org/Consortium/Legal/ + +-------------------------------------------------------------------------------- +jakarta.annotation.jakarta.annotation-api +-------------------------------------------------------------------------------- + +# Notices for Jakarta Annotations + +This content is produced and maintained by the Jakarta Annotations project. + + * Project home: https://projects.eclipse.org/projects/ee4j.ca + +## Trademarks + +Jakarta Annotations is a trademark of the Eclipse Foundation. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made +available under the following Secondary Licenses when the conditions for such +availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU +General Public License, version 2 with the GNU Classpath Exception which is +available at https://www.gnu.org/software/classpath/license.html. + +SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + +## Source Code + +The project maintains the following source code repositories: + + * https://github.com/eclipse-ee4j/common-annotations-api + +## Third-party Content + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. + +-------------------------------------------------------------------------------- +org.apache.commons.commons-text +-------------------------------------------------------------------------------- + +Apache Commons Text +Copyright 2014-2023 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). diff --git a/notebook-migration-service/build.sbt b/notebook-migration-service/build.sbt index f744653c63c..899280ed28b 100644 --- a/notebook-migration-service/build.sbt +++ b/notebook-migration-service/build.sbt @@ -22,6 +22,16 @@ name := "notebook-migration-service" enablePlugins(JavaAppPackaging) +// Ship LICENSE-binary, NOTICE-binary, DISCLAIMER, and the licenses/ +// directory at the top of the Universal dist zip. +// See project/AddMetaInfLicenseFiles.scala. +Universal / mappings := AddMetaInfLicenseFiles.distMappings( + (Universal / mappings).value, + (ThisBuild / baseDirectory).value, + baseDirectory.value / "LICENSE-binary", + baseDirectory.value / "NOTICE-binary" +) + // Enable semanticdb for Scalafix ThisBuild / semanticdbEnabled := true ThisBuild / semanticdbVersion := scalafixSemanticdb.revision From 0fe0df860125e59a6f6ad53004c6afda63ecb716 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Wed, 17 Jun 2026 14:51:19 -0700 Subject: [PATCH 18/30] register notebook-migration-service in license checks and CI matrix --- .github/workflows/build.yml | 2 ++ bin/licensing/check_binary_deps.py | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 81373be4229..addb2d3236f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -637,6 +637,8 @@ jobs: sbt_project: ComputingUnitManagingService - service: workflow-compiling-service sbt_project: WorkflowCompilingService + - service: notebook-migration-service + sbt_project: NotebookMigrationService env: JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 diff --git a/bin/licensing/check_binary_deps.py b/bin/licensing/check_binary_deps.py index 0e9574203bc..f5c86ec93d1 100755 --- a/bin/licensing/check_binary_deps.py +++ b/bin/licensing/check_binary_deps.py @@ -51,6 +51,7 @@ "file-service/LICENSE-binary", "workflow-compiling-service/LICENSE-binary", "computing-unit-managing-service/LICENSE-binary", + "notebook-migration-service/LICENSE-binary", "amber/LICENSE-binary-java", "amber/LICENSE-binary-python", "frontend/LICENSE-binary", From 3d112c97e1d24a0945370c76f727ebfd0bde93d8 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Wed, 17 Jun 2026 15:33:12 -0700 Subject: [PATCH 19/30] regenerate NOTICE-binary as utf-8 to fix copyright mojibake --- notebook-migration-service/NOTICE-binary | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notebook-migration-service/NOTICE-binary b/notebook-migration-service/NOTICE-binary index c194ff42fb3..f5b737219f1 100644 --- a/notebook-migration-service/NOTICE-binary +++ b/notebook-migration-service/NOTICE-binary @@ -461,7 +461,7 @@ jackson-core bundles a shaded copy of FastDoubleParser under the following copyright. -Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. +Copyright © 2023 Werner Randelshofer, Switzerland. MIT License. See FastDoubleParser-NOTICE for details of other source code included in FastDoubleParser and the licenses and copyrights that apply to that code. @@ -473,7 +473,7 @@ This project provides parsers for double, float, BigDecimal and BigInteger value ## Copyright -Copyright © 2024 Werner Randelshofer, Switzerland. +Copyright © 2024 Werner Randelshofer, Switzerland. ## Licensing From 8bed0f971bf703c73e23f6074866b4cd066ef268 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Mon, 22 Jun 2026 09:47:53 -0700 Subject: [PATCH 20/30] add HealthCheckResource to notebook-migration-service --- .../service/NotebookMigrationService.scala | 3 +- .../resource/HealthCheckResource.scala | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 notebook-migration-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala index 7ed1cea0b30..b0150020672 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala @@ -26,7 +26,7 @@ import org.apache.texera.common.config.StorageConfig import org.apache.texera.auth.RequestLoggingFilter import org.apache.texera.dao.SqlServer import java.nio.file.Path -import org.apache.texera.service.resource.NotebookMigrationResource +import org.apache.texera.service.resource.{HealthCheckResource, NotebookMigrationResource} class NotebookMigrationService extends Application[NotebookMigrationServiceConfiguration] @@ -56,6 +56,7 @@ class NotebookMigrationService // Serve backend at /api environment.jersey.setUrlPattern("/api/*") + environment.jersey.register(classOf[HealthCheckResource]) environment.jersey.register(classOf[NotebookMigrationResource]) // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala new file mode 100644 index 00000000000..ef928b4dd16 --- /dev/null +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/HealthCheckResource.scala @@ -0,0 +1,32 @@ +/* + * 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.service.resource + +import jakarta.annotation.security.PermitAll +import jakarta.ws.rs.core.MediaType +import jakarta.ws.rs.{GET, Path, Produces} + +@Path("/healthcheck") +@PermitAll +@Produces(Array(MediaType.APPLICATION_JSON)) +class HealthCheckResource { + @GET + def healthCheck: Map[String, String] = Map("status" -> "ok") +} From fd0d472fc988f8a07c9762626a42c341dcab0e87 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Mon, 22 Jun 2026 09:57:41 -0700 Subject: [PATCH 21/30] validate notebook name to block path traversal and json injection --- .../resource/NotebookMigrationResource.scala | 39 ++++++++++++------- .../NotebookMigrationResourceSpec.scala | 16 ++++++++ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 66df19c4a9a..33f96788905 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -128,20 +128,6 @@ object NotebookMigrationResource extends LazyLogging { // Set the notebook in Jupyter def setNotebook(body: String): Response = { - if (!isJupyterAvailable(jupyterUrl)) { - return Response - .status(500) - .entity( - """ - { - "success": false, - "message": "Cannot connect to Jupyter server" - } - """ - ) - .build() - } - var conn: HttpURLConnection = null try { val json = mapper.readTree(body) @@ -149,6 +135,31 @@ object NotebookMigrationResource extends LazyLogging { val notebookName = json.get("notebookName").asText() val notebookData = json.get("notebookData") + // Allow only a plain ".ipynb" filename. Validated before any network call so a + // bad name is rejected with a 400 up front. This blocks path traversal in the + // Jupyter contents URL (e.g. "../../etc/x.ipynb") and keeps notebookName out of + // the raw-interpolated jupyterIframeURL JSON (no quotes/control chars). + if (!notebookName.matches("[A-Za-z0-9._-]+\\.ipynb")) { + return Response + .status(Response.Status.BAD_REQUEST) + .entity(errorJson(s"Invalid notebook name: $notebookName")) + .build() + } + + if (!isJupyterAvailable(jupyterUrl)) { + return Response + .status(500) + .entity( + """ + { + "success": false, + "message": "Cannot connect to Jupyter server" + } + """ + ) + .build() + } + // Construct Jupyter API URL val apiUrl = s"$jupyterUrl/api/contents/work/$notebookName" diff --git a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala index 222e0bb1781..82fd63edcb9 100644 --- a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala +++ b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala @@ -217,4 +217,20 @@ class NotebookMigrationResourceSpec NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()).getEntity.toString entity should include("\"v1\"") } + + // -- setNotebook ------------------------------------------------------------ + + "setNotebook" should "reject a notebook name that is not a plain .ipynb filename with 400" in { + // The name is validated before any Jupyter call, so these are rejected with a + // 400 without a running Jupyter server. Covers path traversal, a wrong + // extension, and an embedded subpath. + Seq("../../etc/evil.ipynb", "notebook.txt", "work/notebook.ipynb").foreach { name => + val body = s"""{"notebookName": "$name", "notebookData": {"cells": []}}""" + withClue(s"name=$name: ") { + NotebookMigrationResource + .setNotebook(body) + .getStatus shouldBe Response.Status.BAD_REQUEST.getStatusCode + } + } + } } From f7ca644c164660c5514d9a65a85569390c918485 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Mon, 22 Jun 2026 11:36:24 -0700 Subject: [PATCH 22/30] require auth and workflow write access on notebook-migration endpoints --- .../service/NotebookMigrationService.scala | 28 ++++- .../resource/NotebookMigrationResource.scala | 38 +++++-- .../resource/WorkflowAccessResource.scala | 46 ++++++++ .../NotebookMigrationResourceSpec.scala | 107 +++++++++++++++--- 4 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala index b0150020672..a94e728b00f 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/NotebookMigrationService.scala @@ -19,12 +19,19 @@ package org.apache.texera.service import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.typesafe.scalalogging.LazyLogging +import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider} import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.common.config.StorageConfig -import org.apache.texera.auth.RequestLoggingFilter +import org.apache.texera.auth.{ + JwtAuthFilter, + RequestLoggingFilter, + SessionUser, + UnauthorizedExceptionMapper +} import org.apache.texera.dao.SqlServer +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature import java.nio.file.Path import org.apache.texera.service.resource.{HealthCheckResource, NotebookMigrationResource} @@ -57,6 +64,9 @@ class NotebookMigrationService environment.jersey.setUrlPattern("/api/*") environment.jersey.register(classOf[HealthCheckResource]) + + NotebookMigrationService.registerAuthFeatures(environment) + environment.jersey.register(classOf[NotebookMigrationResource]) // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL @@ -64,6 +74,22 @@ class NotebookMigrationService } } object NotebookMigrationService { + // Registers JWT auth, @Auth injection, and @RolesAllowed enforcement. + // Mirrors the other Dropwizard services' registerAuthFeatures so they don't drift apart. + def registerAuthFeatures(environment: Environment): Unit = { + // Register JWT authentication filter + environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) + environment.jersey.register(classOf[UnauthorizedExceptionMapper]) + + // Enable @Auth annotation for injecting SessionUser + environment.jersey.register( + new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser]) + ) + + // Enforce @RolesAllowed annotations on resource methods + environment.jersey.register(classOf[RolesAllowedDynamicFeature]) + } + def main(args: Array[String]): Unit = { val notebookMigrationPath = Path .of(sys.env.getOrElse("TEXERA_HOME", ".")) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 33f96788905..4a27d024baf 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -20,8 +20,11 @@ package org.apache.texera.service.resource import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.typesafe.scalalogging.LazyLogging +import io.dropwizard.auth.Auth +import jakarta.annotation.security.RolesAllowed import jakarta.ws.rs._ import jakarta.ws.rs.core._ +import org.apache.texera.auth.SessionUser import org.apache.texera.dao.SqlServer import org.jooq.JSONB import org.apache.texera.dao.jooq.generated.tables.Notebook @@ -227,7 +230,7 @@ object NotebookMigrationResource extends LazyLogging { } // Store notebook + mapping in database - def storeNotebookAndMapping(body: String): Response = { + def storeNotebookAndMapping(body: String, uid: java.lang.Integer): Response = { try { val json = mapper.readTree(body) @@ -236,6 +239,14 @@ object NotebookMigrationResource extends LazyLogging { val mappingNode = json.get("mapping") val notebookNode = json.get("notebook") + // Only a user with write access to the workflow may store its notebook. + if (!WorkflowAccessResource.hasWriteAccess(wid, uid)) { + return Response + .status(Response.Status.FORBIDDEN) + .entity(errorJson(s"No write access to workflow $wid")) + .build() + } + val dsl = SqlServer.getInstance().createDSLContext() // notebook.wid is UNIQUE: a workflow has at most one notebook. If one already @@ -299,13 +310,21 @@ object NotebookMigrationResource extends LazyLogging { } // Fetch notebook + mapping - def fetchNotebookAndMapping(body: String): Response = { + def fetchNotebookAndMapping(body: String, uid: java.lang.Integer): Response = { try { val json = mapper.readTree(body) val wid: java.lang.Integer = json.get("wid").asInt() val vid: java.lang.Integer = json.get("vid").asInt() + // Only a user with write access to the workflow may fetch its notebook. + if (!WorkflowAccessResource.hasWriteAccess(wid, uid)) { + return Response + .status(Response.Status.FORBIDDEN) + .entity(errorJson(s"No write access to workflow $wid")) + .build() + } + val dsl = SqlServer.getInstance().createDSLContext() // Fetch the most recent notebook (highest nid) for this workflow version @@ -361,42 +380,43 @@ object NotebookMigrationResource extends LazyLogging { } @Path("/notebook-migration") +@RolesAllowed(Array("REGULAR", "ADMIN")) @Produces(Array(MediaType.APPLICATION_JSON)) @Consumes(Array(MediaType.APPLICATION_JSON)) class NotebookMigrationResource extends LazyLogging { @GET @Path("/get-jupyter-iframe-url") - def getJupyterIframeURL: Response = { + def getJupyterIframeURL(@Auth user: SessionUser): Response = { logger.info("Getting Jupyter iframe URL") NotebookMigrationResource.getJupyterIframeURL() } @GET @Path("/get-jupyter-url") - def getJupyterURL: Response = { + def getJupyterURL(@Auth user: SessionUser): Response = { logger.info("Getting Jupyter API URL") NotebookMigrationResource.getJupyterURL() } @POST @Path("/set-notebook") - def setNotebook(body: String): Response = { + def setNotebook(body: String, @Auth user: SessionUser): Response = { logger.info("Setting notebook") NotebookMigrationResource.setNotebook(body) } @POST @Path("/store-notebook-and-mapping") - def storeNotebookAndMapping(body: String): Response = { + def storeNotebookAndMapping(body: String, @Auth user: SessionUser): Response = { logger.info("Storing notebook and mapping") - NotebookMigrationResource.storeNotebookAndMapping(body) + NotebookMigrationResource.storeNotebookAndMapping(body, user.getUid) } @POST @Path("/fetch-notebook-and-mapping") - def fetchNotebookAndMapping(body: String): Response = { + def fetchNotebookAndMapping(body: String, @Auth user: SessionUser): Response = { logger.info("Fetching notebook and mapping") - NotebookMigrationResource.fetchNotebookAndMapping(body) + NotebookMigrationResource.fetchNotebookAndMapping(body, user.getUid) } } diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala new file mode 100644 index 00000000000..bef9c38cc21 --- /dev/null +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/WorkflowAccessResource.scala @@ -0,0 +1,46 @@ +// 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.service.resource + +import org.apache.texera.dao.SqlServer +import org.apache.texera.dao.jooq.generated.Tables.WORKFLOW_USER_ACCESS +import org.apache.texera.dao.jooq.generated.enums.PrivilegeEnum +import org.jooq.DSLContext + +object WorkflowAccessResource { + private def context: DSLContext = + SqlServer.getInstance().createDSLContext() + + /** + * Whether the given user holds a direct WRITE grant on the given workflow. + * + * Only the direct WORKFLOW_USER_ACCESS grant is consulted — no project or + * public-visibility fallback — so the notebook endpoints stay self-contained. + * + * @param wid workflow id + * @param uid user id + */ + def hasWriteAccess(wid: Integer, uid: Integer): Boolean = { + val privilege = context + .select(WORKFLOW_USER_ACCESS.PRIVILEGE) + .from(WORKFLOW_USER_ACCESS) + .where(WORKFLOW_USER_ACCESS.WID.eq(wid).and(WORKFLOW_USER_ACCESS.UID.eq(uid))) + .fetchOne(WORKFLOW_USER_ACCESS.PRIVILEGE) + privilege == PrivilegeEnum.WRITE + } +} diff --git a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala index 82fd63edcb9..92c00e5b415 100644 --- a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala +++ b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala @@ -21,12 +21,25 @@ package org.apache.texera.service.resource import jakarta.ws.rs.core.Response import org.apache.texera.dao.MockTexeraDB +import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum} import org.apache.texera.dao.jooq.generated.tables.Notebook.NOTEBOOK +import org.apache.texera.dao.jooq.generated.tables.User.USER import org.apache.texera.dao.jooq.generated.tables.Workflow.WORKFLOW import org.apache.texera.dao.jooq.generated.tables.WorkflowNotebookMapping.WORKFLOW_NOTEBOOK_MAPPING +import org.apache.texera.dao.jooq.generated.tables.WorkflowUserAccess.WORKFLOW_USER_ACCESS import org.apache.texera.dao.jooq.generated.tables.WorkflowVersion.WORKFLOW_VERSION -import org.apache.texera.dao.jooq.generated.tables.daos.{WorkflowDao, WorkflowVersionDao} -import org.apache.texera.dao.jooq.generated.tables.pojos.{Workflow, WorkflowVersion} +import org.apache.texera.dao.jooq.generated.tables.daos.{ + UserDao, + WorkflowDao, + WorkflowUserAccessDao, + WorkflowVersionDao +} +import org.apache.texera.dao.jooq.generated.tables.pojos.{ + User, + Workflow, + WorkflowUserAccess, + WorkflowVersion +} import org.jooq.JSONB import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -45,10 +58,16 @@ class NotebookMigrationResourceSpec // Randomise the seeded wid so a parallel run of unrelated specs that happen // to seed the same id wouldn't collide on the embedded postgres. private val testWid = 9000 + scala.util.Random.nextInt(1000) + private val writerEmail = s"nbms_writer_$testWid@example.com" + private val readerEmail = s"nbms_reader_$testWid@example.com" private var workflowDao: WorkflowDao = _ private var workflowVersionDao: WorkflowVersionDao = _ + private var userDao: UserDao = _ + private var workflowUserAccessDao: WorkflowUserAccessDao = _ private var seededVid: Integer = _ + private var writerUid: Integer = _ // holds WRITE access to testWid + private var readerUid: Integer = _ // holds READ access to testWid private val sampleNotebook = """{"cells":[{"cell_type":"code","metadata":{},"source":"print(1)"}]}""" @@ -62,6 +81,8 @@ class NotebookMigrationResourceSpec val cfg = getDSLContext.configuration() workflowDao = new WorkflowDao(cfg) workflowVersionDao = new WorkflowVersionDao(cfg) + userDao = new UserDao(cfg) + workflowUserAccessDao = new WorkflowUserAccessDao(cfg) cleanup() val workflow = new Workflow @@ -79,21 +100,51 @@ class NotebookMigrationResourceSpec version.setCreationTime(new Timestamp(System.currentTimeMillis())) workflowVersionDao.insert(version) seededVid = version.getVid + + // One user with WRITE access (the happy path) and one with only READ + // access, so the write-access gate can be exercised both ways. + writerUid = insertUser("nbms_writer", writerEmail) + readerUid = insertUser("nbms_reader", readerEmail) + insertAccess(writerUid, PrivilegeEnum.WRITE) + insertAccess(readerUid, PrivilegeEnum.READ) } override protected def afterEach(): Unit = cleanup() + private def insertUser(name: String, email: String): Integer = { + val user = new User + user.setName(name) + user.setEmail(email) + user.setRole(UserRoleEnum.REGULAR) + user.setPassword("password") + userDao.insert(user) + user.getUid + } + + private def insertAccess(uid: Integer, privilege: PrivilegeEnum): Unit = { + val access = new WorkflowUserAccess + access.setWid(testWid) + access.setUid(uid) + access.setPrivilege(privilege) + workflowUserAccessDao.insert(access) + } + private def cleanup(): Unit = { - // notebook and workflow_notebook_mapping cascade on workflow/version delete, - // but explicit deletes here keep state observable across tests and avoid - // depending on cascade ordering. + // Delete children before parents. workflow_user_access and notebook cascade + // on workflow delete, but explicit deletes keep state observable across tests + // and avoid depending on cascade ordering. getDSLContext.deleteFrom(WORKFLOW_NOTEBOOK_MAPPING).execute() getDSLContext.deleteFrom(NOTEBOOK).execute() + getDSLContext + .deleteFrom(WORKFLOW_USER_ACCESS) + .where(WORKFLOW_USER_ACCESS.WID.eq(testWid)) + .execute() getDSLContext .deleteFrom(WORKFLOW_VERSION) .where(WORKFLOW_VERSION.WID.eq(testWid)) .execute() getDSLContext.deleteFrom(WORKFLOW).where(WORKFLOW.WID.eq(testWid)).execute() + getDSLContext.deleteFrom(USER).where(USER.EMAIL.in(writerEmail, readerEmail)).execute() } private def storePayload( @@ -109,7 +160,7 @@ class NotebookMigrationResourceSpec // -- storeNotebookAndMapping ------------------------------------------------ "storeNotebookAndMapping" should "insert one notebook and one mapping tied to the workflow version" in { - val response = NotebookMigrationResource.storeNotebookAndMapping(storePayload()) + val response = NotebookMigrationResource.storeNotebookAndMapping(storePayload(), writerUid) response.getStatus shouldBe Response.Status.OK.getStatusCode getDSLContext.fetchCount(NOTEBOOK) shouldBe 1 @@ -131,7 +182,7 @@ class NotebookMigrationResourceSpec val mapping = """{"operator_to_cell":{"op1":["cell1"]},"cell_to_operator":{"cell1":["op1"]}}""" - NotebookMigrationResource.storeNotebookAndMapping(storePayload(notebook, mapping)) + NotebookMigrationResource.storeNotebookAndMapping(storePayload(notebook, mapping), writerUid) val storedNotebookJson = getDSLContext @@ -165,7 +216,8 @@ class NotebookMigrationResourceSpec // failed store. val unknownVid: Integer = -1 val response = NotebookMigrationResource.storeNotebookAndMapping( - storePayload(vid = unknownVid) + storePayload(vid = unknownVid), + writerUid ) response.getStatus shouldBe Response.Status.INTERNAL_SERVER_ERROR.getStatusCode getDSLContext.fetchCount(NOTEBOOK) shouldBe 0 @@ -176,10 +228,10 @@ class NotebookMigrationResourceSpec // notebook.wid is UNIQUE — one notebook per workflow. The second store must be // rejected with an explicit 409 (not a 500 from the constraint violation), and // must not add a second notebook or mapping row. - val first = NotebookMigrationResource.storeNotebookAndMapping(storePayload()) + val first = NotebookMigrationResource.storeNotebookAndMapping(storePayload(), writerUid) first.getStatus shouldBe Response.Status.OK.getStatusCode - val second = NotebookMigrationResource.storeNotebookAndMapping(storePayload()) + val second = NotebookMigrationResource.storeNotebookAndMapping(storePayload(), writerUid) second.getStatus shouldBe Response.Status.CONFLICT.getStatusCode getDSLContext.fetchCount(NOTEBOOK) shouldBe 1 @@ -189,16 +241,19 @@ class NotebookMigrationResourceSpec // -- fetchNotebookAndMapping ------------------------------------------------ "fetchNotebookAndMapping" should "return exists=false when no notebook is stored for the (wid, vid)" in { - val response = NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()) + val response = NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload(), writerUid) response.getStatus shouldBe Response.Status.OK.getStatusCode response.getEntity.toString should include("\"exists\": false") } it should "return exists=true with the stored notebook and mapping when a row exists" in { - NotebookMigrationResource.storeNotebookAndMapping(storePayload()) + NotebookMigrationResource.storeNotebookAndMapping(storePayload(), writerUid) val entity = - NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()).getEntity.toString + NotebookMigrationResource + .fetchNotebookAndMapping(fetchPayload(), writerUid) + .getEntity + .toString entity should include("\"exists\": true") entity should include("\"notebook\":") entity should include("\"mapping\":") @@ -211,13 +266,35 @@ class NotebookMigrationResourceSpec val notebook = """{"cells":[{"cell_type":"code","metadata":{},"source":"v1"}]}""" - NotebookMigrationResource.storeNotebookAndMapping(storePayload(notebook, sampleMapping)) + NotebookMigrationResource.storeNotebookAndMapping( + storePayload(notebook, sampleMapping), + writerUid + ) val entity = - NotebookMigrationResource.fetchNotebookAndMapping(fetchPayload()).getEntity.toString + NotebookMigrationResource + .fetchNotebookAndMapping(fetchPayload(), writerUid) + .getEntity + .toString entity should include("\"v1\"") } + // -- workflow write-access enforcement -------------------------------------- + + "store/fetch" should "return 403 Forbidden when the user lacks write access to the workflow" in { + // readerUid holds only READ access; the endpoints require WRITE, so both must + // be rejected with a 403 and no notebook may be written. + NotebookMigrationResource + .storeNotebookAndMapping(storePayload(), readerUid) + .getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode + + NotebookMigrationResource + .fetchNotebookAndMapping(fetchPayload(), readerUid) + .getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode + + getDSLContext.fetchCount(NOTEBOOK) shouldBe 0 + } + // -- setNotebook ------------------------------------------------------------ "setNotebook" should "reject a notebook name that is not a plain .ipynb filename with 400" in { From 372fe100755a9ce33b3952f34f6f86961a943853 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Mon, 22 Jun 2026 12:03:05 -0700 Subject: [PATCH 23/30] add resource-class and jupyter-unreachable test coverage --- .../NotebookMigrationResourceSpec.scala | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala index 92c00e5b415..9c1f8421b9e 100644 --- a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala +++ b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala @@ -20,6 +20,7 @@ package org.apache.texera.service.resource import jakarta.ws.rs.core.Response +import org.apache.texera.auth.SessionUser import org.apache.texera.dao.MockTexeraDB import org.apache.texera.dao.jooq.generated.enums.{PrivilegeEnum, UserRoleEnum} import org.apache.texera.dao.jooq.generated.tables.Notebook.NOTEBOOK @@ -157,6 +158,14 @@ class NotebookMigrationResourceSpec private def fetchPayload(vid: Integer = seededVid): String = s"""{"wid": $testWid, "vid": $vid}""" + private val resource = new NotebookMigrationResource() + + private def sessionUser(uid: Integer): SessionUser = { + val u = new User + u.setUid(uid) + new SessionUser(u) + } + // -- storeNotebookAndMapping ------------------------------------------------ "storeNotebookAndMapping" should "insert one notebook and one mapping tied to the workflow version" in { @@ -295,6 +304,39 @@ class NotebookMigrationResourceSpec getDSLContext.fetchCount(NOTEBOOK) shouldBe 0 } + // -- JAX-RS resource class (@Auth wrappers + Jupyter reachability) ---------- + + "the resource class endpoints" should "store and fetch via the authenticated class methods for a write-access user" in { + resource + .storeNotebookAndMapping(storePayload(), sessionUser(writerUid)) + .getStatus shouldBe Response.Status.OK.getStatusCode + + resource + .fetchNotebookAndMapping(fetchPayload(), sessionUser(writerUid)) + .getEntity + .toString should include("\"exists\": true") + } + + it should "reject the class methods for a read-only user with 403" in { + resource + .storeNotebookAndMapping(storePayload(), sessionUser(readerUid)) + .getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode + resource + .fetchNotebookAndMapping(fetchPayload(), sessionUser(readerUid)) + .getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode + } + + it should "return 500 from the Jupyter endpoints when the Jupyter server is unreachable" in { + // No Jupyter server runs in the unit-test environment, so isJupyterAvailable + // fails the connection and these endpoints surface a 500 rather than crashing. + val user = sessionUser(writerUid) + val validNotebook = """{"notebookName": "notebook.ipynb", "notebookData": {"cells": []}}""" + + resource.setNotebook(validNotebook, user).getStatus shouldBe 500 + resource.getJupyterURL(user).getStatus shouldBe 500 + resource.getJupyterIframeURL(user).getStatus shouldBe 500 + } + // -- setNotebook ------------------------------------------------------------ "setNotebook" should "reject a notebook name that is not a plain .ipynb filename with 400" in { From 97106a84e70fab6f0cfc04168b59b4b1796384f4 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Mon, 22 Jun 2026 12:48:44 -0700 Subject: [PATCH 24/30] cover resource HTTP paths and service run in tests --- .../NotebookMigrationServiceRunSpec.scala | 57 +++++++++++++++ .../NotebookMigrationResourceSpec.scala | 73 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 notebook-migration-service/src/test/scala/org/apache/texera/service/NotebookMigrationServiceRunSpec.scala diff --git a/notebook-migration-service/src/test/scala/org/apache/texera/service/NotebookMigrationServiceRunSpec.scala b/notebook-migration-service/src/test/scala/org/apache/texera/service/NotebookMigrationServiceRunSpec.scala new file mode 100644 index 00000000000..e71529a367d --- /dev/null +++ b/notebook-migration-service/src/test/scala/org/apache/texera/service/NotebookMigrationServiceRunSpec.scala @@ -0,0 +1,57 @@ +/* + * 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.service + +import io.dropwizard.auth.AuthDynamicFeature +import io.dropwizard.core.setup.Environment +import io.dropwizard.jersey.setup.JerseyEnvironment +import io.dropwizard.jetty.MutableServletContextHandler +import io.dropwizard.jetty.setup.ServletEnvironment +import org.apache.texera.auth.UnauthorizedExceptionMapper +import org.apache.texera.service.resource.{HealthCheckResource, NotebookMigrationResource} +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature +import org.mockito.ArgumentMatchers.isA +import org.mockito.Mockito.{mock, verify, when} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class NotebookMigrationServiceRunSpec extends AnyFlatSpec with Matchers { + + "NotebookMigrationService.run" should "register the resources and the JWT auth stack on the Jersey environment" in { + val jersey = mock(classOf[JerseyEnvironment]) + val servlets = mock(classOf[ServletEnvironment]) + val context = mock(classOf[MutableServletContextHandler]) + val env = mock(classOf[Environment]) + when(env.jersey).thenReturn(jersey) + when(env.servlets).thenReturn(servlets) + when(env.getApplicationContext).thenReturn(context) + + val service = new NotebookMigrationService + service.run(mock(classOf[NotebookMigrationServiceConfiguration]), env) + + verify(jersey).setUrlPattern("/api/*") + verify(jersey).register(classOf[HealthCheckResource]) + verify(jersey).register(classOf[NotebookMigrationResource]) + // Auth stack from registerAuthFeatures — without these, @RolesAllowed / @Auth are ignored. + verify(jersey).register(isA(classOf[AuthDynamicFeature])) + verify(jersey).register(classOf[UnauthorizedExceptionMapper]) + verify(jersey).register(classOf[RolesAllowedDynamicFeature]) + } +} diff --git a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala index 9c1f8421b9e..83c12db72ad 100644 --- a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala +++ b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala @@ -46,6 +46,9 @@ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} +import com.sun.net.httpserver.HttpServer + +import java.net.InetSocketAddress import java.sql.Timestamp import java.util.UUID @@ -166,6 +169,41 @@ class NotebookMigrationResourceSpec new SessionUser(u) } + // Runs `test` with a stub Jupyter server on localhost:9100 (the configured + // jupyter URL): GET /api returns 200 so isJupyterAvailable() passes, and PUT + // /api/contents/ returns `contentsStatus`. Lets the HTTP success/failure + // paths run without a real Jupyter. Sequential test execution (Tags.limit) keeps + // this from colliding with the "unreachable" test, which needs the port free. + private def withFakeJupyter(contentsStatus: Int)(test: => Unit): Unit = { + val server = HttpServer.create(new InetSocketAddress("localhost", 9100), 0) + server.createContext( + "/api", + (exchange: com.sun.net.httpserver.HttpExchange) => { + exchange.getRequestBody.readAllBytes() + val body = """{"version":"2.7.0"}""".getBytes("UTF-8") + exchange.sendResponseHeaders(200, body.length) + val os = exchange.getResponseBody + os.write(body) + os.close() + } + ) + // Longest-prefix match means /api/contents/... routes here, not to /api. + server.createContext( + "/api/contents", + (exchange: com.sun.net.httpserver.HttpExchange) => { + exchange.getRequestBody.readAllBytes() + val body = "{}".getBytes("UTF-8") + exchange.sendResponseHeaders(contentsStatus, body.length) + val os = exchange.getResponseBody + os.write(body) + os.close() + } + ) + server.start() + try test + finally server.stop(0) + } + // -- storeNotebookAndMapping ------------------------------------------------ "storeNotebookAndMapping" should "insert one notebook and one mapping tied to the workflow version" in { @@ -337,6 +375,41 @@ class NotebookMigrationResourceSpec resource.getJupyterIframeURL(user).getStatus shouldBe 500 } + it should "return 500 when the request body is malformed JSON" in { + // Exercises the NonFatal catch paths in setNotebook and fetchNotebookAndMapping. + val user = sessionUser(writerUid) + resource.setNotebook("not json", user).getStatus shouldBe 500 + resource.fetchNotebookAndMapping("not json", user).getStatus shouldBe 500 + } + + it should "upload the notebook and return success when Jupyter accepts it" in { + withFakeJupyter(contentsStatus = 201) { + val body = """{"notebookName": "notebook.ipynb", "notebookData": {"cells": []}}""" + val resp = resource.setNotebook(body, sessionUser(writerUid)) + resp.getStatus shouldBe Response.Status.OK.getStatusCode + resp.getEntity.toString should include("success") + } + } + + it should "return 500 when Jupyter rejects the notebook upload" in { + withFakeJupyter(contentsStatus = 500) { + val body = """{"notebookName": "notebook.ipynb", "notebookData": {"cells": []}}""" + resource.setNotebook(body, sessionUser(writerUid)).getStatus shouldBe 500 + } + } + + it should "return the Jupyter URL and iframe URL when the server is reachable" in { + withFakeJupyter(contentsStatus = 201) { + val urlResp = resource.getJupyterURL(sessionUser(writerUid)) + urlResp.getStatus shouldBe Response.Status.OK.getStatusCode + urlResp.getEntity.toString should include("localhost:9100") + + val iframeResp = resource.getJupyterIframeURL(sessionUser(writerUid)) + iframeResp.getStatus shouldBe Response.Status.OK.getStatusCode + iframeResp.getEntity.toString should include("/notebooks/work/") + } + } + // -- setNotebook ------------------------------------------------------------ "setNotebook" should "reject a notebook name that is not a plain .ipynb filename with 400" in { From f6b7b2cd67fd522b3733634794764eaa19abfbbb Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Mon, 22 Jun 2026 13:01:57 -0700 Subject: [PATCH 25/30] cover jupyter availability and upload status-code branches --- .../NotebookMigrationResourceSpec.scala | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala index 83c12db72ad..de10cf688eb 100644 --- a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala +++ b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala @@ -174,14 +174,14 @@ class NotebookMigrationResourceSpec // /api/contents/ returns `contentsStatus`. Lets the HTTP success/failure // paths run without a real Jupyter. Sequential test execution (Tags.limit) keeps // this from colliding with the "unreachable" test, which needs the port free. - private def withFakeJupyter(contentsStatus: Int)(test: => Unit): Unit = { + private def withFakeJupyter(contentsStatus: Int, apiStatus: Int = 200)(test: => Unit): Unit = { val server = HttpServer.create(new InetSocketAddress("localhost", 9100), 0) server.createContext( "/api", (exchange: com.sun.net.httpserver.HttpExchange) => { exchange.getRequestBody.readAllBytes() val body = """{"version":"2.7.0"}""".getBytes("UTF-8") - exchange.sendResponseHeaders(200, body.length) + exchange.sendResponseHeaders(apiStatus, body.length) val os = exchange.getResponseBody os.write(body) os.close() @@ -410,6 +410,27 @@ class NotebookMigrationResourceSpec } } + it should "treat a 403 from Jupyter's /api as reachable" in { + // isJupyterAvailable accepts 200 OR 403 (403 = server up but auth-gated). + withFakeJupyter(contentsStatus = 201, apiStatus = 403) { + resource.getJupyterURL(sessionUser(writerUid)).getStatus shouldBe Response.Status.OK.getStatusCode + } + } + + it should "treat an unexpected /api status (neither 200 nor 403) as unavailable" in { + withFakeJupyter(contentsStatus = 201, apiStatus = 500) { + resource.getJupyterURL(sessionUser(writerUid)).getStatus shouldBe 500 + } + } + + it should "treat a 200 from the contents API as a successful upload" in { + // Jupyter returns 200 when overwriting an existing notebook, 201 when creating. + withFakeJupyter(contentsStatus = 200) { + val body = """{"notebookName": "notebook.ipynb", "notebookData": {"cells": []}}""" + resource.setNotebook(body, sessionUser(writerUid)).getStatus shouldBe Response.Status.OK.getStatusCode + } + } + // -- setNotebook ------------------------------------------------------------ "setNotebook" should "reject a notebook name that is not a plain .ipynb filename with 400" in { From e416ce32d802de90846e2379c2eb6ae2e689d4d5 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Mon, 22 Jun 2026 13:09:55 -0700 Subject: [PATCH 26/30] fix formatting --- .../service/resource/NotebookMigrationResourceSpec.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala index de10cf688eb..a36ecdd5908 100644 --- a/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala +++ b/notebook-migration-service/src/test/scala/org/apache/texera/service/resource/NotebookMigrationResourceSpec.scala @@ -413,7 +413,9 @@ class NotebookMigrationResourceSpec it should "treat a 403 from Jupyter's /api as reachable" in { // isJupyterAvailable accepts 200 OR 403 (403 = server up but auth-gated). withFakeJupyter(contentsStatus = 201, apiStatus = 403) { - resource.getJupyterURL(sessionUser(writerUid)).getStatus shouldBe Response.Status.OK.getStatusCode + resource + .getJupyterURL(sessionUser(writerUid)) + .getStatus shouldBe Response.Status.OK.getStatusCode } } @@ -427,7 +429,9 @@ class NotebookMigrationResourceSpec // Jupyter returns 200 when overwriting an existing notebook, 201 when creating. withFakeJupyter(contentsStatus = 200) { val body = """{"notebookName": "notebook.ipynb", "notebookData": {"cells": []}}""" - resource.setNotebook(body, sessionUser(writerUid)).getStatus shouldBe Response.Status.OK.getStatusCode + resource + .setNotebook(body, sessionUser(writerUid)) + .getStatus shouldBe Response.Status.OK.getStatusCode } } From c0e243e1a00c40e4812c3d5111e07085b9a00a4b Mon Sep 17 00:00:00 2001 From: Ryan Zhang <97552093+zyratlo@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:42:58 -0700 Subject: [PATCH 27/30] add volatile modifier to jupyterIframeURL Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Ryan Zhang <97552093+zyratlo@users.noreply.github.com> --- .../texera/service/resource/NotebookMigrationResource.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 4a27d024baf..712d70942a1 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -50,7 +50,7 @@ object NotebookMigrationResource extends LazyLogging { // (own notebook-migration-service JVM + own Jupyter) in the k8s deployment, so this singleton is // effectively per-user. Do NOT deploy this service as a shared multi-user instance without adding // per-user keying here, or one user's upload would overwrite another's iframe URL. - private var jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb?token=$jupyterToken" + @volatile private var jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb?token=$jupyterToken" private def isJupyterAvailable(jupyterUrl: String): Boolean = { var conn: java.net.HttpURLConnection = null From 319f0a32abebd9060c4d81786a5a8040d62ad044 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 23 Jun 2026 10:51:51 -0700 Subject: [PATCH 28/30] build jupyter URL responses via mapper to guarantee valid JSON --- .../resource/NotebookMigrationResource.scala | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 4a27d024baf..dd94a19ac75 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -43,6 +43,11 @@ object NotebookMigrationResource extends LazyLogging { private def errorJson(message: String): String = mapper.writeValueAsString(mapper.createObjectNode().put("error", message)) + // Build a {"success": true, "url": ...} body via the mapper so the URL is JSON-escaped + // rather than raw-interpolated. + private def successUrlJson(url: String): String = + mapper.writeValueAsString(mapper.createObjectNode().put("success", true).put("url", url)) + private val jupyterUrl = StorageConfig.jupyterURL private val jupyterToken = StorageConfig.jupyterToken // The token is passed as a URL param so the browser iframe can authenticate when loading the notebook. @@ -89,16 +94,7 @@ object NotebookMigrationResource extends LazyLogging { .build() } - Response - .ok( - s""" - { - "success": true, - "url": "$jupyterIframeURL" - } - """ - ) - .build() + Response.ok(successUrlJson(jupyterIframeURL)).build() } // Returns the URL of Jupyter @@ -117,16 +113,7 @@ object NotebookMigrationResource extends LazyLogging { .build() } - Response - .ok( - s""" - { - "success": true, - "url": "$jupyterUrl" - } - """ - ) - .build() + Response.ok(successUrlJson(jupyterUrl)).build() } // Set the notebook in Jupyter From 7f90d74b75837881eeaaf3ed926aa96bb696fb6a Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 23 Jun 2026 10:58:21 -0700 Subject: [PATCH 29/30] translate unique-violation to 409 as a concurrency backstop in store --- .../service/resource/NotebookMigrationResource.scala | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index 01884d53efe..d4d34156341 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -27,6 +27,7 @@ import jakarta.ws.rs.core._ import org.apache.texera.auth.SessionUser import org.apache.texera.dao.SqlServer import org.jooq.JSONB +import org.jooq.exception.DataAccessException import org.apache.texera.dao.jooq.generated.tables.Notebook import org.apache.texera.dao.jooq.generated.tables.WorkflowNotebookMapping import java.net.{HttpURLConnection, URL} @@ -287,6 +288,14 @@ object NotebookMigrationResource extends LazyLogging { .build() } catch { + // Backstop for the pre-check TOCTOU race: two writers on a shared workflow can both + // pass the existence check, then one INSERT trips the UNIQUE(wid) constraint. Translate + // that (Postgres SQLState 23505) to a 409 rather than a generic 500. + case e: DataAccessException if e.sqlState == "23505" => + Response + .status(Response.Status.CONFLICT) + .entity(errorJson("A notebook is already stored for this workflow")) + .build() case NonFatal(e) => logger.error("Error storing mapping and workflow", e) Response From 44aafb194aa061c4255609ccfcf8cc29deafc9bb Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 23 Jun 2026 10:59:01 -0700 Subject: [PATCH 30/30] formatting --- .../texera/service/resource/NotebookMigrationResource.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala index d4d34156341..b8ebbb9f195 100644 --- a/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala +++ b/notebook-migration-service/src/main/scala/org/apache/texera/service/resource/NotebookMigrationResource.scala @@ -56,7 +56,8 @@ object NotebookMigrationResource extends LazyLogging { // (own notebook-migration-service JVM + own Jupyter) in the k8s deployment, so this singleton is // effectively per-user. Do NOT deploy this service as a shared multi-user instance without adding // per-user keying here, or one user's upload would overwrite another's iframe URL. - @volatile private var jupyterIframeURL = s"$jupyterUrl/notebooks/work/notebook.ipynb?token=$jupyterToken" + @volatile private var jupyterIframeURL = + s"$jupyterUrl/notebooks/work/notebook.ipynb?token=$jupyterToken" private def isJupyterAvailable(jupyterUrl: String): Boolean = { var conn: java.net.HttpURLConnection = null