github.com/hernad/nomad@v1.6.112/ui/app/abilities/variable.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  // @ts-check
     7  import { computed, get } from '@ember/object';
     8  import { or } from '@ember/object/computed';
     9  import AbstractAbility from './abstract';
    10  import doesMatchPattern from 'nomad-ui/utils/match-glob';
    11  
    12  const WILDCARD_GLOB = '*';
    13  const WILDCARD_PATTERN = '/';
    14  const GLOBAL_FLAG = 'g';
    15  const WILDCARD_PATTERN_REGEX = new RegExp(WILDCARD_PATTERN, GLOBAL_FLAG);
    16  
    17  export default class Variable extends AbstractAbility {
    18    // Pass in a namespace to `can` or `cannot` calls to override
    19    // https://github.com/minutebase/ember-can#additional-attributes
    20    path = '*';
    21  
    22    get _path() {
    23      if (!this.path) return '*';
    24      return this.path;
    25    }
    26  
    27    @or(
    28      'bypassAuthorization',
    29      'selfTokenIsManagement',
    30      'policiesSupportVariableList'
    31    )
    32    canList;
    33  
    34    @or(
    35      'bypassAuthorization',
    36      'selfTokenIsManagement',
    37      'policiesSupportVariableWriting'
    38    )
    39    canWrite;
    40  
    41    @or(
    42      'bypassAuthorization',
    43      'selfTokenIsManagement',
    44      'policiesSupportVariableDestroy'
    45    )
    46    canDestroy;
    47  
    48    @or(
    49      'bypassAuthorization',
    50      'selfTokenIsManagement',
    51      'policiesSupportVariableRead'
    52    )
    53    canRead;
    54  
    55    @computed('token.selfTokenPolicies')
    56    get policiesSupportVariableList() {
    57      return this.policyNamespacesIncludeVariablesCapabilities(
    58        this.token.selfTokenPolicies,
    59        ['list', 'read', 'write', 'destroy']
    60      );
    61    }
    62  
    63    @computed('path', 'allPaths')
    64    get policiesSupportVariableRead() {
    65      const matchingPath = this._nearestMatchingPath(this.path);
    66      return this.allPaths
    67        .find((path) => path.name === matchingPath)
    68        ?.capabilities?.includes('read');
    69    }
    70  
    71    /**
    72     *
    73     * Map to your policy's namespaces,
    74     * and each of their Variables blocks' paths,
    75     * and each of their capabilities.
    76     * Then, check to see if any of the permissions you're looking for
    77     * are contained within at least one of them.
    78     *
    79     * @param {Object} policies
    80     * @param {string[]} capabilities
    81     * @returns {boolean}
    82     */
    83    policyNamespacesIncludeVariablesCapabilities(
    84      policies = [],
    85      capabilities = [],
    86      path
    87    ) {
    88      const namespacesWithVariableCapabilities = policies
    89        .toArray()
    90        .filter((policy) => get(policy, 'rulesJSON.Namespaces'))
    91        .map((policy) => get(policy, 'rulesJSON.Namespaces'))
    92        .flat()
    93        .map((namespace = {}) => {
    94          return namespace.Variables?.Paths;
    95        })
    96        .flat()
    97        .compact()
    98        .filter((varsBlock = {}) => {
    99          if (!path || path === WILDCARD_GLOB) {
   100            return true;
   101          } else {
   102            return varsBlock.PathSpec === path;
   103          }
   104        })
   105        .map((varsBlock = {}) => {
   106          return varsBlock.Capabilities;
   107        })
   108        .flat()
   109        .compact();
   110  
   111      // Check for requested permissions
   112      return namespacesWithVariableCapabilities.some((abilityList) => {
   113        return capabilities.includes(abilityList);
   114      });
   115    }
   116  
   117    @computed('allPaths', 'namespace', 'path', 'token.selfTokenPolicies')
   118    get policiesSupportVariableWriting() {
   119      if (this.namespace === WILDCARD_GLOB && this.path === WILDCARD_GLOB) {
   120        // If you're checking if you can write from root, and you don't specify a namespace,
   121        // Then if you can write in ANY path in ANY namespace, you can get to /new.
   122        return this.policyNamespacesIncludeVariablesCapabilities(
   123          this.token.selfTokenPolicies,
   124          ['write'],
   125          this._nearestMatchingPath(this.path)
   126        );
   127      } else {
   128        // Checking a specific path in a specific namespace.
   129        // TODO: This doesn't cover the case when you're checking for the * namespace at a specific path.
   130        // Right now we require you to specify yournamespace to enable the button.
   131        const matchingPath = this._nearestMatchingPath(this.path);
   132        return this.allPaths
   133          .find((path) => path.name === matchingPath)
   134          ?.capabilities?.includes('write');
   135      }
   136    }
   137  
   138    @computed('path', 'allPaths')
   139    get policiesSupportVariableDestroy() {
   140      const matchingPath = this._nearestMatchingPath(this.path);
   141      return this.allPaths
   142        .find((path) => path.name === matchingPath)
   143        ?.capabilities?.includes('destroy');
   144    }
   145  
   146    @computed('token.selfTokenPolicies.[]', 'namespace')
   147    get allPaths() {
   148      return (get(this, 'token.selfTokenPolicies') || [])
   149        .toArray()
   150        .reduce((paths, policy) => {
   151          const namespaces = get(policy, 'rulesJSON.Namespaces');
   152          const matchingNamespace = this._nearestMatchingNamespace(
   153            namespaces,
   154            this.namespace
   155          );
   156  
   157          const variables = (namespaces || []).find(
   158            (namespace) => namespace.Name === matchingNamespace
   159          )?.Variables;
   160  
   161          const pathNames = variables?.Paths?.map((path) => ({
   162            name: path.PathSpec,
   163            capabilities: path.Capabilities,
   164          }));
   165  
   166          if (pathNames) {
   167            paths = [...paths, ...pathNames];
   168          }
   169  
   170          return paths;
   171        }, []);
   172    }
   173  
   174    _nearestMatchingNamespace(policyNamespaces, namespace) {
   175      if (!namespace || !policyNamespaces) return 'default';
   176  
   177      return this._findMatchingNamespace(policyNamespaces, namespace);
   178    }
   179  
   180    _formatMatchingPathRegEx(path, wildCardPlacement = 'end') {
   181      const replacer = () => '\\/';
   182      if (wildCardPlacement === 'end') {
   183        const trimmedPath = path.slice(0, path.length - 1);
   184        const pattern = trimmedPath.replace(WILDCARD_PATTERN_REGEX, replacer);
   185        return pattern;
   186      } else {
   187        const trimmedPath = path.slice(1, path.length);
   188        const pattern = trimmedPath.replace(WILDCARD_PATTERN_REGEX, replacer);
   189        return pattern;
   190      }
   191    }
   192  
   193    _computeAllMatchingPaths(pathNames, path) {
   194      const matches = [];
   195  
   196      for (const pathName of pathNames) {
   197        if (doesMatchPattern(pathName, path)) matches.push(pathName);
   198      }
   199  
   200      return matches;
   201    }
   202  
   203    _nearestMatchingPath(path) {
   204      const pathNames = this.allPaths.map((path) => path.name);
   205      if (pathNames.includes(path)) {
   206        return path;
   207      }
   208  
   209      const allMatchingPaths = this._computeAllMatchingPaths(pathNames, path);
   210  
   211      if (!allMatchingPaths.length) return WILDCARD_GLOB;
   212  
   213      return this._smallestDifference(allMatchingPaths, path);
   214    }
   215  
   216    _computeLengthDiff(pattern, path) {
   217      const countGlobsInPattern = pattern
   218        ?.split('')
   219        .filter((el) => el === WILDCARD_GLOB).length;
   220  
   221      return path?.length - pattern?.length + countGlobsInPattern;
   222    }
   223  
   224    _smallestDifference(matches, path) {
   225      const sortingCallBack = (patternA, patternB) =>
   226        this._computeLengthDiff(patternA, path) -
   227        this._computeLengthDiff(patternB, path);
   228  
   229      const sortedMatches = matches?.sort(sortingCallBack);
   230      const isTie =
   231        this._computeLengthDiff(sortedMatches[0], path) ===
   232        this._computeLengthDiff(sortedMatches[1], path);
   233      const doesFirstMatchHaveLeadingGlob = sortedMatches[0][0] === WILDCARD_GLOB;
   234  
   235      return isTie && doesFirstMatchHaveLeadingGlob
   236        ? sortedMatches[1]
   237        : sortedMatches[0];
   238    }
   239  }