11454: Conditional login to remote API servers.
[arvados.git] / apps / workbench / app / assets / javascripts / models / session_db.js
index 8330a68d188d9a4bd7d8947a23cbc83682dc17b5..79e98ca37e84a0b577e5c342e6a2e804cdf355eb 100644 (file)
@@ -2,9 +2,11 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-window.SessionDB = function() {
+window.SessionDB = function(rhosts) {
     var db = this
     Object.assign(db, {
+        remoteHosts: rhosts || [],
+        discoveryCache: {},
         loadFromLocalStorage: function() {
             try {
                 return JSON.parse(window.localStorage.getItem('sessions')) || {}
@@ -27,6 +29,17 @@ window.SessionDB = function() {
             })
             return sessions
         },
+        loadLocal: function() {
+            var sessions = db.loadActive()
+            var s = false
+            Object.values(sessions).forEach(function(session) {
+                if (session.isFromRails) {
+                    s = session
+                    return
+                }
+            })
+            return s
+        },
         save: function(k, v) {
             var sessions = db.loadAll()
             sessions[k] = v
@@ -69,7 +82,21 @@ window.SessionDB = function() {
             // also call checkForNewToken() on (at least) its first
             // render. Otherwise, the login procedure can't be
             // completed.
-            document.location = baseURL + 'login?return_to=' + encodeURIComponent(document.location.href.replace(/\?.*/, '')+'?baseURL='+encodeURIComponent(baseURL))
+            var session = db.loadLocal()
+            var uuidPrefix = session.user.owner_uuid.slice(0, 5)
+            var apiHostname = new URL(session.baseURL).hostname
+            m.request(baseURL+'discovery/v1/apis/arvados/v1/rest').then(function(dd) {
+                if (uuidPrefix in dd.remoteHosts ||
+                    (dd.remoteHostsViaDNS && apiHostname.indexOf('arvadosapi.com') >= 0)) {
+                    // Federated identity login via salted token
+                    db.saltedToken(dd.uuidPrefix).then(function(token) {
+                        document.location = baseURL + 'login?return_to=' + encodeURIComponent(document.location.href.replace(/\?.*/, '')+'?baseURL='+encodeURIComponent(baseURL)) + '&api_token='+encodeURIComponent(token)
+                    })
+                } else {
+                    // Classic login
+                    document.location = baseURL + 'login?return_to=' + encodeURIComponent(document.location.href.replace(/\?.*/, '')+'?baseURL='+encodeURIComponent(baseURL))
+                }
+            })
             return false
         },
         logout: function(k) {
@@ -80,6 +107,27 @@ window.SessionDB = function() {
             delete sessions[k].token
             db.save(k, sessions[k])
         },
+        saltedToken: function(uuid_prefix) {
+            // Takes a cluster UUID prefix and returns a salted token to allow
+            // log into said cluster using federated identity.
+            var session = db.loadLocal()
+            return db.request(session, '/arvados/v1/api_client_authorizations', {
+                data: {
+                    filters: JSON.stringify([['api_token', '=', session.token]]),
+                }
+            }).then(function(resp) {
+                if (resp.items.length == 1) {
+                    var token_uuid = resp.items[0].uuid
+                    if (token_uuid.length !== '') {
+                        var shaObj = new jsSHA("SHA-1", "TEXT")
+                        shaObj.setHMACKey(session.token, "TEXT")
+                        shaObj.update(uuid_prefix)
+                        var hmac = shaObj.getHMAC("HEX")
+                        return 'v2/' + token_uuid + '/' + hmac
+                    } else { return null }
+                }
+            }).catch(function(err) { return null })
+        },
         checkForNewToken: function() {
             // If there's a token and baseURL in the location bar (i.e.,
             // we just landed here after a successful login), save it and
@@ -115,11 +163,41 @@ window.SessionDB = function() {
                     },
                 }).then(function(user) {
                     session.user = user
-                    db.save(user.uuid.slice(0, 5), session)
+                    db.save(user.owner_uuid.slice(0, 5), session)
                     db.trash(key)
                 })
             })
         },
+        // Return the Workbench base URL advertised by the session's
+        // API server, or a reasonable guess, or (if neither strategy
+        // works out) null.
+        workbenchBaseURL: function(session) {
+            var dd = db.discoveryDoc(session)()
+            if (!dd)
+                // Don't fall back to guessing until we receive the discovery doc
+                return null
+            if (dd.workbenchUrl)
+                return dd.workbenchUrl
+            // Guess workbench.{apihostport} is a Workbench... unless
+            // the host part of apihostport is an IPv4 or [IPv6]
+            // address.
+            if (!session.baseURL.match('://(\\[|\\d+\\.\\d+\\.\\d+\\.\\d+[:/])')) {
+                var wbUrl = session.baseURL.replace('://', '://workbench.')
+                // Remove the trailing slash, if it's there.
+                return wbUrl.slice(-1) == '/' ? wbUrl.slice(0, -1) : wbUrl
+            }
+            return null
+        },
+        // Return a m.stream that will get fulfilled with the
+        // discovery doc from a session's API server.
+        discoveryDoc: function(session) {
+            var cache = db.discoveryCache[session.baseURL]
+            if (!cache) {
+                db.discoveryCache[session.baseURL] = cache = m.stream()
+                m.request(session.baseURL+'discovery/v1/apis/arvados/v1/rest').then(cache)
+            }
+            return cache
+        },
         request: function(session, path, opts) {
             opts = opts || {}
             opts.headers = opts.headers || {}