Skip to content

Commit

Permalink
Add support to parameterize unauthenticated paths (#12668)
Browse files Browse the repository at this point in the history
* store unauthenticated path wildcards in map

* working unauthenticated paths with basic unit tests

* refactor wildcard logic

* add parseUnauthenticatedPaths unit tests

* use parseUnauthenticatedPaths when reloading backend

* add more wildcard test cases

* update special paths doc; add changelog

* remove buggy prefix check; add test cases

* prevent false positives for prefix matches

If we ever encounter a mismatched segment, break and set a flag to
prevent false positives for prefix matches.

If it is a match we need to do a prefix check. But we should not return
unless HasPrefix also evaluates to true. Otherwise we should let the for
loop continue to check other possibilities and only return false once
all wildcard paths have been evaluated.

* refactor switch and add more test cases

* remove comment leftover from debug session

* add more wildcard path validation and test cases

* update changelong; feature -> improvement

* simplify wildcard segment matching logic

* refactor wildcard matching into func

* fix glob matching, add more wildcard validation, refactor

* refactor common wildcard errors to func

* move doc comment to logical.Paths

* optimize wildcard paths storage with pre-split slices

* fix comment typo

* fix test case after changing wildcard paths storage type

* move prefix check to parseUnauthenticatedPaths

* tweak regex, remove unneeded array copy, refactor

* add test case around wildcard and glob matching
  • Loading branch information
fairclothjm committed Oct 13, 2021
1 parent e0bfb73 commit 56c6f3c
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 15 deletions.
3 changes: 3 additions & 0 deletions changelog/12668.txt
@@ -0,0 +1,3 @@
```release-note:improvement
sdk/framework: The '+' wildcard is now supported for parameterizing unauthenticated paths.
```
6 changes: 2 additions & 4 deletions sdk/framework/backend.go
Expand Up @@ -41,10 +41,8 @@ type Backend struct {
// paths, including adding or removing, is not allowed once the
// backend is in use).
//
// PathsSpecial is the list of path patterns that denote the
// paths above that require special privileges. These can't be
// regular expressions, it is either exact match or prefix match.
// For prefix match, append '*' as a suffix.
// PathsSpecial is the list of path patterns that denote the paths above
// that require special privileges.
Paths []*Path
PathsSpecial *logical.Paths

Expand Down
4 changes: 4 additions & 0 deletions sdk/logical/logical.go
Expand Up @@ -117,6 +117,10 @@ type Paths struct {
Root []string

// Unauthenticated are the paths that can be accessed without any auth.
// These can't be regular expressions, it is either exact match, a prefix
// match and/or a wildcard match. For prefix match, append '*' as a suffix.
// For a wildcard match, use '+' in the segment to match any identifier
// (e.g. 'foo/+/bar'). Note that '+' can't be adjacent to a non-slash.
Unauthenticated []string

// LocalStorage are paths (prefixes) that are local to this instance; this
Expand Down
2 changes: 2 additions & 0 deletions vault/identity_store.go
Expand Up @@ -89,6 +89,8 @@ func NewIdentityStore(ctx context.Context, core *Core, config *logical.BackendCo
PathsSpecial: &logical.Paths{
Unauthenticated: []string{
"oidc/.well-known/*",
"oidc/provider/+/.well-known/*",
"oidc/provider/+/token",
},
},
PeriodicFunc: func(ctx context.Context, req *logical.Request) error {
Expand Down
6 changes: 5 additions & 1 deletion vault/plugin_reload.go
Expand Up @@ -188,7 +188,11 @@ func (c *Core) reloadBackendCommon(ctx context.Context, entry *MountEntry, isAut
paths := backend.SpecialPaths()
if paths != nil {
re.rootPaths.Store(pathsToRadix(paths.Root))
re.loginPaths.Store(pathsToRadix(paths.Unauthenticated))
loginPathsEntry, err := parseUnauthenticatedPaths(paths.Unauthenticated)
if err != nil {
return err
}
re.loginPaths.Store(loginPathsEntry)
}
}

Expand Down
139 changes: 129 additions & 10 deletions vault/router.go
Expand Up @@ -3,6 +3,7 @@ package vault
import (
"context"
"fmt"
"regexp"
"strings"
"sync"
"sync/atomic"
Expand All @@ -22,6 +23,9 @@ var deniedPassthroughRequestHeaders = []string{
consts.AuthHeaderName,
}

// matches when '+' is next to a non-slash char
var wcAdjacentNonSlashRegEx = regexp.MustCompile(`\+[^/]|[^/]\+`).MatchString

// Router is used to do prefix based routing of a request to a logical backend
type Router struct {
l sync.RWMutex
Expand Down Expand Up @@ -59,6 +63,19 @@ type routeEntry struct {
l sync.RWMutex
}

type wildcardPath struct {
// this sits in the hot path of requests so we are micro-optimizing by
// storing pre-split slices of path segments
segments []string
isPrefix bool
}

// loginPathsEntry is used to hold the routeEntry loginPaths
type loginPathsEntry struct {
paths *radix.Tree
wildcardPaths []wildcardPath
}

type ValidateMountResponse struct {
MountType string `json:"mount_type" structs:"mount_type" mapstructure:"mount_type"`
MountAccessor string `json:"mount_accessor" structs:"mount_accessor" mapstructure:"mount_accessor"`
Expand Down Expand Up @@ -137,7 +154,11 @@ func (r *Router) Mount(backend logical.Backend, prefix string, mountEntry *Mount
storageView: storageView,
}
re.rootPaths.Store(pathsToRadix(paths.Root))
re.loginPaths.Store(pathsToRadix(paths.Unauthenticated))
loginPathsEntry, err := parseUnauthenticatedPaths(paths.Unauthenticated)
if err != nil {
return err
}
re.loginPaths.Store(loginPathsEntry)

switch {
case prefix == "":
Expand Down Expand Up @@ -782,6 +803,10 @@ func (r *Router) RootPath(ctx context.Context, path string) bool {
}

// LoginPath checks if the given path is used for logins
// Matching Priority
// 1. prefix
// 2. exact
// 3. wildcard
func (r *Router) LoginPath(ctx context.Context, path string) bool {
ns, err := namespace.FromContext(ctx)
if err != nil {
Expand All @@ -802,20 +827,114 @@ func (r *Router) LoginPath(ctx context.Context, path string) bool {
remain := strings.TrimPrefix(adjustedPath, mount)

// Check the loginPaths of this backend
loginPaths := re.loginPaths.Load().(*radix.Tree)
match, raw, ok := loginPaths.LongestPrefix(remain)
if !ok {
pe := re.loginPaths.Load().(*loginPathsEntry)
match, raw, ok := pe.paths.LongestPrefix(remain)
if !ok && len(pe.wildcardPaths) == 0 {
// no match found
return false
}
prefixMatch := raw.(bool)

// Handle the prefix match case
if prefixMatch {
return strings.HasPrefix(remain, match)
if ok {
prefixMatch := raw.(bool)
if prefixMatch {
// Handle the prefix match case
return strings.HasPrefix(remain, match)
}
if match == remain {
// Handle the exact match case
return true
}
}

// Handle the exact match case
return match == remain
// check Login Paths containing wildcards
reqPathParts := strings.Split(remain, "/")
for _, w := range pe.wildcardPaths {
if pathMatchesWildcardPath(reqPathParts, w.segments, w.isPrefix) {
return true
}
}
return false
}

// pathMatchesWildcardPath returns true if the path made up of the path slice
// matches the given wildcard path slice
func pathMatchesWildcardPath(path, wcPath []string, isPrefix bool) bool {
if len(wcPath) == 0 {
return false
}

if len(path) < len(wcPath) {
// check if the path coming in is shorter; if so it can't match
return false
}
if !isPrefix && len(wcPath) != len(path) {
// If it's not a prefix we expect the same number of segments
return false
}

for i, wcPathPart := range wcPath {
switch {
case wcPathPart == "+":
case wcPathPart == path[i]:
case isPrefix && i == len(wcPath)-1 && strings.HasPrefix(path[i], wcPathPart):
default:
// we encountered segments that did not match
return false
}
}
return true
}

func wildcardError(path, msg string) error {
return fmt.Errorf("path %q: invalid use of wildcards %s", path, msg)
}

func isValidUnauthenticatedPath(path string) (bool, error) {
switch {
case strings.Count(path, "*") > 1:
return false, wildcardError(path, "(multiple '*' is forbidden)")
case strings.Contains(path, "+*"):
return false, wildcardError(path, "('+*' is forbidden)")
case strings.Contains(path, "*") && path[len(path)-1] != '*':
return false, wildcardError(path, "('*' is only allowed at the end of a path)")
case wcAdjacentNonSlashRegEx(path):
return false, wildcardError(path, "('+' is not allowed next to a non-slash)")
}
return true, nil
}

// parseUnauthenticatedPaths converts a list of special paths to a
// loginPathsEntry
func parseUnauthenticatedPaths(paths []string) (*loginPathsEntry, error) {
var tempPaths []string
tempWildcardPaths := make([]wildcardPath, 0)
for _, path := range paths {
if ok, err := isValidUnauthenticatedPath(path); !ok {
return nil, err
}

if strings.Contains(path, "+") {
// Paths with wildcards are not stored in the radix tree because
// the radix tree does not handle wildcards in the middle of strings.
isPrefix := false
if path[len(path)-1] == '*' {
isPrefix = true
path = path[0 : len(path)-1]
}
// We are micro-optimizing by storing pre-split slices of path segments
wcPath := wildcardPath{segments: strings.Split(path, "/"), isPrefix: isPrefix}
tempWildcardPaths = append(tempWildcardPaths, wcPath)
} else {
// accumulate paths that do not contain wildcards
// to be stored in the radix tree
tempPaths = append(tempPaths, path)
}
}

return &loginPathsEntry{
paths: pathsToRadix(tempPaths),
wildcardPaths: tempWildcardPaths,
}, nil
}

// pathsToRadix converts a list of special paths to a radix tree.
Expand Down

0 comments on commit 56c6f3c

Please sign in to comment.