Skip to content

Commit

Permalink
[770] Token refresh not working from JupyterLab (#787)
Browse files Browse the repository at this point in the history
* Enable google_sign_in.js as a lab extension

* Use common google_sign_in.js between jupyter and Lab

* fix init-actions.sh

* Add comment

* All extension installation happens as root; lab probably needs to as well
  • Loading branch information
rtitle authored Feb 25, 2019
1 parent 3a65d6c commit 94ac1b4
Show file tree
Hide file tree
Showing 14 changed files with 113 additions and 66 deletions.
File renamed without changes.
7 changes: 7 additions & 0 deletions src/main/resources/jupyter/google_plugin_jupyterlab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = [{
id: 'google_plugin_jupyterlab',
autoStart: true,
activate: function(app) {
require('/home/jupyter-user/.jupyter/custom/google_sign_in');
}
}];
77 changes: 48 additions & 29 deletions src/main/resources/jupyter/google_sign_in.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
require.config({
"shim": {
"gapi": {
"exports": "gapi"
}
},
"paths": {
"gapi": "https://apis.google.com/js/platform"
}
})
/*
* This library is designed to run as a Jupyter/JupyterLab extension to refresh the user's
* Google credentials while using a notebook. This flow is described in more detail here:
* https://github.com/DataBiosphere/leonardo/wiki/Connecting-to-a-Leo-Notebook#token-refresh
*
* Note since this runs inside both Jupyter and JupyterLab, it should not use any
* libraries/functionality that exists in one but not the other. Examples: node, requireJS.
*/


// TEMPLATED CODE
// Leonardo has logic to find/replace templated values in the format $(...).
Expand Down Expand Up @@ -42,27 +41,27 @@ function receive(event) {
}

function startTimer() {
require(['gapi'], function (gapi) {
gapi.load('auth2', function () {
function doAuth() {
if (googleClientId) {
gapi.auth2.authorize({
'client_id': googleClientId,
'scope': 'openid profile email',
'login_hint': loginHint,
'prompt': 'none'
}, function (result) {
if (result.error) {
return;
}
set_cookie(result.access_token, result.expires_in);
});
}
loadGapi('auth2', function () {
function doAuth() {
if (googleClientId) {
gapi.auth2.authorize({
'client_id': googleClientId,
'scope': 'openid profile email',
'login_hint': loginHint,
'prompt': 'none'
}, function (result) {
if (result.error) {
console.error("Error occurred authorizing with Google: " + result.error);
return;
}
set_cookie(result.access_token, result.expires_in);
});
}
}

// refresh token every 2 minutes
setInterval(doAuth, 120000);
});
// refresh token every 2 minutes
console.log('Starting token refresh timer');
setInterval(doAuth, 120000);
});


Expand All @@ -80,7 +79,27 @@ function set_cookie(token, expires_in) {
document.cookie = "LeoToken="+token+";secure;expires="+expiresDate.toUTCString()+";path=/";
}

function loadGapi(google_lib, continuation) {
console.log('Loading Google APIs');
// Get the gapi script from Google.
const gapiScript = document.createElement('script');
gapiScript.src = 'https://apis.google.com/js/api.js';
gapiScript.type = 'text/javascript';
gapiScript.async = true;

// Load requested API scripts onto the page.
gapiScript.onload = function () {
console.log("Loading Google library '"+google_lib+"'");
gapi.load(google_lib, continuation);
}
gapiScript.onerror = function () {
console.error('Unable to load Google APIs');
}
document.head.appendChild(gapiScript);
}

function init() {
console.log('Starting google_sign_in extension');
startTimer();
window.addEventListener('message', receive);
if (!googleClientId && window.opener) {
Expand Down
45 changes: 28 additions & 17 deletions src/main/resources/jupyter/init-actions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ if [[ "${ROLE}" == 'Master' ]]; then
JUPYTER_NB_EXTENSIONS=$(jupyterNbExtensions)
JUPYTER_COMBINED_EXTENSIONS=$(jupyterCombinedExtensions)
JUPYTER_LAB_EXTENSIONS=$(jupyterLabExtensions)
JUPYTER_CUSTOM_JS_URI=$(jupyterCustomJsUri)
JUPYTER_GOOGLE_SIGN_IN_JS_URI=$(jupyterGoogleSignInJsUri)
GOOGLE_SIGN_IN_JS_URI=$(googleSignInJsUri)
JUPYTER_GOOGLE_PLUGIN_URI=$(jupyterGooglePluginUri)
JUPYTER_LAB_GOOGLE_PLUGIN_URI=$(jupyterLabGooglePluginUri)
JUPYTER_USER_SCRIPT_URI=$(jupyterUserScriptUri)
JUPYTER_NOTEBOOK_CONFIG_URI=$(jupyterNotebookConfigUri)

Expand Down Expand Up @@ -258,38 +259,48 @@ if [[ "${ROLE}" == 'Master' ]]; then
gsutil cp -r $ext /etc
JUPYTER_EXTENSION_ARCHIVE=`basename $ext`
docker cp /etc/${JUPYTER_EXTENSION_ARCHIVE} ${JUPYTER_SERVER_NAME}:${JUPYTER_HOME}/${JUPYTER_EXTENSION_ARCHIVE}
retry 3 docker exec ${JUPYTER_SERVER_NAME} ${JUPYTER_SCRIPTS}/extension/jupyter_install_lab_extension.sh ${JUPYTER_HOME}/${JUPYTER_EXTENSION_ARCHIVE}
retry 3 docker exec -u root ${JUPYTER_SERVER_NAME} ${JUPYTER_SCRIPTS}/extension/jupyter_install_lab_extension.sh ${JUPYTER_HOME}/${JUPYTER_EXTENSION_ARCHIVE}
elif [[ $ext == 'http://'* || $ext == 'https://'* ]]; then
JUPYTER_EXTENSION_FILE=`basename $ext`
curl $ext -o /etc/${JUPYTER_EXTENSION_FILE}
docker cp /etc/${JUPYTER_EXTENSION_FILE} ${JUPYTER_SERVER_NAME}:${JUPYTER_HOME}/${JUPYTER_EXTENSION_FILE}
retry 3 docker exec ${JUPYTER_SERVER_NAME} ${JUPYTER_SCRIPTS}/extension/jupyter_install_lab_extension.sh ${JUPYTER_HOME}/${JUPYTER_EXTENSION_FILE}
retry 3 docker exec -u root ${JUPYTER_SERVER_NAME} ${JUPYTER_SCRIPTS}/extension/jupyter_install_lab_extension.sh ${JUPYTER_HOME}/${JUPYTER_EXTENSION_FILE}

else
retry 3 docker exec ${JUPYTER_SERVER_NAME} ${JUPYTER_SCRIPTS}/extension/jupyter_install_lab_extension.sh $ext
retry 3 docker exec -u root ${JUPYTER_SERVER_NAME} ${JUPYTER_SCRIPTS}/extension/jupyter_install_lab_extension.sh $ext
fi
done
fi


retry 3 docker exec -u root -e PIP_USER=false ${JUPYTER_SERVER_NAME} ${JUPYTER_SCRIPTS}/extension/install_jupyter_contrib_nbextensions.sh

# If a custom.js was specified, copy it into the jupyter docker container.
if [ ! -z ${JUPYTER_CUSTOM_JS_URI} ] ; then
log 'Installing Jupyter custom.js...'
gsutil cp ${JUPYTER_CUSTOM_JS_URI} /etc
JUPYTER_CUSTOM_JS=`basename ${JUPYTER_CUSTOM_JS_URI}`
# If a google_sign_in.js was specified, copy it into the jupyter docker container.
if [ ! -z ${GOOGLE_SIGN_IN_JS_URI} ] ; then
log 'Installing Google sign in extension...'
gsutil cp ${GOOGLE_SIGN_IN_JS_URI} /etc
GOOGLE_SIGN_IN_JS=`basename ${GOOGLE_SIGN_IN_JS_URI}`
retry 3 docker exec ${JUPYTER_SERVER_NAME} mkdir -p ${JUPYTER_USER_HOME}/.jupyter/custom
docker cp /etc/${JUPYTER_CUSTOM_JS} ${JUPYTER_SERVER_NAME}:${JUPYTER_USER_HOME}/.jupyter/custom/
docker cp /etc/${GOOGLE_SIGN_IN_JS} ${JUPYTER_SERVER_NAME}:${JUPYTER_USER_HOME}/.jupyter/custom/
fi

# If a google_sign_in.js was specified, copy it into the jupyter docker container.
if [ ! -z ${JUPYTER_GOOGLE_SIGN_IN_JS_URI} ] ; then
log 'Installing Google sign in extension...'
gsutil cp ${JUPYTER_GOOGLE_SIGN_IN_JS_URI} /etc
JUPYTER_GOOGLE_SIGN_IN_JS=`basename ${JUPYTER_GOOGLE_SIGN_IN_JS_URI}`
# If a jupyter_google_plugin.js was specified, copy it into the jupyter docker container.
if [ ! -z ${JUPYTER_GOOGLE_PLUGIN_URI} ] ; then
log 'Installing jupyter_google_plugin.js extension..'
gsutil cp ${JUPYTER_GOOGLE_PLUGIN_URI} /etc
JUPYTER_GOOGLE_PLUGIN=`basename ${JUPYTER_GOOGLE_PLUGIN_URI}`
retry 3 docker exec ${JUPYTER_SERVER_NAME} mkdir -p ${JUPYTER_USER_HOME}/.jupyter/custom
docker cp /etc/${JUPYTER_GOOGLE_SIGN_IN_JS} ${JUPYTER_SERVER_NAME}:${JUPYTER_USER_HOME}/.jupyter/custom/
# note this needs to be named custom.js in the container
docker cp /etc/${JUPYTER_GOOGLE_PLUGIN} ${JUPYTER_SERVER_NAME}:${JUPYTER_USER_HOME}/.jupyter/custom/custom.js
fi

# If a jupyterlab_google_plugin.js was specified, install it as a Lab extension
if [ ! -z ${JUPYTER_LAB_GOOGLE_PLUGIN_URI} ] ; then
log 'Installing jupyterlab_google_plugin.js extension...'
gsutil cp ${JUPYTER_LAB_GOOGLE_PLUGIN_URI} /etc
JUPYTER_LAB_GOOGLE_PLUGIN=`basename ${JUPYTER_LAB_GOOGLE_PLUGIN_URI}`
docker cp /etc/${JUPYTER_LAB_GOOGLE_PLUGIN} ${JUPYTER_SERVER_NAME}:${JUPYTER_HOME}/${JUPYTER_LAB_GOOGLE_PLUGIN}
retry 3 docker exec -u root ${JUPYTER_SERVER_NAME} ${JUPYTER_SCRIPTS}/extension/jupyter_install_lab_extension.sh ${JUPYTER_HOME}/${JUPYTER_LAB_GOOGLE_PLUGIN}
fi

if [ ! -z ${JUPYTER_NOTEBOOK_CONFIG_URI} ] ; then
Expand Down
5 changes: 3 additions & 2 deletions src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ clusterResources {
initActionsScript = "init-actions.sh"
clusterDockerCompose = "cluster-docker-compose.yaml"
proxySiteConf = "cluster-site.conf"
jupyterCustomJs = "custom.js"
jupyterGoogleSignInJs = "google_sign_in.js"
googleSignInJs = "google_sign_in.js"
jupyterGooglePlugin = "google_plugin_jupyter.js"
jupyterLabGooglePlugin = "google_plugin_jupyterlab.js"
jupyterNotebookConfigUri = "jupyter_notebook_config.py"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import org.broadinstitute.dsde.workbench.leonardo.model.ClusterResource
case class ClusterResourcesConfig(initActionsScript: ClusterResource,
clusterDockerCompose: ClusterResource,
jupyterProxySiteConf: ClusterResource,
jupyterCustomJs: ClusterResource,
jupyterGoogleSignInJs: ClusterResource,
googleSignInJs: ClusterResource,
jupyterGooglePlugin: ClusterResource,
jupyterLabGooglePlugin: ClusterResource,
jupyterNotebookConfigUri: ClusterResource
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ package object config {
ClusterResource(config.getString("initActionsScript")),
ClusterResource(config.getString("clusterDockerCompose")),
ClusterResource(config.getString("proxySiteConf")),
ClusterResource(config.getString("jupyterCustomJs")),
ClusterResource(config.getString("jupyterGoogleSignInJs")),
ClusterResource(config.getString("googleSignInJs")),
ClusterResource(config.getString("jupyterGooglePlugin")),
ClusterResource(config.getString("jupyterLabGooglePlugin")),
ClusterResource(config.getString("jupyterNotebookConfigUri"))
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,9 @@ case class ClusterInitValues(googleProject: String,
jupyterExtensionUri: String,
jupyterUserScriptUri: String,
jupyterServiceAccountCredentials: String,
jupyterCustomJsUri: String,
jupyterGoogleSignInJsUri: String,
googleSignInJsUri: String,
jupyterGooglePluginUri: String,
jupyterLabGooglePluginUri: String,
userEmailLoginHint: String,
contentSecurityPolicy: String,
jupyterServerExtensions: String,
Expand Down Expand Up @@ -256,8 +257,9 @@ object ClusterInitValues {
clusterRequest.jupyterExtensionUri.map(_.toUri).getOrElse(""),
clusterRequest.jupyterUserScriptUri.map(_.toUri).getOrElse(""),
serviceAccountKey.map(_ => GcsPath(initBucketName, GcsObjectName(serviceAccountCredentialsFilename)).toUri).getOrElse(""),
GcsPath(initBucketName, GcsObjectName(clusterResourcesConfig.jupyterCustomJs.value)).toUri,
GcsPath(initBucketName, GcsObjectName(clusterResourcesConfig.jupyterGoogleSignInJs.value)).toUri,
GcsPath(initBucketName, GcsObjectName(clusterResourcesConfig.googleSignInJs.value)).toUri,
GcsPath(initBucketName, GcsObjectName(clusterResourcesConfig.jupyterGooglePlugin.value)).toUri,
GcsPath(initBucketName, GcsObjectName(clusterResourcesConfig.jupyterLabGooglePlugin.value)).toUri,
userEmailLoginHint.value,
contentSecurityPolicy,
clusterRequest.userJupyterExtensionConfig.map(x => x.serverExtensions.values.mkString(" ")).getOrElse(""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,8 @@ class LeonardoService(protected val dataprocConfig: DataprocConfig,
val resourcesToUpload = List(
clusterResourcesConfig.clusterDockerCompose,
clusterResourcesConfig.jupyterProxySiteConf,
clusterResourcesConfig.jupyterCustomJs)
clusterResourcesConfig.jupyterGooglePlugin,
clusterResourcesConfig.jupyterLabGooglePlugin)

// Uploads the service account private key to the init bucket, if defined.
// This is a no-op if createClusterAsPetServiceAccount is true.
Expand All @@ -816,15 +817,15 @@ class LeonardoService(protected val dataprocConfig: DataprocConfig,

// Fill in templated resources with the given replacements
val initScriptContent = templateResource(clusterResourcesConfig.initActionsScript, replacements)
val googleSignInJsContent = templateResource(clusterResourcesConfig.jupyterGoogleSignInJs, replacements)
val googleSignInJsContent = templateResource(clusterResourcesConfig.googleSignInJs, replacements)
val jupyterNotebookConfigContent = templateResource(clusterResourcesConfig.jupyterNotebookConfigUri, replacements)

for {
// Upload the init script to the bucket
_ <- leoGoogleStorageDAO.storeObject(initBucketName, GcsObjectName(clusterResourcesConfig.initActionsScript.value), initScriptContent, "text/plain")

// Upload the googleSignInJs file to the bucket
_ <- leoGoogleStorageDAO.storeObject(initBucketName, GcsObjectName(clusterResourcesConfig.jupyterGoogleSignInJs.value), googleSignInJsContent, "text/plain")
_ <- leoGoogleStorageDAO.storeObject(initBucketName, GcsObjectName(clusterResourcesConfig.googleSignInJs.value), googleSignInJsContent, "text/plain")

// Update the jupytyer notebook config file
_ <- leoGoogleStorageDAO.storeObject(initBucketName, GcsObjectName(clusterResourcesConfig.jupyterNotebookConfigUri.value), jupyterNotebookConfigContent, "text/plain")
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
alert("Hello Lab!");
5 changes: 3 additions & 2 deletions src/test/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ clusterResources {
initActionsScript = "test-init-actions.sh"
clusterDockerCompose = "test-cluster-docker-compose.yaml"
proxySiteConf = "test-site.conf"
jupyterCustomJs = "test-custom.js"
jupyterGoogleSignInJs = "test-google_sign_in.js"
googleSignInJs = "test-google_sign_in.js"
jupyterGooglePlugin = "test-google_plugin_jupyter.js"
jupyterLabGooglePlugin = "test-google_plugin_jupyterlab.js"
jupyterNotebookConfigUri = "jupyter_notebook_config.py"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class LeonardoModelSpec extends TestComponent with FlatSpecLike with Matchers wi
clusterInitMap("proxyDockerImage") shouldBe proxyConfig.jupyterProxyDockerImage
clusterInitMap("defaultClientId") shouldBe testClusterRequestWithExtensionAndScript.defaultClientId.getOrElse("")

clusterInitMap.size shouldBe 24
clusterInitMap.size shouldBe 25
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.broadinstitute.dsde.workbench.leonardo.CommonTestData
import org.broadinstitute.dsde.workbench.leonardo.ClusterEnrichments.stripFieldsForListCluster
import org.broadinstitute.dsde.workbench.leonardo.auth.WhitelistAuthProvider
import org.broadinstitute.dsde.workbench.leonardo.auth.sam.{MockPetClusterServiceAccountProvider, MockSwaggerSamClient}
import org.broadinstitute.dsde.workbench.leonardo.config.ClusterResourcesConfig
import org.broadinstitute.dsde.workbench.leonardo.dao.google.MockGoogleComputeDAO
import org.broadinstitute.dsde.workbench.leonardo.db.{DbSingleton, LeoComponent, TestComponent}
import org.broadinstitute.dsde.workbench.leonardo.model.LeonardoJsonSupport._
Expand Down Expand Up @@ -85,8 +86,9 @@ class LeonardoServiceSpec extends TestKit(ActorSystem("leonardotest")) with Flat
clusterFilesConfig.jupyterServerKey.getName,
clusterFilesConfig.jupyterRootCaPem.getName,
clusterResourcesConfig.jupyterProxySiteConf.value,
clusterResourcesConfig.jupyterCustomJs.value,
clusterResourcesConfig.jupyterGoogleSignInJs.value,
clusterResourcesConfig.googleSignInJs.value,
clusterResourcesConfig.jupyterGooglePlugin.value,
clusterResourcesConfig.jupyterLabGooglePlugin.value,
clusterResourcesConfig.jupyterNotebookConfigUri.value)

lazy val initFiles = (configFiles ++ serviceAccountCredentialFile).map(GcsObjectName(_))
Expand Down Expand Up @@ -577,14 +579,14 @@ class LeonardoServiceSpec extends TestKit(ActorSystem("leonardotest")) with Flat
result shouldEqual expected
}

it should "template google_sign_in.js with config values" in isolatedDbTest {
it should s"template google_sign_in.js with config values" in isolatedDbTest {

// Create replacements map
val clusterInit = ClusterInitValues(project, name1, initBucketPath, testClusterRequest, dataprocConfig, clusterFilesConfig, clusterResourcesConfig, proxyConfig, Some(serviceAccountKey), userInfo.userEmail, contentSecurityPolicy, Set(jupyterImage))
val replacements: Map[String, String] = clusterInit.toMap

// Each value in the replacement map will replace it's key in the file being processed
val result = leo.templateResource(clusterResourcesConfig.jupyterGoogleSignInJs, replacements)
val result = leo.templateResource(clusterResourcesConfig.googleSignInJs, replacements)

// Check that the values in the bash script file were correctly replaced
val expected =
Expand Down

0 comments on commit 94ac1b4

Please sign in to comment.