go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth/identity/glob.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package identity
    16  
    17  import (
    18  	"fmt"
    19  	"regexp"
    20  	"strings"
    21  	"sync"
    22  )
    23  
    24  // Glob is glob like pattern that matches identity strings of some kind.
    25  //
    26  // It is a string of the form "kind:<pattern>" where 'kind' is one of Kind
    27  // constants and 'pattern' is a wildcard pattern to apply to identity name.
    28  //
    29  // The only supported glob syntax is '*', which matches zero or more characters.
    30  //
    31  // Case sensitive. Doesn't support multi-line strings or patterns. There's no
    32  // way to match '*' itself.
    33  type Glob string
    34  
    35  // MakeGlob ensures 'glob' string looks like a valid identity glob and
    36  // returns it as Glob value.
    37  func MakeGlob(glob string) (Glob, error) {
    38  	g := Glob(glob)
    39  	if err := g.Validate(); err != nil {
    40  		return "", err
    41  	}
    42  	return g, nil
    43  }
    44  
    45  // Validate checks that the identity glob string is well-formed.
    46  func (g Glob) Validate() error {
    47  	chunks := strings.SplitN(string(g), ":", 2)
    48  	if len(chunks) != 2 {
    49  		return fmt.Errorf("auth: bad identity glob string %q", g)
    50  	}
    51  	if KnownKinds[Kind(chunks[0])] == nil {
    52  		return fmt.Errorf("auth: bad identity glob kind %q", chunks[0])
    53  	}
    54  	if chunks[1] == "" {
    55  		return fmt.Errorf("auth: identity glob can't be empty")
    56  	}
    57  	if _, err := cache.translate(chunks[1]); err != nil {
    58  		return fmt.Errorf("auth: bad identity glob pattern %q - %s", chunks[1], err)
    59  	}
    60  	return nil
    61  }
    62  
    63  // Kind returns identity glob kind. If identity glob string is invalid returns
    64  // Anonymous.
    65  func (g Glob) Kind() Kind {
    66  	chunks := strings.SplitN(string(g), ":", 2)
    67  	if len(chunks) != 2 {
    68  		return Anonymous
    69  	}
    70  	return Kind(chunks[0])
    71  }
    72  
    73  // Pattern returns a pattern part of the identity glob. If the identity glob
    74  // string is invalid returns empty string.
    75  func (g Glob) Pattern() string {
    76  	chunks := strings.SplitN(string(g), ":", 2)
    77  	if len(chunks) != 2 {
    78  		return ""
    79  	}
    80  	return chunks[1]
    81  }
    82  
    83  // Match returns true if glob matches an identity. If identity string
    84  // or identity glob string are invalid, returns false.
    85  func (g Glob) Match(id Identity) bool {
    86  	globChunks := strings.SplitN(string(g), ":", 2)
    87  	if len(globChunks) != 2 || KnownKinds[Kind(globChunks[0])] == nil {
    88  		return false
    89  	}
    90  	globKind := globChunks[0]
    91  	pattern := globChunks[1]
    92  
    93  	idChunks := strings.SplitN(string(id), ":", 2)
    94  	if len(idChunks) != 2 || KnownKinds[Kind(idChunks[0])] == nil {
    95  		return false
    96  	}
    97  	idKind := idChunks[0]
    98  	name := idChunks[1]
    99  
   100  	if idKind != globKind {
   101  		return false
   102  	}
   103  	if strings.ContainsRune(name, '\n') {
   104  		return false
   105  	}
   106  
   107  	re, err := cache.translate(pattern)
   108  	if err != nil {
   109  		return false
   110  	}
   111  	return re.MatchString(name)
   112  }
   113  
   114  // Preprocess splits the glob into its kind and a regexp against identity names.
   115  //
   116  // For example "user:*@example.com" => ("user", "^.*@example\.com$"). Returns
   117  // an error if the glob is malformed.
   118  func (g Glob) Preprocess() (kind Kind, regexp string, err error) {
   119  	globChunks := strings.SplitN(string(g), ":", 2)
   120  	if len(globChunks) != 2 || KnownKinds[Kind(globChunks[0])] == nil {
   121  		return "", "", fmt.Errorf("bad identity glob format")
   122  	}
   123  	kind = Kind(globChunks[0])
   124  	regexp, err = translate(globChunks[1])
   125  	return
   126  }
   127  
   128  ////
   129  
   130  var cache patternCache
   131  
   132  // patternCache implements caching for compiled pattern regexps, similar to how
   133  // python does that. Uses extremely dumb algorithm that assumes there are less
   134  // than 500 active patterns in the process. That's how python runtime does it
   135  // too.
   136  type patternCache struct {
   137  	sync.RWMutex
   138  	cache map[string]cacheEntry
   139  }
   140  
   141  type cacheEntry struct {
   142  	re  *regexp.Regexp
   143  	err error
   144  }
   145  
   146  // translate grabs converted regexp from cache or calls 'translate' to get it.
   147  func (c *patternCache) translate(pat string) (*regexp.Regexp, error) {
   148  	c.RLock()
   149  	val, ok := c.cache[pat]
   150  	c.RUnlock()
   151  	if ok {
   152  		return val.re, val.err
   153  	}
   154  
   155  	c.Lock()
   156  	defer c.Unlock()
   157  	if val, ok := c.cache[pat]; ok {
   158  		return val.re, val.err
   159  	}
   160  
   161  	if c.cache == nil || len(c.cache) > 500 {
   162  		c.cache = map[string]cacheEntry{}
   163  	}
   164  
   165  	var re *regexp.Regexp
   166  	reStr, err := translate(pat)
   167  	if err == nil {
   168  		re, err = regexp.Compile(reStr)
   169  	}
   170  
   171  	c.cache[pat] = cacheEntry{re, err}
   172  	return re, err
   173  }
   174  
   175  // translate converts glob pattern to a regular expression string.
   176  func translate(pat string) (string, error) {
   177  	res := "^"
   178  	for _, runeVal := range pat {
   179  		switch runeVal {
   180  		case '\n':
   181  			return "", fmt.Errorf("new lines are not supported in globs")
   182  		case '*':
   183  			res += ".*"
   184  		default:
   185  			res += regexp.QuoteMeta(string(runeVal))
   186  		}
   187  	}
   188  	return res + "$", nil
   189  }