1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 window.SessionDB = function() {
10 loadFromLocalStorage: function() {
12 return JSON.parse(window.localStorage.getItem('sessions')) || {}
17 var all = db.loadFromLocalStorage()
18 if (window.defaultSession) {
19 window.defaultSession.isFromRails = true
20 all[window.defaultSession.user.uuid.slice(0, 5)] = window.defaultSession
24 loadActive: function() {
25 var sessions = db.loadAll()
26 Object.keys(sessions).forEach(function(key) {
27 if (!sessions[key].token || (sessions[key].user && !sessions[key].user.is_active))
32 loadLocal: function() {
33 var sessions = db.loadActive()
35 Object.values(sessions).forEach(function(session) {
36 if (session.isFromRails) {
43 save: function(k, v) {
44 var sessions = db.loadAll()
46 Object.keys(sessions).forEach(function(key) {
47 if (sessions[key].isFromRails)
50 window.localStorage.setItem('sessions', JSON.stringify(sessions))
53 var sessions = db.loadAll()
55 window.localStorage.setItem('sessions', JSON.stringify(sessions))
57 findAPI: function(url) {
58 // Given a Workbench or API host or URL, return a promise
59 // for the corresponding API server's base URL. Typical
61 // sessionDB.findAPI('https://workbench.example/foo').then(sessionDB.login)
62 if (url.length === 5 && url.indexOf('.') < 0)
63 url += '.arvadosapi.com'
64 if (url.indexOf('://') < 0)
65 url = 'https://' + url
67 return m.request(url.origin + '/discovery/v1/apis/arvados/v1/rest').then(function() {
68 return url.origin + '/'
69 }).catch(function(err) {
70 // If url is a Workbench site (and isn't too old),
71 // /status.json will tell us its API host.
72 return m.request(url.origin + '/status.json').then(function(resp) {
74 throw 'no apiBaseURL in status response'
75 return resp.apiBaseURL
79 login: function(baseURL, fallbackLogin = true) {
80 // Initiate login procedure with given API base URL (e.g.,
81 // "http://api.example/").
83 // Any page that has a button that invokes login() must
84 // also call checkForNewToken() on (at least) its first
85 // render. Otherwise, the login procedure can't be
87 var session = db.loadLocal()
88 var uuidPrefix = session.user.owner_uuid.slice(0, 5)
89 var apiHostname = new URL(session.baseURL).hostname
90 m.request(session.baseURL+'discovery/v1/apis/arvados/v1/rest').then(function(localDD) {
91 m.request(baseURL+'discovery/v1/apis/arvados/v1/rest').then(function(dd) {
92 if (uuidPrefix in dd.remoteHosts ||
93 (dd.remoteHostsViaDNS && apiHostname.indexOf('arvadosapi.com') >= 0)) {
94 // Federated identity login via salted token
95 db.saltedToken(dd.uuidPrefix).then(function(token) {
96 m.request(baseURL+'arvados/v1/users/current', {
98 authorization: 'Bearer '+token,
100 }).then(function(user) {
101 // Federated login successful.
102 var remoteSession = {
106 listedHost: (dd.uuidPrefix in localDD.remoteHosts),
108 db.save(dd.uuidPrefix, remoteSession)
109 }).catch(function(e) {
110 if (dd.uuidPrefix in localDD.remoteHosts) {
111 // If the remote system is configured to allow federated
112 // logins from this cluster, but rejected the salted
113 // token, save as a logged out session anyways.
114 var remoteSession = {
118 db.save(dd.uuidPrefix, remoteSession)
119 } else if (fallbackLogin) {
120 // Remote cluster not listed as a remote host and rejecting
121 // the salted token, try classic login.
122 db.loginClassic(baseURL)
126 } else if (fallbackLogin) {
127 // Classic login will be used when the remote system doesn't list this
128 // cluster as part of the federation.
129 db.loginClassic(baseURL)
135 loginClassic: function(baseURL) {
136 document.location = baseURL + 'login?return_to=' + encodeURIComponent(document.location.href.replace(/\?.*/, '')+'?baseURL='+encodeURIComponent(baseURL))
138 logout: function(k) {
139 // Forget the token, but leave the other info in the db so
140 // the user can log in again without providing the login
142 var sessions = db.loadAll()
143 delete sessions[k].token
144 db.save(k, sessions[k])
146 saltedToken: function(uuid_prefix) {
147 // Takes a cluster UUID prefix and returns a salted token to allow
148 // log into said cluster using federated identity.
149 var session = db.loadLocal()
150 return db.tokenUUID().then(function(token_uuid){
151 var shaObj = new jsSHA("SHA-1", "TEXT")
152 shaObj.setHMACKey(session.token, "TEXT")
153 shaObj.update(uuid_prefix)
154 var hmac = shaObj.getHMAC("HEX")
155 return 'v2/' + token_uuid + '/' + hmac
158 checkForNewToken: function() {
159 // If there's a token and baseURL in the location bar (i.e.,
160 // we just landed here after a successful login), save it and
161 // scrub the location bar.
162 if (document.location.search[0] != '?')
165 document.location.search.slice(1).split('&').map(function(kv) {
166 var e = kv.indexOf('=')
169 params[decodeURIComponent(kv.slice(0, e))] = decodeURIComponent(kv.slice(e+1))
171 if (!params.baseURL || !params.api_token)
172 // Have a query string, but it's not a login callback.
174 params.token = params.api_token
175 delete params.api_token
176 db.save(params.baseURL, params)
177 history.replaceState({}, '', document.location.origin + document.location.pathname)
179 fillMissingUUIDs: function() {
180 var sessions = db.loadAll()
181 Object.keys(sessions).map(function(key) {
182 if (key.indexOf('://') < 0)
184 // key is the baseURL placeholder. We need to get our user
185 // record to find out the cluster's real uuid prefix.
186 var session = sessions[key]
187 m.request(session.baseURL+'arvados/v1/users/current', {
189 authorization: 'OAuth2 '+session.token,
191 }).then(function(user) {
193 db.save(user.owner_uuid.slice(0, 5), session)
198 // Return the Workbench base URL advertised by the session's
199 // API server, or a reasonable guess, or (if neither strategy
201 workbenchBaseURL: function(session) {
202 var dd = db.discoveryDoc(session)()
204 // Don't fall back to guessing until we receive the discovery doc
207 return dd.workbenchUrl
208 // Guess workbench.{apihostport} is a Workbench... unless
209 // the host part of apihostport is an IPv4 or [IPv6]
211 if (!session.baseURL.match('://(\\[|\\d+\\.\\d+\\.\\d+\\.\\d+[:/])')) {
212 var wbUrl = session.baseURL.replace('://', '://workbench.')
213 // Remove the trailing slash, if it's there.
214 return wbUrl.slice(-1) == '/' ? wbUrl.slice(0, -1) : wbUrl
218 // Return a m.stream that will get fulfilled with the
219 // discovery doc from a session's API server.
220 discoveryDoc: function(session) {
221 var cache = db.discoveryCache[session.baseURL]
223 db.discoveryCache[session.baseURL] = cache = m.stream()
224 m.request(session.baseURL+'discovery/v1/apis/arvados/v1/rest').then(cache)
228 // Return a promise with the local session token's UUID from the API server.
229 tokenUUID: function() {
230 var cache = db.tokenUUIDCache
232 var session = db.loadLocal()
233 return db.request(session, '/arvados/v1/api_client_authorizations', {
235 filters: JSON.stringify([['api_token', '=', session.token]]),
237 }).then(function(resp) {
238 var uuid = resp.items[0].uuid
239 db.tokenUUIDCache = uuid
243 return new Promise(function(resolve, reject) {
248 request: function(session, path, opts) {
250 opts.headers = opts.headers || {}
251 opts.headers.authorization = 'OAuth2 '+ session.token
252 return m.request(session.baseURL + path, opts)
254 // Check non-federated remote active sessions if they should be migrated to
256 migrateNonFederatedSessions: function() {
257 var sessions = db.loadActive()
258 Object.keys(sessions).map(function(uuidPrefix) {
259 session = sessions[uuidPrefix]
260 if (!session.isFromRails && session.token && session.token.indexOf('v2/') < 0) {
261 // Only try the federated login
262 db.login(session.baseURL, false)
266 // If remoteHosts is listed on the local API discovery doc, try to add any
267 // listed remote without an active session.
268 autoLoadRemoteHosts: function() {
269 var activeSessions = db.loadActive()
270 var doc = db.discoveryDoc(db.loadLocal())
271 doc.map(function(d) {
272 Object.keys(d.remoteHosts).map(function(uuidPrefix) {
273 if (!(uuidPrefix in Object.keys(activeSessions))) {
274 db.findAPI(d.remoteHosts[uuidPrefix]).then(function(baseURL) {
275 db.login(baseURL, false)
281 // If the current logged in account is from a remote federated cluster,
282 // redirect the user to their home cluster's workbench.
283 // This is meant to avoid confusion when the user clicks through a search
284 // result on the home cluster's multi site search page, landing on the
285 // remote workbench and later trying to do another search by just clicking
286 // on the multi site search button instead of going back with the browser.
287 autoRedirectToHomeCluster: function(path = '/') {
288 var session = db.loadLocal()
289 var userUUIDPrefix = session.user.uuid.slice(0, 5)
290 // If the current user is local to the cluster, do nothing.
291 if (userUUIDPrefix == session.user.owner_uuid.slice(0, 5)) {
294 var doc = db.discoveryDoc(session)
295 doc.map(function(d) {
296 // Guess the remote host from the local discovery doc settings
298 if (d.remoteHosts[userUUIDPrefix]) {
299 rHost = d.remoteHosts[userUUIDPrefix]
300 } else if (d.remoteHostsViaDNS) {
301 rHost = userUUIDPrefix + '.arvadosapi.com'
303 // This should not happen: having remote user whose uuid prefix
304 // isn't listed on remoteHosts and dns mechanism is deactivated
307 // Get the remote cluster workbench url & redirect there.
308 db.findAPI(rHost).then(function(apiUrl) {
309 var doc = db.discoveryDoc({baseURL: apiUrl})
310 doc.map(function(d) {
311 document.location = d.workbenchUrl + path