1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
|
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Package patterns provides types and functions for working with path patterns
// for request rules related to AppArmor Prompting.
package patterns
import (
"encoding/json"
"errors"
"fmt"
"strings"
doublestar "github.com/bmatcuk/doublestar/v4"
prompting_errors "github.com/snapcore/snapd/interfaces/prompting/errors"
)
var ErrNoPatterns = errors.New("cannot establish precedence: no patterns given")
// Limit the number of expanded path patterns for a particular pattern.
// When fully expanded, the number of patterns for a given unexpanded pattern
// may not exceed this limit.
const maxExpandedPatterns = 1000
// PathPattern is an iterator which yields expanded path patterns.
type PathPattern struct {
original string
renderTree renderNode
}
// ParsePathPattern validates the given pattern and parses it into a PathPattern
// from which expanded path patterns can be iterated, and returns it.
func ParsePathPattern(pattern string) (*PathPattern, error) {
pathPattern := &PathPattern{}
if err := pathPattern.parse(pattern); err != nil {
return nil, err
}
return pathPattern, nil
}
// parse validates the given pattern and parses it into a PathPattern from
// which expanded path patterns can be iterated, overwriting the receiver.
func (p *PathPattern) parse(pattern string) error {
tokens, err := scan(pattern)
if err != nil {
return prompting_errors.NewInvalidPathPatternError(pattern, err.Error())
}
tree, err := parse(tokens)
if err != nil {
return prompting_errors.NewInvalidPathPatternError(pattern, err.Error())
}
if count := tree.NumVariants(); count > maxExpandedPatterns {
return prompting_errors.NewInvalidPathPatternError(pattern, fmt.Sprintf("exceeded maximum number of expanded path patterns (%d): %d", maxExpandedPatterns, count))
}
p.original = pattern
p.renderTree = tree
return nil
}
// Match returns true if the path pattern matches the given path.
func (p *PathPattern) Match(path string) (bool, error) {
return PathPatternMatches(p.original, path)
}
func (p *PathPattern) String() string {
return p.original
}
// MarshalJSON implements json.Marshaller for PathPattern.
func (p *PathPattern) MarshalJSON() ([]byte, error) {
return json.Marshal(p.original)
}
// UnmarshalJSON implements json.Unmarshaller for PathPattern.
func (p *PathPattern) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
return p.parse(s)
}
// NumVariants returns the total number of expanded path patterns for the
// given path pattern.
func (p *PathPattern) NumVariants() int {
return p.renderTree.NumVariants()
}
// RenderAllVariants enumerates every alternative for each group in the path
// pattern and renders each one into a PatternVariant which is then passed into
// the given observe closure, along with the index of the variant.
//
// The given observe closure should perform some action with the rendered
// variant, such as adding it to a data structure.
func (p *PathPattern) RenderAllVariants(observe func(index int, variant PatternVariant)) {
renderAllVariants(p.renderTree, observe)
}
// PathPatternMatches returns true if the given pattern matches the given path.
//
// Paths to directories are received with trailing slashes, but we don't want
// to require the user to include a trailing '/' if they want to match
// directories (and end their pattern with `{,/}` if they want to match both
// directories and non-directories). Thus, we want to ensure that patterns
// without trailing slashes match paths with trailing slashes. However,
// patterns with trailing slashes should not match paths without trailing
// slashes.
//
// The doublestar package (v4.6.1) has special cases for patterns ending in
// `/**` and `/**/`: `/foo/**`, and `/foo/**/` both match `/foo` and `/foo/`.
// We want to override this behavior to make `/foo/**/` not match `/foo`.
// We also want to override doublestar to make `/foo` match `/foo/`.
func PathPatternMatches(pattern string, path string) (bool, error) {
// If pattern ends in '/', it only matches paths which also end in '/'
if strings.HasSuffix(pattern, "/") && !strings.HasSuffix(path, "/") {
return false, nil
}
matched, err := doublestar.Match(pattern, path)
if err != nil {
// Pattern should not be malformed, since it was rendered internally
return false, err
}
if matched {
return true, nil
}
// If pattern already has trailing '/', don't try to match with additional
// trailing '/'.
if strings.HasSuffix(pattern, "/") {
return false, nil
}
// Try again with a '/' appended to the pattern, so patterns like `/foo`
// match paths like `/foo/`.
return doublestar.Match(pattern+"/", path)
}
// HighestPrecedencePattern determines which of the given path patterns is the
// most specific, and thus has the highest precedence.
//
// Precedence is only defined between patterns which match the same path.
// Precedence is determined according to which pattern places the earliest and
// greatest restriction on the path.
func HighestPrecedencePattern(patterns []PatternVariant, matchingPath string) (PatternVariant, error) {
switch len(patterns) {
case 0:
return PatternVariant{}, ErrNoPatterns
case 1:
return patterns[0], nil
}
currHighest := patterns[0]
for _, contender := range patterns[1:] {
result, err := currHighest.Compare(contender, matchingPath)
if err != nil {
return PatternVariant{}, err
}
if result < 0 {
currHighest = contender
}
}
return currHighest, nil
}
|