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 }