github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/app/services/token.js (about)

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