github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  }