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

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