github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/pgwire/hba/hba.go (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 // Package hba implements an hba.conf parser. 12 package hba 13 14 // conf.rl is a ragel v6.10 file containing a parser for pg_hba.conf 15 // files. "make" should be executed in this directory when conf.rl is 16 // changed. Since it is changed so rarely it is not hooked up to the top-level 17 // Makefile since that would require ragel being a dev dependency, which is 18 // an annoying burden since it's written in C and we can't auto install it 19 // on all systems. 20 21 import ( 22 "fmt" 23 "net" 24 "reflect" 25 "strings" 26 27 "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" 28 "github.com/cockroachdb/errors" 29 "github.com/olekukonko/tablewriter" 30 ) 31 32 // Conf is a parsed configuration. 33 type Conf struct { 34 Entries []Entry 35 } 36 37 // Entry is a single line of a configuration. 38 type Entry struct { 39 // ConnType is the connection type to match. 40 ConnType ConnType 41 // Database is the list of databases to match. An empty list means 42 // "match any database". 43 Database []String 44 // User is the list of users to match. An empty list means "match 45 // any user". 46 User []String 47 // Address is either AnyAddr, *net.IPNet or (unsupported) String for a hostname. 48 Address interface{} 49 Method String 50 // MethodFn is populated during name resolution of Method. 51 MethodFn interface{} 52 Options [][2]string 53 OptionQuotes []bool 54 // Input is the original configuration line in the HBA configuration string. 55 // This is used for auditing purposes. 56 Input string 57 // Generated is true if the entry was expanded from another. All the 58 // generated entries share the same value for Input. 59 Generated bool 60 } 61 62 // ConnType represents the type of connection matched by a rule. 63 type ConnType int 64 65 const ( 66 // ConnLocal matches unix socket connections. 67 ConnLocal ConnType = 1 << iota 68 // ConnHostNoSSL matches TCP connections without SSL/TLS. 69 ConnHostNoSSL 70 // ConnHostSSL matches TCP connections with SSL/TLS. 71 ConnHostSSL 72 73 // ConnHostAny matches TCP connections with or without SSL/TLS. 74 ConnHostAny = ConnHostNoSSL | ConnHostSSL 75 76 // ConnAny matches any connection type. Used when registering auth 77 // methods. 78 ConnAny = ConnHostAny | ConnLocal 79 ) 80 81 // String implements the fmt.Formatter interface. 82 func (t ConnType) String() string { 83 switch t { 84 case ConnLocal: 85 return "local" 86 case ConnHostNoSSL: 87 return "hostnossl" 88 case ConnHostSSL: 89 return "hostssl" 90 case ConnHostAny: 91 return "host" 92 default: 93 panic("unimplemented") 94 } 95 } 96 97 // String implements the fmt.Formatter interface. 98 func (c Conf) String() string { 99 if len(c.Entries) == 0 { 100 return "# (empty configuration)\n" 101 } 102 var sb strings.Builder 103 sb.WriteString("# Original configuration:\n") 104 for _, e := range c.Entries { 105 if e.Generated { 106 continue 107 } 108 fmt.Fprintf(&sb, "# %s\n", e.Input) 109 } 110 sb.WriteString("#\n# Interpreted configuration:\n") 111 112 table := tablewriter.NewWriter(&sb) 113 table.SetAutoWrapText(false) 114 table.SetReflowDuringAutoWrap(false) 115 table.SetAlignment(tablewriter.ALIGN_LEFT) 116 table.SetBorder(false) 117 table.SetNoWhiteSpace(true) 118 table.SetTrimWhiteSpaceAtEOL(true) 119 table.SetTablePadding(" ") 120 121 row := []string{"# TYPE", "DATABASE", "USER", "ADDRESS", "METHOD", "OPTIONS"} 122 table.Append(row) 123 for _, e := range c.Entries { 124 row[0] = e.ConnType.String() 125 row[1] = e.DatabaseString() 126 row[2] = e.UserString() 127 row[3] = e.AddressString() 128 row[4] = e.Method.String() 129 row[5] = e.OptionsString() 130 table.Append(row) 131 } 132 table.Render() 133 return sb.String() 134 } 135 136 // AnyAddr represents "any address" and is used when parsing "all" for 137 // the "Address" field. 138 type AnyAddr struct{} 139 140 // String implements the fmt.Formatter interface. 141 func (AnyAddr) String() string { return "all" } 142 143 // GetOption returns the value of option name if there is exactly one 144 // occurrence of name in the options list, otherwise the empty string. 145 func (h Entry) GetOption(name string) string { 146 var val string 147 for _, opt := range h.Options { 148 if opt[0] == name { 149 // If there is more than one entry, return empty string. 150 if val != "" { 151 return "" 152 } 153 val = opt[1] 154 } 155 } 156 return val 157 } 158 159 // Equivalent returns true iff the entry is equivalent to another, 160 // excluding the original syntax. 161 func (h Entry) Equivalent(other Entry) bool { 162 h.Input = "" 163 other.Input = "" 164 return reflect.DeepEqual(h, other) 165 } 166 167 // GetOptions returns all values of option name. 168 func (h Entry) GetOptions(name string) []string { 169 var val []string 170 for _, opt := range h.Options { 171 if opt[0] == name { 172 val = append(val, opt[1]) 173 } 174 } 175 return val 176 } 177 178 // ConnTypeMatches returns true iff the provided actual client connection 179 // type matches the connection type specified in the rule. 180 func (h Entry) ConnTypeMatches(clientConn ConnType) bool { 181 switch clientConn { 182 case ConnLocal: 183 return h.ConnType == ConnLocal 184 case ConnHostSSL: 185 // A SSL connection matches both "hostssl" and "host". 186 return h.ConnType&ConnHostSSL != 0 187 case ConnHostNoSSL: 188 // A non-SSL connection matches both "hostnossl" and "host". 189 return h.ConnType&ConnHostNoSSL != 0 190 default: 191 panic("unimplemented") 192 } 193 } 194 195 // ConnMatches returns true iff the provided client connection 196 // type and address matches the entry spec. 197 func (h Entry) ConnMatches(clientConn ConnType, ip net.IP) (bool, error) { 198 if !h.ConnTypeMatches(clientConn) { 199 return false, nil 200 } 201 if clientConn != ConnLocal { 202 return h.AddressMatches(ip) 203 } 204 return true, nil 205 } 206 207 // UserMatches returns true iff the provided username matches the an 208 // entry in the User list or if the user list is empty (the entry 209 // matches all). 210 // 211 // The provided username must be normalized already. 212 // The function assumes the entry was normalized to contain only 213 // one user and its username normalized. See ParseAndNormalize(). 214 func (h Entry) UserMatches(userName string) bool { 215 if h.User == nil { 216 return true 217 } 218 for _, u := range h.User { 219 if u.Value == userName { 220 return true 221 } 222 } 223 return false 224 } 225 226 // AddressMatches returns true iff the provided address matches the 227 // entry. The function assumes the entry was normalized already. 228 // See ParseAndNormalize. 229 func (h Entry) AddressMatches(addr net.IP) (bool, error) { 230 switch a := h.Address.(type) { 231 case AnyAddr: 232 return true, nil 233 case *net.IPNet: 234 return a.Contains(addr), nil 235 default: 236 // This is where name-based validation can occur later. 237 return false, errors.Newf("unknown address type: %T", addr) 238 } 239 } 240 241 // DatabaseString returns a string that describes the database field. 242 func (h Entry) DatabaseString() string { 243 if h.Database == nil { 244 return "all" 245 } 246 var sb strings.Builder 247 comma := "" 248 for _, s := range h.Database { 249 sb.WriteString(comma) 250 sb.WriteString(s.String()) 251 comma = "," 252 } 253 return sb.String() 254 } 255 256 // UserString returns a string that describes the username field. 257 func (h Entry) UserString() string { 258 if h.User == nil { 259 return "all" 260 } 261 var sb strings.Builder 262 comma := "" 263 for _, s := range h.User { 264 sb.WriteString(comma) 265 sb.WriteString(s.String()) 266 comma = "," 267 } 268 return sb.String() 269 } 270 271 // AddressString returns a string that describes the address field. 272 func (h Entry) AddressString() string { 273 if h.Address == nil { 274 // This is possible for conn type "local". 275 return "" 276 } 277 return fmt.Sprintf("%s", h.Address) 278 } 279 280 // OptionsString returns a string that describes the option field. 281 func (h Entry) OptionsString() string { 282 var sb strings.Builder 283 sp := "" 284 for i, opt := range h.Options { 285 sb.WriteString(sp) 286 sb.WriteString(String{Value: opt[0] + "=" + opt[1], Quoted: h.OptionQuotes[i]}.String()) 287 sp = " " 288 } 289 return sb.String() 290 } 291 292 // String implements the fmt.Formatter interface. 293 func (h Entry) String() string { 294 return Conf{Entries: []Entry{h}}.String() 295 } 296 297 // String is a possibly quoted string. 298 type String struct { 299 Value string 300 Quoted bool 301 } 302 303 // String implements the fmt.Formatter interface. 304 func (s String) String() string { 305 if s.Quoted { 306 return `"` + s.Value + `"` 307 } 308 return s.Value 309 } 310 311 // Empty returns true iff s is the unquoted empty string. 312 func (s String) Empty() bool { return s.IsKeyword("") } 313 314 // IsKeyword returns whether s is the non-quoted string v. 315 func (s String) IsKeyword(v string) bool { 316 return !s.Quoted && s.Value == v 317 } 318 319 // ParseAndNormalize parses the HBA configuration from the provided 320 // string and performs two tasks: 321 // 322 // - it unicode-normalizes the usernames. Since usernames are 323 // initialized during pgwire session initialization, this 324 // ensures that string comparisons can be used to match usernames. 325 // 326 // - it ensures there is one entry per username. This simplifies 327 // the code in the authentication logic. 328 // 329 func ParseAndNormalize(val string) (*Conf, error) { 330 conf, err := Parse(val) 331 if err != nil { 332 return nil, err 333 } 334 335 entries := conf.Entries[:0] 336 entriesCopied := false 337 outer: 338 for i := range conf.Entries { 339 entry := conf.Entries[i] 340 341 // The database field is not supported yet in CockroachDB. 342 entry.Database = nil 343 344 // Normalize the 'all' keyword into AnyAddr. 345 if addr, ok := entry.Address.(String); ok && addr.IsKeyword("all") { 346 entry.Address = AnyAddr{} 347 } 348 349 // If we're observing an "any" entry, just keep that and move 350 // along. 351 for _, iu := range entry.User { 352 if iu.IsKeyword("all") { 353 entry.User = nil 354 entries = append(entries, entry) 355 continue outer 356 } 357 } 358 359 // If we're about to change the size of the slice, first copy the 360 // result entries. 361 if len(entry.User) != 1 && !entriesCopied { 362 entries = append([]Entry(nil), conf.Entries[:len(entries)]...) 363 entriesCopied = true 364 } 365 // Expand and normalize the usernames. 366 allUsers := entry.User 367 for userIdx, iu := range allUsers { 368 entry.User = allUsers[userIdx : userIdx+1] 369 entry.User[0].Value = tree.Name(iu.Value).Normalize() 370 if userIdx > 0 { 371 entry.Generated = true 372 } 373 entries = append(entries, entry) 374 } 375 } 376 conf.Entries = entries 377 return conf, nil 378 }