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)) {
33 loadLocal: function() {
34 var sessions = db.loadActive();
36 Object.keys(sessions).forEach(function(key) {
37 if (sessions[key].isFromRails) {
44 save: function(k, v) {
45 var sessions = db.loadAll();
47 Object.keys(sessions).forEach(function(key) {
48 if (sessions[key].isFromRails) {
52 window.localStorage.setItem('sessions', JSON.stringify(sessions));
55 var sessions = db.loadAll();
57 window.localStorage.setItem('sessions', JSON.stringify(sessions));
59 findAPI: function(url) {
60 // Given a Workbench or API host or URL, return a promise
61 // for the corresponding API server's base URL. Typical
63 // sessionDB.findAPI('https://workbench.example/foo').then(sessionDB.login)
64 if (url.length === 5 && url.indexOf('.') < 0) {
65 url += '.arvadosapi.com';
67 if (url.indexOf('://') < 0) {
68 url = 'https://' + url;
71 return m.request(url.origin + '/discovery/v1/apis/arvados/v1/rest').then(function() {
72 return url.origin + '/';
73 }).catch(function(err) {
74 // If url is a Workbench site (and isn't too old),
75 // /status.json will tell us its API host.
76 return m.request(url.origin + '/status.json').then(function(resp) {
77 if (!resp.apiBaseURL) {
78 throw 'no apiBaseURL in status response';
80 return resp.apiBaseURL;
84 login: function(baseURL, fallbackLogin) {
85 // Initiate login procedure with given API base URL (e.g.,
86 // "http://api.example/").
88 // Any page that has a button that invokes login() must
89 // also call checkForNewToken() on (at least) its first
90 // render. Otherwise, the login procedure can't be
92 if (fallbackLogin === undefined) {
95 var session = db.loadLocal();
96 var apiHostname = new URL(session.baseURL).hostname;
97 db.discoveryDoc(session).map(function(localDD) {
98 var uuidPrefix = localDD.uuidPrefix;
99 db.discoveryDoc({baseURL: baseURL}).map(function(dd) {
100 if (uuidPrefix in dd.remoteHosts ||
101 (dd.remoteHostsViaDNS && apiHostname.endsWith('.arvadosapi.com'))) {
102 // Federated identity login via salted token
103 db.saltedToken(dd.uuidPrefix).then(function(token) {
104 m.request(baseURL+'arvados/v1/users/current', {
106 authorization: 'Bearer '+token
108 }).then(function(user) {
109 // Federated login successful.
110 var remoteSession = {
114 listedHost: (dd.uuidPrefix in localDD.remoteHosts)
116 db.save(dd.uuidPrefix, remoteSession);
117 }).catch(function(e) {
118 if (dd.uuidPrefix in localDD.remoteHosts) {
119 // If the remote system is configured to allow federated
120 // logins from this cluster, but rejected the salted
121 // token, save as a logged out session anyways.
122 var remoteSession = {
126 db.save(dd.uuidPrefix, remoteSession);
127 } else if (fallbackLogin) {
128 // Remote cluster not listed as a remote host and rejecting
129 // the salted token, try classic login.
130 db.loginClassic(baseURL);
134 } else if (fallbackLogin) {
135 // Classic login will be used when the remote system doesn't list this
136 // cluster as part of the federation.
137 db.loginClassic(baseURL);
143 loginClassic: function(baseURL) {
144 document.location = baseURL + 'login?return_to=' + encodeURIComponent(document.location.href.replace(/\?.*/, '')+'?baseURL='+encodeURIComponent(baseURL));
146 logout: function(k) {
147 // Forget the token, but leave the other info in the db so
148 // the user can log in again without providing the login
150 var sessions = db.loadAll();
151 delete sessions[k].token;
152 db.save(k, sessions[k]);
154 saltedToken: function(uuid_prefix) {
155 // Takes a cluster UUID prefix and returns a salted token to allow
156 // log into said cluster using federated identity.
157 var session = db.loadLocal();
158 return db.tokenUUID().then(function(token_uuid) {
159 var shaObj = new jsSHA("SHA-1", "TEXT");
160 var secret = session.token;
161 if (session.token.startsWith("v2/")) {
162 secret = session.token.split("/")[2];
164 shaObj.setHMACKey(secret, "TEXT");
165 shaObj.update(uuid_prefix);
166 var hmac = shaObj.getHMAC("HEX");
167 return 'v2/' + token_uuid + '/' + hmac;
170 checkForNewToken: function() {
171 // If there's a token and baseURL in the location bar (i.e.,
172 // we just landed here after a successful login), save it and
173 // scrub the location bar.
174 if (document.location.search[0] != '?') { return; }
176 document.location.search.slice(1).split('&').forEach(function(kv) {
177 var e = kv.indexOf('=');
181 params[decodeURIComponent(kv.slice(0, e))] = decodeURIComponent(kv.slice(e+1));
183 if (!params.baseURL || !params.api_token) {
184 // Have a query string, but it's not a login callback.
187 params.token = params.api_token;
188 delete params.api_token;
189 db.save(params.baseURL, params);
190 history.replaceState({}, '', document.location.origin + document.location.pathname);
192 fillMissingUUIDs: function() {
193 var sessions = db.loadAll();
194 Object.keys(sessions).forEach(function(key) {
195 if (key.indexOf('://') < 0) {
198 // key is the baseURL placeholder. We need to get our user
199 // record to find out the cluster's real uuid prefix.
200 var session = sessions[key];
201 m.request(session.baseURL+'arvados/v1/users/current', {
203 authorization: 'OAuth2 '+session.token
205 }).then(function(user) {
207 db.save(user.owner_uuid.slice(0, 5), session);
212 // Return the Workbench base URL advertised by the session's
213 // API server, or a reasonable guess, or (if neither strategy
215 workbenchBaseURL: function(session) {
216 var dd = db.discoveryDoc(session)();
218 // Don't fall back to guessing until we receive the discovery doc
221 if (dd.workbenchUrl) {
222 return dd.workbenchUrl;
224 // Guess workbench.{apihostport} is a Workbench... unless
225 // the host part of apihostport is an IPv4 or [IPv6]
227 if (!session.baseURL.match('://(\\[|\\d+\\.\\d+\\.\\d+\\.\\d+[:/])')) {
228 var wbUrl = session.baseURL.replace('://', '://workbench.');
229 // Remove the trailing slash, if it's there.
230 return wbUrl.slice(-1) === '/' ? wbUrl.slice(0, -1) : wbUrl;
234 // Return a m.stream that will get fulfilled with the
235 // discovery doc from a session's API server.
236 discoveryDoc: function(session) {
237 var cache = db.discoveryCache[session.baseURL];
238 if (!cache && session) {
239 db.discoveryCache[session.baseURL] = cache = m.stream();
240 var baseURL = session.baseURL;
241 if (baseURL[baseURL.length - 1] !== '/') {
244 m.request(baseURL+'discovery/v1/apis/arvados/v1/rest')
245 .then(function (dd) {
246 // Just in case we're talking with an old API server.
247 dd.remoteHosts = dd.remoteHosts || {};
248 if (dd.remoteHostsViaDNS === undefined) {
249 dd.remoteHostsViaDNS = false;
257 // Return a promise with the local session token's UUID from the API server.
258 tokenUUID: function() {
259 var cache = db.tokenUUIDCache;
261 var session = db.loadLocal();
262 if (session.token.startsWith("v2/")) {
263 var uuid = session.token.split("/")[1]
264 db.tokenUUIDCache = uuid;
265 return new Promise(function(resolve, reject) {
269 return db.request(session, 'arvados/v1/api_client_authorizations', {
271 filters: JSON.stringify([['api_token', '=', session.token]])
273 }).then(function(resp) {
274 var uuid = resp.items[0].uuid;
275 db.tokenUUIDCache = uuid;
279 return new Promise(function(resolve, reject) {
284 request: function(session, path, opts) {
286 opts.headers = opts.headers || {};
287 opts.headers.authorization = 'OAuth2 '+ session.token;
288 return m.request(session.baseURL + path, opts);
290 // Check non-federated remote active sessions if they should be migrated to
292 migrateNonFederatedSessions: function() {
293 var sessions = db.loadActive();
294 Object.keys(sessions).forEach(function(uuidPrefix) {
295 session = sessions[uuidPrefix];
296 if (!session.isFromRails && session.token) {
297 db.saltedToken(uuidPrefix).then(function(saltedToken) {
298 if (session.token != saltedToken) {
299 // Only try the federated login
300 db.login(session.baseURL, false);
306 // If remoteHosts is populated on the local API discovery doc, try to
307 // add any listed missing session.
308 autoLoadRemoteHosts: function() {
309 var sessions = db.loadAll();
310 var doc = db.discoveryDoc(db.loadLocal());
311 if (doc === undefined) { return; }
312 doc.map(function(d) {
313 Object.keys(d.remoteHosts).forEach(function(uuidPrefix) {
314 if (!(sessions[uuidPrefix])) {
315 db.findAPI(d.remoteHosts[uuidPrefix]).then(function(baseURL) {
316 db.login(baseURL, false);
322 // If the current logged in account is from a remote federated cluster,
323 // redirect the user to their home cluster's workbench.
324 // This is meant to avoid confusion when the user clicks through a search
325 // result on the home cluster's multi site search page, landing on the
326 // remote workbench and later trying to do another search by just clicking
327 // on the multi site search button instead of going back with the browser.
328 autoRedirectToHomeCluster: function(path) {
330 var session = db.loadLocal();
331 var userUUIDPrefix = session.user.uuid.slice(0, 5);
332 // If the current user is local to the cluster, do nothing.
333 if (userUUIDPrefix === session.user.owner_uuid.slice(0, 5)) {
336 db.discoveryDoc(session).map(function (d) {
337 // Guess the remote host from the local discovery doc settings
339 if (d.remoteHosts[userUUIDPrefix]) {
340 rHost = d.remoteHosts[userUUIDPrefix];
341 } else if (d.remoteHostsViaDNS) {
342 rHost = userUUIDPrefix + '.arvadosapi.com';
344 // This should not happen: having remote user whose uuid prefix
345 // isn't listed on remoteHosts and dns mechanism is deactivated
348 // Get the remote cluster workbench url & redirect there.
349 db.findAPI(rHost).then(function (apiUrl) {
350 db.discoveryDoc({baseURL: apiUrl}).map(function (d) {
351 document.location = d.workbenchUrl + path;