github.com/hernad/nomad@v1.6.112/ui/app/services/token.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 import Service, { inject as service } from '@ember/service'; 7 import { computed } from '@ember/object'; 8 import { alias, reads } from '@ember/object/computed'; 9 import { getOwner } from '@ember/application'; 10 import { assign } from '@ember/polyfills'; 11 import { task, timeout } from 'ember-concurrency'; 12 import queryString from 'query-string'; 13 import fetch from 'nomad-ui/utils/fetch'; 14 import classic from 'ember-classic-decorator'; 15 import moment from 'moment'; 16 17 const MINUTES_LEFT_AT_WARNING = 10; 18 const EXPIRY_NOTIFICATION_TITLE = 'Your access is about to expire'; 19 @classic 20 export default class TokenService extends Service { 21 @service store; 22 @service system; 23 @service router; 24 @service notifications; 25 26 aclEnabled = true; 27 28 tokenNotFound = false; 29 30 @computed 31 get secret() { 32 return window.localStorage.nomadTokenSecret; 33 } 34 35 set secret(value) { 36 if (value == null) { 37 window.localStorage.removeItem('nomadTokenSecret'); 38 } else { 39 window.localStorage.nomadTokenSecret = value; 40 } 41 } 42 43 @task(function* () { 44 const TokenAdapter = getOwner(this).lookup('adapter:token'); 45 try { 46 var token = yield TokenAdapter.findSelf(); 47 this.secret = token.secret; 48 return token; 49 } catch (e) { 50 const errors = e.errors ? e.errors.mapBy('detail') : []; 51 if (errors.find((error) => error === 'ACL support disabled')) { 52 this.set('aclEnabled', false); 53 } 54 if (errors.find((error) => error === 'ACL token not found')) { 55 this.set('tokenNotFound', true); 56 } 57 return null; 58 } 59 }) 60 fetchSelfToken; 61 62 @reads('fetchSelfToken.lastSuccessful.value') selfToken; 63 64 async exchangeOneTimeToken(oneTimeToken) { 65 const TokenAdapter = getOwner(this).lookup('adapter:token'); 66 67 const token = await TokenAdapter.exchangeOneTimeToken(oneTimeToken); 68 this.secret = token.secret; 69 } 70 71 @task(function* () { 72 try { 73 if (this.selfToken) { 74 return yield this.selfToken.get('policies'); 75 } else { 76 let policy = yield this.store.findRecord('policy', 'anonymous'); 77 return [policy]; 78 } 79 } catch (e) { 80 return []; 81 } 82 }) 83 fetchSelfTokenPolicies; 84 85 @alias('fetchSelfTokenPolicies.lastSuccessful.value') selfTokenPolicies; 86 87 @task(function* () { 88 yield this.fetchSelfToken.perform(); 89 this.kickoffTokenTTLMonitoring(); 90 if (this.aclEnabled) { 91 yield this.fetchSelfTokenPolicies.perform(); 92 } 93 }) 94 fetchSelfTokenAndPolicies; 95 96 // All non Ember Data requests should go through authorizedRequest. 97 // However, the request that gets regions falls into that category. 98 // This authorizedRawRequest is necessary in order to fetch data 99 // with the guarantee of a token but without the automatic region 100 // param since the region cannot be known at this point. 101 authorizedRawRequest(url, options = {}) { 102 const credentials = 'include'; 103 const headers = {}; 104 const token = this.secret; 105 106 if (token) { 107 headers['X-Nomad-Token'] = token; 108 } 109 110 return fetch(url, assign(options, { headers, credentials })); 111 } 112 113 authorizedRequest(url, options) { 114 if (this.get('system.shouldIncludeRegion')) { 115 const region = this.get('system.activeRegion'); 116 if (region && url.indexOf('region=') === -1) { 117 url = addParams(url, { region }); 118 } 119 } 120 121 return this.authorizedRawRequest(url, options); 122 } 123 124 reset() { 125 this.fetchSelfToken.cancelAll({ resetState: true }); 126 this.fetchSelfTokenPolicies.cancelAll({ resetState: true }); 127 this.fetchSelfTokenAndPolicies.cancelAll({ resetState: true }); 128 this.monitorTokenTime.cancelAll({ resetState: true }); 129 window.localStorage.removeItem('nomadOIDCNonce'); 130 window.localStorage.removeItem('nomadOIDCAuthMethod'); 131 } 132 133 kickoffTokenTTLMonitoring() { 134 this.monitorTokenTime.perform(); 135 } 136 137 @task(function* () { 138 while (this.selfToken?.expirationTime) { 139 const diff = new Date(this.selfToken.expirationTime) - new Date(); 140 // Let the user know at the 10 minute mark, 141 // or any time they refresh with under 10 minutes left 142 if (diff < 1000 * 60 * MINUTES_LEFT_AT_WARNING) { 143 const existingNotification = this.notifications.queue?.find( 144 (m) => m.title === EXPIRY_NOTIFICATION_TITLE 145 ); 146 // For the sake of updating the "time left" message, we keep running the task down to the moment of expiration 147 if (diff > 0) { 148 if (existingNotification) { 149 existingNotification.set( 150 'message', 151 `Your token access expires ${moment( 152 this.selfToken.expirationTime 153 ).fromNow()}` 154 ); 155 } else { 156 if (!this.expirationNotificationDismissed) { 157 this.notifications.add({ 158 title: EXPIRY_NOTIFICATION_TITLE, 159 message: `Your token access expires ${moment( 160 this.selfToken.expirationTime 161 ).fromNow()}`, 162 color: 'warning', 163 sticky: true, 164 customCloseAction: () => { 165 this.set('expirationNotificationDismissed', true); 166 }, 167 customAction: { 168 label: 'Re-authenticate', 169 action: () => { 170 this.router.transitionTo('settings.tokens'); 171 }, 172 }, 173 }); 174 } 175 } 176 } else { 177 if (existingNotification) { 178 existingNotification.setProperties({ 179 title: 'Your access has expired', 180 message: `Your token will need to be re-authenticated`, 181 }); 182 } 183 this.monitorTokenTime.cancelAll(); // Stop updating time left after expiration 184 } 185 } 186 yield timeout(1000); 187 } 188 }) 189 monitorTokenTime; 190 } 191 192 function addParams(url, params) { 193 const paramsStr = queryString.stringify(params); 194 const delimiter = url.includes('?') ? '&' : '?'; 195 return `${url}${delimiter}${paramsStr}`; 196 }