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  }