github.com/hernad/nomad@v1.6.112/ui/app/controllers/settings/tokens.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  // @ts-check
     7  import { inject as service } from '@ember/service';
     8  import Controller from '@ember/controller';
     9  import { getOwner } from '@ember/application';
    10  import { alias } from '@ember/object/computed';
    11  import { action } from '@ember/object';
    12  import classic from 'ember-classic-decorator';
    13  import { tracked } from '@glimmer/tracking';
    14  import Ember from 'ember';
    15  
    16  /**
    17   * @type {RegExp}
    18   */
    19  const JWT_MATCH_EXPRESSION = /^[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+$/;
    20  
    21  @classic
    22  export default class Tokens extends Controller {
    23    @service token;
    24    @service store;
    25    @service router;
    26  
    27    queryParams = ['code', 'state', 'jwtAuthMethod'];
    28  
    29    @tracked secret = this.token.secret;
    30  
    31    /**
    32     * @type {(null | "success" | "failure" | "jwtFailure")} signInStatus
    33     */
    34    @tracked
    35    signInStatus = null;
    36  
    37    @alias('token.selfToken') tokenRecord;
    38  
    39    resetStore() {
    40      this.store.unloadAll();
    41    }
    42  
    43    @action
    44    clearTokenProperties() {
    45      this.token.setProperties({
    46        secret: undefined,
    47        tokenNotFound: false,
    48      });
    49      this.signInStatus = null;
    50      // Clear out all data to ensure only data the anonymous token is privileged to see is shown
    51      this.resetStore();
    52      this.token.reset();
    53      this.store.findAll('auth-method');
    54    }
    55  
    56    /**
    57     * @returns {import('@ember/array/mutable').default<import('../../models/auth-method').default>}
    58     */
    59    get authMethods() {
    60      return this.model?.authMethods || [];
    61    }
    62  
    63    get hasJWTAuthMethods() {
    64      return this.authMethods.any((method) => method.type === 'JWT');
    65    }
    66  
    67    get nonTokenAuthMethods() {
    68      return this.authMethods.rejectBy('type', 'JWT');
    69    }
    70  
    71    get JWTAuthMethods() {
    72      return this.authMethods.filterBy('type', 'JWT');
    73    }
    74  
    75    get JWTAuthMethodOptions() {
    76      return this.JWTAuthMethods.map((method) => ({
    77        key: method.name,
    78        label: method.name,
    79      }));
    80    }
    81  
    82    get defaultJWTAuthMethod() {
    83      return (
    84        this.JWTAuthMethods.findBy('default', true) || this.JWTAuthMethods[0]
    85      );
    86    }
    87  
    88    @action
    89    setCurrentAuthMethod() {
    90      if (!this.jwtAuthMethod) {
    91        this.jwtAuthMethod = this.defaultJWTAuthMethod?.name;
    92      }
    93    }
    94  
    95    /**
    96     * @type {string}
    97     */
    98    @tracked jwtAuthMethod;
    99  
   100    /**
   101     * @type {boolean}
   102     */
   103    get currentSecretIsJWT() {
   104      return this.secret?.length > 36 && this.secret.match(JWT_MATCH_EXPRESSION);
   105    }
   106  
   107    @action
   108    async verifyToken() {
   109      const { secret } = this;
   110      /**
   111       * @type {import('../../adapters/token').default}
   112       */
   113  
   114      // Ember currently lacks types for getOwner: https://github.com/emberjs/ember.js/issues/19916
   115      // @ts-ignore
   116      const TokenAdapter = getOwner(this).lookup('adapter:token');
   117  
   118      const isJWT = secret.length > 36 && secret.match(JWT_MATCH_EXPRESSION);
   119  
   120      if (isJWT) {
   121        const methodName = this.jwtAuthMethod;
   122  
   123        // If user passes a JWT token, but there is no JWT auth method, throw an error
   124        if (!methodName) {
   125          this.token.set('secret', undefined);
   126          this.signInStatus = 'jwtFailure';
   127          return;
   128        }
   129  
   130        this.clearTokenProperties();
   131  
   132        // Set bearer token instead of findSelf etc.
   133        TokenAdapter.loginJWT(secret, methodName).then(
   134          (token) => {
   135            this.token.setProperties({
   136              secret: token.secret,
   137              tokenNotFound: false,
   138            });
   139            this.set('secret', null);
   140  
   141            // Clear out all data to ensure only data the new token is privileged to see is shown
   142            this.resetStore();
   143  
   144            // Refetch the token and associated policies
   145            this.token.get('fetchSelfTokenAndPolicies').perform().catch();
   146  
   147            this.signInStatus = 'success';
   148          },
   149          () => {
   150            this.token.set('secret', undefined);
   151            this.signInStatus = 'failure';
   152          }
   153        );
   154      } else {
   155        this.clearTokenProperties();
   156        this.token.set('secret', secret);
   157        this.set('secret', null);
   158  
   159        TokenAdapter.findSelf().then(
   160          () => {
   161            // Clear out all data to ensure only data the new token is privileged to see is shown
   162            this.resetStore();
   163  
   164            // Refetch the token and associated policies
   165            this.token.get('fetchSelfTokenAndPolicies').perform().catch();
   166  
   167            this.signInStatus = 'success';
   168            this.token.set('tokenNotFound', false);
   169          },
   170          () => {
   171            this.token.set('secret', undefined);
   172            this.signInStatus = 'failure';
   173          }
   174        );
   175      }
   176    }
   177  
   178    // Generate a 20-char nonce, using window.crypto to
   179    // create a sufficiently-large output then trimming
   180    generateNonce() {
   181      let randomArray = new Uint32Array(10);
   182      crypto.getRandomValues(randomArray);
   183      return randomArray.join('').slice(0, 20);
   184    }
   185  
   186    @action redirectToSSO(method) {
   187      const provider = method.name;
   188      const nonce = this.generateNonce();
   189  
   190      window.localStorage.setItem('nomadOIDCNonce', nonce);
   191      window.localStorage.setItem('nomadOIDCAuthMethod', provider);
   192  
   193      let redirectURL;
   194      if (Ember.testing) {
   195        redirectURL = this.router.currentURL;
   196      } else {
   197        redirectURL = new URL(window.location.toString());
   198        redirectURL.search = '';
   199        redirectURL = redirectURL.href;
   200      }
   201  
   202      method
   203        .getAuthURL({
   204          AuthMethodName: provider,
   205          ClientNonce: nonce,
   206          RedirectUri: redirectURL,
   207        })
   208        .then(({ AuthURL }) => {
   209          if (Ember.testing) {
   210            this.router.transitionTo(AuthURL.split('/ui')[1]);
   211          } else {
   212            window.location = AuthURL;
   213          }
   214        });
   215    }
   216  
   217    @tracked code = null;
   218    @tracked state = null;
   219  
   220    get isValidatingToken() {
   221      if (this.code && this.state) {
   222        this.validateSSO();
   223        return true;
   224      } else {
   225        return false;
   226      }
   227    }
   228  
   229    async validateSSO() {
   230      let redirectURL;
   231      if (Ember.testing) {
   232        redirectURL = this.router.currentURL;
   233      } else {
   234        redirectURL = new URL(window.location.toString());
   235        redirectURL.search = '';
   236        redirectURL = redirectURL.href;
   237      }
   238  
   239      const res = await this.token.authorizedRequest(
   240        '/v1/acl/oidc/complete-auth',
   241        {
   242          method: 'POST',
   243          body: JSON.stringify({
   244            AuthMethodName: window.localStorage.getItem('nomadOIDCAuthMethod'),
   245            ClientNonce: window.localStorage.getItem('nomadOIDCNonce'),
   246            Code: this.code,
   247            State: this.state,
   248            RedirectURI: redirectURL,
   249          }),
   250        }
   251      );
   252  
   253      if (res.ok) {
   254        const data = await res.json();
   255        this.clearTokenProperties();
   256        this.token.set('secret', data.SecretID);
   257        this.state = null;
   258        this.code = null;
   259  
   260        // Refetch the token and associated policies
   261        this.token.get('fetchSelfTokenAndPolicies').perform().catch();
   262  
   263        this.signInStatus = 'success';
   264        this.token.set('tokenNotFound', false);
   265      } else {
   266        this.state = 'failure';
   267        this.code = null;
   268      }
   269    }
   270  
   271    get SSOFailure() {
   272      return this.state === 'failure';
   273    }
   274  
   275    get canSignIn() {
   276      return !this.tokenRecord || this.tokenRecord.isExpired;
   277    }
   278  
   279    get shouldShowPolicies() {
   280      return this.tokenRecord;
   281    }
   282  }