//
// SPDX-License-Identifier: AGPL-3.0
-window.models = window.models || {}
-window.models.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')) || {}
})
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
delete sessions[k]
window.localStorage.setItem('sessions', JSON.stringify(sessions))
},
- login: function(host) {
- // Initiate login procedure with given API host (which can
- // optionally include scheme://).
+ findAPI: function(url) {
+ // Given a Workbench or API host or URL, return a promise
+ // for the corresponding API server's base URL. Typical
+ // use:
+ // sessionDB.findAPI('https://workbench.example/foo').then(sessionDB.login)
+ if (url.indexOf('://') < 0)
+ url = 'https://' + url
+ url = new URL(url)
+ return m.request(url.origin + '/discovery/v1/apis/arvados/v1/rest').then(function() {
+ return url.origin + '/'
+ }).catch(function(err) {
+ // If url is a Workbench site (and isn't too old),
+ // /status.json will tell us its API host.
+ return m.request(url.origin + '/status.json').then(function(resp) {
+ if (!resp.apiBaseURL)
+ throw 'no apiBaseURL in status response'
+ return resp.apiBaseURL
+ })
+ })
+ },
+ login: function(baseURL) {
+ // Initiate login procedure with given API base URL (e.g.,
+ // "http://api.example/").
//
// Any page that has a button that invokes login() must
// also call checkForNewToken() on (at least) its first
// render. Otherwise, the login procedure can't be
// completed.
- var baseURL = host
- if (baseURL.indexOf('://') < 0)
- baseURL = 'https://' + baseURL
- if (!baseURL.endsWith('/'))
- baseURL = baseURL + '/'
- 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) {
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
// scrub the location bar.
- if (!document.location.search.startsWith('?'))
+ if (document.location.search[0] != '?')
return
var params = {}
document.location.search.slice(1).split('&').map(function(kv) {
},
}).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)
})
})
- // m.request(session.baseURL + 'discovery/v1/apis/arvados/v1/rest').then(function(dd) {})
+ },
+ // 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 || {}