github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libpages/config/per_path_configs_v1.go (about) 1 // Copyright 2017 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package config 6 7 import ( 8 "errors" 9 "path" 10 "strings" 11 ) 12 13 const ( 14 // PermRead is the read permission. 15 PermRead = "read" 16 // PermList is the list permission. 17 PermList = "list" 18 // PermReadAndList allows both read and list. 19 PermReadAndList = "read,list" 20 ) 21 22 // PerPathConfigV1 defines a per-path configuration structure, including an 23 // access control list (ACL) for the V1 config. 24 type PerPathConfigV1 struct { 25 // WhitelistAdditionalPermissions is a map of username -> permissions that 26 // defines a list of additional permissions that authenticated users have 27 // in addition to AnonymousPermissions. 28 WhitelistAdditionalPermissions map[string]string `json:"whitelist_additional_permissions"` 29 // AnonymousPermissions is the permissions for 30 // unauthenticated/anonymous requests. 31 AnonymousPermissions string `json:"anonymous_permissions"` 32 33 // AccessControlAllowOrigin, if set, causes the setting of the 34 // Access-Control-Allow-Origin header when serving requests under the 35 // corresponding path. 36 AccessControlAllowOrigin string `json:"Access-Control-Allow-Origin,omitempty"` 37 // Custom403Forbidden specifies a path (relative to site root) to a html 38 // file to be served when 403 errors happen. 39 Custom403Forbidden string `json:"custom_403_forbidden,omitempty"` 40 // Custom404NotFound specifies a path (relative to site root) to a html 41 // file to be served when 404 errors happen. 42 Custom404NotFound string `json:"custom_404_not_found,omitempty"` 43 } 44 45 // permissionsV1 is the parsed version of a permission string. 46 type permissionsV1 struct { 47 read bool 48 list bool 49 } 50 51 func parsePermissionsV1(permsStr string) (permissionsV1, error) { 52 permsStr = strings.TrimSpace(permsStr) 53 var perms permissionsV1 54 if len(permsStr) == 0 { 55 return perms, nil 56 } 57 for _, p := range strings.Split(permsStr, ",") { 58 switch p { 59 case PermRead: 60 perms.read = true 61 case PermList: 62 perms.list = true 63 default: 64 return permissionsV1{}, ErrInvalidPermissions{ 65 permissions: permsStr} 66 } 67 } 68 return perms, nil 69 } 70 71 // perPathConfigV1 is the parsed version of PerPathConfigV1. It can be used 72 // directly with no parsing errors by the perPathConfigsReaderV1. 73 type perPathConfigV1 struct { 74 // whitelistAdditional is the internal version of 75 // PerPathConfigV1.WhitelistAdditionalPermissions. See comment of latter 76 // for more details. It's a map of username -> permissionsV1. 77 whitelistAdditional map[string]permissionsV1 78 anonymous permissionsV1 79 // maxPermission stores the most permissive permission that either an 80 // anonymous or an authenticated user can get for this path. Note that this 81 // doesn't necessarily mean there's a user able to get exactly this 82 // permission, since it's possible to have a user with `read` and a user 83 // with `list`, causing maxPermission to be {read: true, list: true} but 84 // never a user getting both. 85 maxPermission permissionsV1 86 // p stores the path (from Config declaration) that an *perPathConfigV1 87 // object is constructed for. When an *perPathConfigsReaderV1 is picked for a path, 88 // the p field can be used as a realm for HTTP Basic Authentication. 89 p string 90 91 accessControlAllowOrigin string 92 custom403Forbidden string 93 custom404NotFound string 94 } 95 96 func checkCors(acao string) (cleaned string, err error) { 97 cleaned = strings.TrimSpace(acao) 98 if cleaned != "" && cleaned != "*" { 99 // TODO: support setting non-wildcard origins. Note that none wildcard 100 // ones need a Vary header too. 101 return "", ErrInvalidConfig{msg: "only \"*\" is supported as " + 102 "non-empty Access-Control-Allow-Origin for now"} 103 } 104 return cleaned, nil 105 } 106 107 func checkCustomPagePath(p string) (cleaned string, err error) { 108 if len(p) == 0 { 109 return "", nil 110 } 111 cleaned = path.Clean(p) 112 if strings.HasPrefix(cleaned, "..") { 113 return "", ErrInvalidConfig{"invalid custom page path: " + p} 114 } 115 return cleaned, nil 116 } 117 118 // makePerPathConfigV1Internal makes an *perPathConfigV1 out of an 119 // *PerPathConfigV1. The users map is used to check if every username defined 120 // in WhitelistAdditionalPermissions is defined. 121 func makePerPathConfigV1Internal( 122 a *PerPathConfigV1, users map[string]string, p string) ( 123 ac *perPathConfigV1, err error) { 124 if a == nil { 125 return nil, errors.New("nil PerPathConfigV1") 126 } 127 ac = &perPathConfigV1{p: p} 128 ac.anonymous, err = parsePermissionsV1(a.AnonymousPermissions) 129 if err != nil { 130 return nil, err 131 } 132 ac.maxPermission = ac.anonymous 133 for username, permissions := range a.WhitelistAdditionalPermissions { 134 if _, ok := users[username]; !ok { 135 return nil, ErrUndefinedUsername{username: username} 136 } 137 if ac.whitelistAdditional == nil { 138 ac.whitelistAdditional = make(map[string]permissionsV1) 139 } 140 parsedPermissions, err := parsePermissionsV1(permissions) 141 if err != nil { 142 return nil, err 143 } 144 ac.whitelistAdditional[username] = parsedPermissions 145 ac.maxPermission.read = ac.maxPermission.read || parsedPermissions.read 146 ac.maxPermission.list = ac.maxPermission.list || parsedPermissions.list 147 } 148 149 if ac.accessControlAllowOrigin, err = checkCors( 150 a.AccessControlAllowOrigin); err != nil { 151 return nil, err 152 } 153 if ac.custom403Forbidden, err = checkCustomPagePath( 154 a.Custom403Forbidden); err != nil { 155 return nil, err 156 } 157 if ac.custom404NotFound, err = checkCustomPagePath( 158 a.Custom404NotFound); err != nil { 159 return nil, err 160 } 161 162 return ac, nil 163 } 164 165 func emptyPerPathConfigV1InternalForRoot() *perPathConfigV1 { 166 return &perPathConfigV1{ 167 anonymous: permissionsV1{}, // no permission 168 p: "/", 169 } 170 } 171 172 type perPathConfigsReaderV1 struct { 173 children map[string]*perPathConfigsReaderV1 174 // ac, if not nil, defines the access control that should be applied to the 175 // path that the *perPathConfigsReaderV1 represents. If it's nil, it means no 176 // specific access control is defined for the path, and the object exists 177 // most likely for the purpose of the children field to realy to checkers 178 // under this path. In this case, the parent's access control is the 179 // effective one for this path. 180 ac *perPathConfigV1 181 } 182 183 // cleanPath cleans p in by first calling path.Clean, then removing any leading 184 // and trailing "/". 185 // 186 // If p represents root, an empty string is returned. 187 // 188 // Use cleanPathAndSplit2 or cleanPathAndSplit to further split the path. 189 func cleanPath(p string) string { 190 p = path.Clean(p) 191 if p == "/" || p == "." { 192 // Examples of p (before clean) that lead us here: 193 // "" 194 // "/" 195 // "." 196 // "/.." 197 // "/." 198 return "" 199 } 200 // After the trim, p can only be form of "a/b/c", i.e., no leading or 201 // trailing "/". Examples: 202 // "a" 203 // "a/b" 204 // "a/b/c" 205 return strings.Trim(p, "/") 206 } 207 208 // cleanPathAndSplit calls cleanPath on p and splits the result using separator 209 // "/", into a slice consisting of at least 1 element. 210 func cleanPathAndSplit(p string) (elems []string) { 211 return strings.Split(cleanPath(p), "/") 212 } 213 214 // cleanPathAndSplit2 calls cleanPath on p and splits the result using 215 // separator "/", into a slice of either 1 or 2 elements. 216 func cleanPathAndSplit2(p string) (elems []string) { 217 return strings.SplitN(cleanPath(p), "/", 2) 218 } 219 220 // getPerPathConfig gets the corresponding perPathConfigV1 for p. It walks 221 // along the children field recursively. If a specifically defined one exists, 222 // it's returned. Otherwise the parent's (parentAC) is returned. 223 func (c *perPathConfigsReaderV1) getPerPathConfig( 224 parentAC *perPathConfigV1, p string) (ac *perPathConfigV1) { 225 effectiveAC := c.ac 226 if c.ac == nil { 227 // If c.ac == nil, it means user didn't specify a config for the 228 // path that c represents. So just inherit from the parent. 229 effectiveAC = parentAC 230 } 231 elems := cleanPathAndSplit2(p) 232 if len(elems[0]) == 0 || c.children == nil { 233 // Either what we are looking for is exactly what c represents, or c 234 // doesn't have any children *perPathConfigsReaderV1's. Either way, c should be 235 // the checker that controls the path p, so we can just returned the 236 // current effectiveAC. 237 return effectiveAC 238 } 239 // See if we have a sub-checker for the next element in the path p. 240 if subChecker, ok := c.children[elems[0]]; ok { 241 if len(elems) > 1 { 242 // There are more elements in the path p, so ask the sub-checker 243 // for check for the rest. 244 return subChecker.getPerPathConfig(effectiveAC, elems[1]) 245 } 246 // The sub-checker is what we need in order to know get the 247 // *perPathConfigV1 for path p, so call it with "." to indicate that. 248 return subChecker.getPerPathConfig(effectiveAC, ".") 249 } 250 251 // We don't have a sub-checker for the next element in the path p, so just 252 // use the current effectiveAC like the `len(elems[0]) == 0 || c.children 253 // == nil` above. 254 return effectiveAC 255 } 256 257 // getPermissions returns the permissions that username has on p. This method 258 // should only be called on the root perPathConfigsReaderV1. 259 func (c *perPathConfigsReaderV1) getPermissions(p string, username *string) ( 260 permissions permissionsV1, max permissionsV1, effectivePath string) { 261 // This is only called on the root perPathConfigsReaderV1, and c.ac is always 262 // populated here. So even if no other path shows up in the per-path 263 // configs, any path will get root's *perPathConfigV1 as the last resort. 264 ac := c.getPerPathConfig(nil, p) 265 permissions = ac.anonymous 266 if ac.whitelistAdditional == nil || username == nil { 267 return permissions, ac.maxPermission, ac.p 268 } 269 if perms, ok := ac.whitelistAdditional[*username]; ok { 270 permissions.read = perms.read || permissions.read 271 permissions.list = perms.list || permissions.list 272 } 273 return permissions, ac.maxPermission, ac.p 274 } 275 276 func (c *perPathConfigsReaderV1) getSetAccessControlAllowOrigin(p string) (setting string) { 277 ac := c.getPerPathConfig(nil, p) 278 return ac.accessControlAllowOrigin 279 } 280 281 // makePerPathConfigsReaderV1 makes an *perPathConfigsReaderV1 out of 282 // user-defined per-path configs. It recursively constructs nested 283 // *perPathConfigsReaderV1 so that each defined path has a corresponding 284 // checker, and all intermediate nodes have a checker populated. 285 func makePerPathConfigsReaderV1(configs map[string]PerPathConfigV1, 286 users map[string]string) (*perPathConfigsReaderV1, error) { 287 root := &perPathConfigsReaderV1{ac: emptyPerPathConfigV1InternalForRoot()} 288 if configs == nil { 289 return root, nil 290 } 291 // path -> *PerPathConfigV1 292 cleaned := make(map[string]*PerPathConfigV1) 293 294 // Make sure there's no duplicate paths. 295 for p := range configs { 296 // We are doing a separate declaration here instead of in the for 297 // statement above because we need to take the address of ac for each 298 // element in configs, declarations in the for statement don't change 299 // address. 300 ac := configs[p] 301 cleanedPath := path.Clean(p) 302 if cleanedPath == "." { 303 // Override "." with "/" since they both represent the site root. 304 cleanedPath = "/" 305 } 306 if _, ok := cleaned[cleanedPath]; ok { 307 return nil, ErrDuplicatePerPathConfigPath{cleanedPath: cleanedPath} 308 } 309 cleaned[cleanedPath] = &ac 310 } 311 312 // Iterate through the cleaned slice, and construct *perPathConfigsReaderV1 objects 313 // along each path. 314 for p, a := range cleaned { 315 ac, err := makePerPathConfigV1Internal(a, users, p) 316 if err != nil { 317 return nil, err 318 } 319 320 elems := cleanPathAndSplit(p) 321 if len(elems[0]) == 0 { 322 root.ac = ac 323 continue 324 } 325 326 c := root 327 // Construct perPathConfigsReaderV1 objects along the path if needed. 328 for _, elem := range elems { 329 if c.children == nil { 330 // path element -> *perPathConfigsReaderV1 331 c.children = make(map[string]*perPathConfigsReaderV1) 332 } 333 if c.children[elem] == nil { 334 // Intentionally leave the ac field empty so if no config is 335 // specified for this directory we'd use the one from its 336 // parent (see getPerPathConfig). 337 c.children[elem] = &perPathConfigsReaderV1{} 338 } 339 c = c.children[elem] 340 } 341 // Now that c points the the *perPathConfigsReaderV1 that represents the path p, 342 // populate c.ac for it. 343 c.ac = ac 344 } 345 346 return root, nil 347 }