1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 window.SessionDB = function(rhosts) {
8 remoteHosts: rhosts || [],
11 loadFromLocalStorage: function() {
13 return JSON.parse(window.localStorage.getItem('sessions')) || {}
18 var all = db.loadFromLocalStorage()
19 if (window.defaultSession) {
20 window.defaultSession.isFromRails = true
21 all[window.defaultSession.user.uuid.slice(0, 5)] = window.defaultSession
25 loadActive: function() {
26 var sessions = db.loadAll()
27 Object.keys(sessions).forEach(function(key) {
28 if (!sessions[key].token)
33 loadLocal: function() {
34 var sessions = db.loadActive()
36 Object.values(sessions).forEach(function(session) {
37 if (session.isFromRails) {
44 save: function(k, v) {
45 var sessions = db.loadAll()
47 Object.keys(sessions).forEach(function(key) {
48 if (sessions[key].isFromRails)
51 window.localStorage.setItem('sessions', JSON.stringify(sessions))
54 var sessions = db.loadAll()
56 window.localStorage.setItem('sessions', JSON.stringify(sessions))
58 findAPI: function(url) {
59 // Given a Workbench or API host or URL, return a promise
60 // for the corresponding API server's base URL. Typical
62 // sessionDB.findAPI('https://workbench.example/foo').then(sessionDB.login)
63 if (url.indexOf('://') < 0)
64 url = 'https://' + url
66 return m.request(url.origin + '/discovery/v1/apis/arvados/v1/rest').then(function() {
67 return url.origin + '/'
68 }).catch(function(err) {
69 // If url is a Workbench site (and isn't too old),
70 // /status.json will tell us its API host.
71 return m.request(url.origin + '/status.json').then(function(resp) {
73 throw 'no apiBaseURL in status response'
74 return resp.apiBaseURL
78 login: function(baseURL) {
79 // Initiate login procedure with given API base URL (e.g.,
80 // "http://api.example/").
82 // Any page that has a button that invokes login() must
83 // also call checkForNewToken() on (at least) its first
84 // render. Otherwise, the login procedure can't be
86 var session = db.loadLocal()
87 var uuidPrefix = session.user.owner_uuid.slice(0, 5)
88 var apiHostname = new URL(session.baseURL).hostname
89 m.request(baseURL+'discovery/v1/apis/arvados/v1/rest').then(function(dd) {
90 if (uuidPrefix in dd.remoteHosts ||
91 (dd.remoteHostsViaDNS && apiHostname.indexOf('arvadosapi.com') >= 0)) {
92 // Federated identity login via salted token
93 db.saltedToken(dd.uuidPrefix).then(function(token) {
94 document.location = baseURL + 'login?return_to=' + encodeURIComponent(document.location.href.replace(/\?.*/, '')+'?baseURL='+encodeURIComponent(baseURL)) + '&api_token='+encodeURIComponent(token)
98 document.location = baseURL + 'login?return_to=' + encodeURIComponent(document.location.href.replace(/\?.*/, '')+'?baseURL='+encodeURIComponent(baseURL))
103 logout: function(k) {
104 // Forget the token, but leave the other info in the db so
105 // the user can log in again without providing the login
107 var sessions = db.loadAll()
108 delete sessions[k].token
109 db.save(k, sessions[k])
111 saltedToken: function(uuid_prefix) {
112 // Takes a cluster UUID prefix and returns a salted token to allow
113 // log into said cluster using federated identity.
114 var session = db.loadLocal()
115 return db.tokenUUID().then(function(token_uuid){
116 var shaObj = new jsSHA("SHA-1", "TEXT")
117 shaObj.setHMACKey(session.token, "TEXT")
118 shaObj.update(uuid_prefix)
119 var hmac = shaObj.getHMAC("HEX")
120 return 'v2/' + token_uuid + '/' + hmac
123 checkForNewToken: function() {
124 // If there's a token and baseURL in the location bar (i.e.,
125 // we just landed here after a successful login), save it and
126 // scrub the location bar.
127 if (document.location.search[0] != '?')
130 document.location.search.slice(1).split('&').map(function(kv) {
131 var e = kv.indexOf('=')
134 params[decodeURIComponent(kv.slice(0, e))] = decodeURIComponent(kv.slice(e+1))
136 if (!params.baseURL || !params.api_token)
137 // Have a query string, but it's not a login callback.
139 params.token = params.api_token
140 delete params.api_token
141 db.save(params.baseURL, params)
142 history.replaceState({}, '', document.location.origin + document.location.pathname)
144 fillMissingUUIDs: function() {
145 var sessions = db.loadAll()
146 Object.keys(sessions).map(function(key) {
147 if (key.indexOf('://') < 0)
149 // key is the baseURL placeholder. We need to get our user
150 // record to find out the cluster's real uuid prefix.
151 var session = sessions[key]
152 m.request(session.baseURL+'arvados/v1/users/current', {
154 authorization: 'OAuth2 '+session.token,
156 }).then(function(user) {
158 db.save(user.owner_uuid.slice(0, 5), session)
163 // Return the Workbench base URL advertised by the session's
164 // API server, or a reasonable guess, or (if neither strategy
166 workbenchBaseURL: function(session) {
167 var dd = db.discoveryDoc(session)()
169 // Don't fall back to guessing until we receive the discovery doc
172 return dd.workbenchUrl
173 // Guess workbench.{apihostport} is a Workbench... unless
174 // the host part of apihostport is an IPv4 or [IPv6]
176 if (!session.baseURL.match('://(\\[|\\d+\\.\\d+\\.\\d+\\.\\d+[:/])')) {
177 var wbUrl = session.baseURL.replace('://', '://workbench.')
178 // Remove the trailing slash, if it's there.
179 return wbUrl.slice(-1) == '/' ? wbUrl.slice(0, -1) : wbUrl
183 // Return a m.stream that will get fulfilled with the
184 // discovery doc from a session's API server.
185 discoveryDoc: function(session) {
186 var cache = db.discoveryCache[session.baseURL]
188 db.discoveryCache[session.baseURL] = cache = m.stream()
189 m.request(session.baseURL+'discovery/v1/apis/arvados/v1/rest').then(cache)
193 // Return a promise with the local session token's UUID from the API server.
194 tokenUUID: function() {
195 var cache = db.tokenUUIDCache
197 var session = db.loadLocal()
198 return db.request(session, '/arvados/v1/api_client_authorizations', {
200 filters: JSON.stringify([['api_token', '=', session.token]]),
202 }).then(function(resp) {
203 var uuid = resp.items[0].uuid
204 db.tokenUUIDCache = uuid
208 return new Promise(function(resolve, reject) {
213 request: function(session, path, opts) {
215 opts.headers = opts.headers || {}
216 opts.headers.authorization = 'OAuth2 '+ session.token
217 return m.request(session.baseURL + path, opts)