11454: Show a link to the remote workbench on the sessions page.
[arvados.git] / apps / workbench / app / assets / javascripts / models / session_db.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 window.SessionDB = function() {
6     var db = this
7     Object.assign(db, {
8         discoveryCache: {},
9         tokenUUIDCache: null,
10         loadFromLocalStorage: function() {
11             try {
12                 return JSON.parse(window.localStorage.getItem('sessions')) || {}
13             } catch(e) {}
14             return {}
15         },
16         loadAll: function() {
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
21             }
22             return all
23         },
24         loadActive: function() {
25             var sessions = db.loadAll()
26             Object.keys(sessions).forEach(function(key) {
27                 if (!sessions[key].token || !sessions[key].user.is_active)
28                     delete sessions[key]
29             })
30             return sessions
31         },
32         loadLocal: function() {
33             var sessions = db.loadActive()
34             var s = false
35             Object.values(sessions).forEach(function(session) {
36                 if (session.isFromRails) {
37                     s = session
38                     return
39                 }
40             })
41             return s
42         },
43         save: function(k, v) {
44             var sessions = db.loadAll()
45             sessions[k] = v
46             Object.keys(sessions).forEach(function(key) {
47                 if (sessions[key].isFromRails)
48                     delete sessions[key]
49             })
50             window.localStorage.setItem('sessions', JSON.stringify(sessions))
51         },
52         trash: function(k) {
53             var sessions = db.loadAll()
54             delete sessions[k]
55             window.localStorage.setItem('sessions', JSON.stringify(sessions))
56         },
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
60             // use:
61             // sessionDB.findAPI('https://workbench.example/foo').then(sessionDB.login)
62             if (url.indexOf('://') < 0)
63                 url = 'https://' + url
64             url = new URL(url)
65             return m.request(url.origin + '/discovery/v1/apis/arvados/v1/rest').then(function() {
66                 return url.origin + '/'
67             }).catch(function(err) {
68                 // If url is a Workbench site (and isn't too old),
69                 // /status.json will tell us its API host.
70                 return m.request(url.origin + '/status.json').then(function(resp) {
71                     if (!resp.apiBaseURL)
72                         throw 'no apiBaseURL in status response'
73                     return resp.apiBaseURL
74                 })
75             })
76         },
77         login: function(baseURL, fallbackLogin = true) {
78             // Initiate login procedure with given API base URL (e.g.,
79             // "http://api.example/").
80             //
81             // Any page that has a button that invokes login() must
82             // also call checkForNewToken() on (at least) its first
83             // render. Otherwise, the login procedure can't be
84             // completed.
85             var session = db.loadLocal()
86             var uuidPrefix = session.user.owner_uuid.slice(0, 5)
87             var apiHostname = new URL(session.baseURL).hostname
88             m.request(baseURL+'discovery/v1/apis/arvados/v1/rest').then(function(dd) {
89                 if (uuidPrefix in dd.remoteHosts ||
90                     (dd.remoteHostsViaDNS && apiHostname.indexOf('arvadosapi.com') >= 0)) {
91                     // Federated identity login via salted token
92                     db.saltedToken(dd.uuidPrefix).then(function(token) {
93                         m.request(baseURL+'arvados/v1/users/current', {
94                             headers: {
95                                 authorization: 'Bearer '+token,
96                             },
97                         }).then(function(user) {
98                             var remoteSession = {
99                                 user: user,
100                                 baseURL: baseURL,
101                                 token: token
102                             }
103                             db.save(user.owner_uuid.slice(0, 5), remoteSession)
104                         })
105                     })
106                 } else if (fallbackLogin) {
107                     // Classic login
108                     document.location = baseURL + 'login?return_to=' + encodeURIComponent(document.location.href.replace(/\?.*/, '')+'?baseURL='+encodeURIComponent(baseURL))
109                 }
110             })
111             return false
112         },
113         logout: function(k) {
114             // Forget the token, but leave the other info in the db so
115             // the user can log in again without providing the login
116             // host again.
117             var sessions = db.loadAll()
118             delete sessions[k].token
119             db.save(k, sessions[k])
120         },
121         saltedToken: function(uuid_prefix) {
122             // Takes a cluster UUID prefix and returns a salted token to allow
123             // log into said cluster using federated identity.
124             var session = db.loadLocal()
125             return db.tokenUUID().then(function(token_uuid){
126                 var shaObj = new jsSHA("SHA-1", "TEXT")
127                 shaObj.setHMACKey(session.token, "TEXT")
128                 shaObj.update(uuid_prefix)
129                 var hmac = shaObj.getHMAC("HEX")
130                 return 'v2/' + token_uuid + '/' + hmac
131             })
132         },
133         checkForNewToken: function() {
134             // If there's a token and baseURL in the location bar (i.e.,
135             // we just landed here after a successful login), save it and
136             // scrub the location bar.
137             if (document.location.search[0] != '?')
138                 return
139             var params = {}
140             document.location.search.slice(1).split('&').map(function(kv) {
141                 var e = kv.indexOf('=')
142                 if (e < 0)
143                     return
144                 params[decodeURIComponent(kv.slice(0, e))] = decodeURIComponent(kv.slice(e+1))
145             })
146             if (!params.baseURL || !params.api_token)
147                 // Have a query string, but it's not a login callback.
148                 return
149             params.token = params.api_token
150             delete params.api_token
151             db.save(params.baseURL, params)
152             history.replaceState({}, '', document.location.origin + document.location.pathname)
153         },
154         fillMissingUUIDs: function() {
155             var sessions = db.loadAll()
156             Object.keys(sessions).map(function(key) {
157                 if (key.indexOf('://') < 0)
158                     return
159                 // key is the baseURL placeholder. We need to get our user
160                 // record to find out the cluster's real uuid prefix.
161                 var session = sessions[key]
162                 m.request(session.baseURL+'arvados/v1/users/current', {
163                     headers: {
164                         authorization: 'OAuth2 '+session.token,
165                     },
166                 }).then(function(user) {
167                     session.user = user
168                     db.save(user.owner_uuid.slice(0, 5), session)
169                     db.trash(key)
170                 })
171             })
172         },
173         // Return the Workbench base URL advertised by the session's
174         // API server, or a reasonable guess, or (if neither strategy
175         // works out) null.
176         workbenchBaseURL: function(session) {
177             var dd = db.discoveryDoc(session)()
178             if (!dd)
179                 // Don't fall back to guessing until we receive the discovery doc
180                 return null
181             if (dd.workbenchUrl)
182                 return dd.workbenchUrl
183             // Guess workbench.{apihostport} is a Workbench... unless
184             // the host part of apihostport is an IPv4 or [IPv6]
185             // address.
186             if (!session.baseURL.match('://(\\[|\\d+\\.\\d+\\.\\d+\\.\\d+[:/])')) {
187                 var wbUrl = session.baseURL.replace('://', '://workbench.')
188                 // Remove the trailing slash, if it's there.
189                 return wbUrl.slice(-1) == '/' ? wbUrl.slice(0, -1) : wbUrl
190             }
191             return null
192         },
193         // Return a m.stream that will get fulfilled with the
194         // discovery doc from a session's API server.
195         discoveryDoc: function(session) {
196             var cache = db.discoveryCache[session.baseURL]
197             if (!cache) {
198                 db.discoveryCache[session.baseURL] = cache = m.stream()
199                 m.request(session.baseURL+'discovery/v1/apis/arvados/v1/rest').then(cache)
200             }
201             return cache
202         },
203         // Return a promise with the local session token's UUID from the API server.
204         tokenUUID: function() {
205             var cache = db.tokenUUIDCache
206             if (!cache) {
207                 var session = db.loadLocal()
208                 return db.request(session, '/arvados/v1/api_client_authorizations', {
209                     data: {
210                         filters: JSON.stringify([['api_token', '=', session.token]]),
211                     }
212                 }).then(function(resp) {
213                     var uuid = resp.items[0].uuid
214                     db.tokenUUIDCache = uuid
215                     return uuid
216                 })
217             } else {
218                 return new Promise(function(resolve, reject) {
219                     resolve(cache)
220                 })
221             }
222         },
223         request: function(session, path, opts) {
224             opts = opts || {}
225             opts.headers = opts.headers || {}
226             opts.headers.authorization = 'OAuth2 '+ session.token
227             return m.request(session.baseURL + path, opts)
228         },
229         // Check non-federated remote active sessions if they should be migrated to
230         // a salted token.
231         migrateNonFederatedSessions: function() {
232             var sessions = db.loadActive()
233             Object.keys(sessions).map(function(uuidPrefix) {
234                 session = sessions[uuidPrefix]
235                 if (!session.isFromRails && session.token && session.token.indexOf('v2/') < 0) {
236                     // Only try the federated login
237                     db.login(session.baseURL, false)
238                 }
239             })
240         },
241     })
242 }