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 }