11454: Cache the local session token's uuid after being requested from the API.
[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(rhosts) {
6     var db = this
7     Object.assign(db, {
8         remoteHosts: rhosts || [],
9         discoveryCache: {},
10         tokenUUIDCache: null,
11         loadFromLocalStorage: function() {
12             try {
13                 return JSON.parse(window.localStorage.getItem('sessions')) || {}
14             } catch(e) {}
15             return {}
16         },
17         loadAll: function() {
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
22             }
23             return all
24         },
25         loadActive: function() {
26             var sessions = db.loadAll()
27             Object.keys(sessions).forEach(function(key) {
28                 if (!sessions[key].token)
29                     delete sessions[key]
30             })
31             return sessions
32         },
33         loadLocal: function() {
34             var sessions = db.loadActive()
35             var s = false
36             Object.values(sessions).forEach(function(session) {
37                 if (session.isFromRails) {
38                     s = session
39                     return
40                 }
41             })
42             return s
43         },
44         save: function(k, v) {
45             var sessions = db.loadAll()
46             sessions[k] = v
47             Object.keys(sessions).forEach(function(key) {
48                 if (sessions[key].isFromRails)
49                     delete sessions[key]
50             })
51             window.localStorage.setItem('sessions', JSON.stringify(sessions))
52         },
53         trash: function(k) {
54             var sessions = db.loadAll()
55             delete sessions[k]
56             window.localStorage.setItem('sessions', JSON.stringify(sessions))
57         },
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
61             // use:
62             // sessionDB.findAPI('https://workbench.example/foo').then(sessionDB.login)
63             if (url.indexOf('://') < 0)
64                 url = 'https://' + url
65             url = new URL(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) {
72                     if (!resp.apiBaseURL)
73                         throw 'no apiBaseURL in status response'
74                     return resp.apiBaseURL
75                 })
76             })
77         },
78         login: function(baseURL) {
79             // Initiate login procedure with given API base URL (e.g.,
80             // "http://api.example/").
81             //
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
85             // completed.
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)
95                     })
96                 } else {
97                     // Classic login
98                     document.location = baseURL + 'login?return_to=' + encodeURIComponent(document.location.href.replace(/\?.*/, '')+'?baseURL='+encodeURIComponent(baseURL))
99                 }
100             })
101             return false
102         },
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
106             // host again.
107             var sessions = db.loadAll()
108             delete sessions[k].token
109             db.save(k, sessions[k])
110         },
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
121             })
122         },
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] != '?')
128                 return
129             var params = {}
130             document.location.search.slice(1).split('&').map(function(kv) {
131                 var e = kv.indexOf('=')
132                 if (e < 0)
133                     return
134                 params[decodeURIComponent(kv.slice(0, e))] = decodeURIComponent(kv.slice(e+1))
135             })
136             if (!params.baseURL || !params.api_token)
137                 // Have a query string, but it's not a login callback.
138                 return
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)
143         },
144         fillMissingUUIDs: function() {
145             var sessions = db.loadAll()
146             Object.keys(sessions).map(function(key) {
147                 if (key.indexOf('://') < 0)
148                     return
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', {
153                     headers: {
154                         authorization: 'OAuth2 '+session.token,
155                     },
156                 }).then(function(user) {
157                     session.user = user
158                     db.save(user.owner_uuid.slice(0, 5), session)
159                     db.trash(key)
160                 })
161             })
162         },
163         // Return the Workbench base URL advertised by the session's
164         // API server, or a reasonable guess, or (if neither strategy
165         // works out) null.
166         workbenchBaseURL: function(session) {
167             var dd = db.discoveryDoc(session)()
168             if (!dd)
169                 // Don't fall back to guessing until we receive the discovery doc
170                 return null
171             if (dd.workbenchUrl)
172                 return dd.workbenchUrl
173             // Guess workbench.{apihostport} is a Workbench... unless
174             // the host part of apihostport is an IPv4 or [IPv6]
175             // address.
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
180             }
181             return null
182         },
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]
187             if (!cache) {
188                 db.discoveryCache[session.baseURL] = cache = m.stream()
189                 m.request(session.baseURL+'discovery/v1/apis/arvados/v1/rest').then(cache)
190             }
191             return cache
192         },
193         // Return a promise with the local session token's UUID from the API server.
194         tokenUUID: function() {
195             var cache = db.tokenUUIDCache
196             if (!cache) {
197                 var session = db.loadLocal()
198                 return db.request(session, '/arvados/v1/api_client_authorizations', {
199                     data: {
200                         filters: JSON.stringify([['api_token', '=', session.token]]),
201                     }
202                 }).then(function(resp) {
203                     var uuid = resp.items[0].uuid
204                     db.tokenUUIDCache = uuid
205                     return uuid
206                 })
207             } else {
208                 return new Promise(function(resolve, reject) {
209                     resolve(cache)
210                 })
211             }
212         },
213         request: function(session, path, opts) {
214             opts = opts || {}
215             opts.headers = opts.headers || {}
216             opts.headers.authorization = 'OAuth2 '+ session.token
217             return m.request(session.baseURL + path, opts)
218         },
219     })
220 }