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  }