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 }