github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/roleoption/role_option.go (about)

     1  // Copyright 2020 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 roleoption
    12  
    13  import (
    14  	"strings"
    15  
    16  	"github.com/cockroachdb/cockroach/pkg/security"
    17  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
    18  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
    19  	"github.com/cockroachdb/cockroach/pkg/sql/sqltelemetry"
    20  	"github.com/cockroachdb/errors"
    21  )
    22  
    23  //go:generate stringer -type=Option
    24  
    25  // Option defines a role option. This is output by the parser
    26  type Option uint32
    27  
    28  // RoleOption represents an Option with a value.
    29  type RoleOption struct {
    30  	Option
    31  	HasValue bool
    32  	// Need to resolve value in Exec for the case of placeholders.
    33  	Value func() (bool, string, error)
    34  }
    35  
    36  // KindList of role options.
    37  const (
    38  	_ Option = iota
    39  	CREATEROLE
    40  	NOCREATEROLE
    41  	PASSWORD
    42  	LOGIN
    43  	NOLOGIN
    44  	VALIDUNTIL
    45  )
    46  
    47  // toSQLStmts is a map of Kind -> SQL statement string for applying the
    48  // option to the role.
    49  var toSQLStmts = map[Option]string{
    50  	CREATEROLE:   `UPSERT INTO system.role_options (username, option) VALUES ($1, 'CREATEROLE')`,
    51  	NOCREATEROLE: `DELETE FROM system.role_options WHERE username = $1 AND option = 'CREATEROLE'`,
    52  	LOGIN:        `DELETE FROM system.role_options WHERE username = $1 AND option = 'NOLOGIN'`,
    53  	NOLOGIN:      `UPSERT INTO system.role_options (username, option) VALUES ($1, 'NOLOGIN')`,
    54  	VALIDUNTIL:   `UPSERT INTO system.role_options (username, option, value) VALUES ($1, 'VALID UNTIL', $2::timestamptz::string)`,
    55  }
    56  
    57  // Mask returns the bitmask for a given role option.
    58  func (o Option) Mask() uint32 {
    59  	return 1 << o
    60  }
    61  
    62  // ByName is a map of string -> kind value.
    63  var ByName = map[string]Option{
    64  	"CREATEROLE":   CREATEROLE,
    65  	"NOCREATEROLE": NOCREATEROLE,
    66  	"PASSWORD":     PASSWORD,
    67  	"LOGIN":        LOGIN,
    68  	"NOLOGIN":      NOLOGIN,
    69  	"VALID_UNTIL":  VALIDUNTIL,
    70  }
    71  
    72  // ToOption takes a string and returns the corresponding Option.
    73  func ToOption(str string) (Option, error) {
    74  	ret := ByName[strings.ToUpper(str)]
    75  	if ret == 0 {
    76  		return 0, pgerror.New(pgcode.Syntax, "option does not exist")
    77  	}
    78  
    79  	return ret, nil
    80  }
    81  
    82  // List is a list of role options.
    83  type List []RoleOption
    84  
    85  // GetSQLStmts returns a map of SQL stmts to apply each role option.
    86  // Maps stmts to values (value of the role option).
    87  func (rol List) GetSQLStmts(op string) (map[string]func() (bool, string, error), error) {
    88  	if len(rol) <= 0 {
    89  		return nil, nil
    90  	}
    91  
    92  	stmts := make(map[string]func() (bool, string, error), len(rol))
    93  
    94  	err := rol.CheckRoleOptionConflicts()
    95  	if err != nil {
    96  		return stmts, err
    97  	}
    98  
    99  	for _, ro := range rol {
   100  		sqltelemetry.IncIAMOptionCounter(
   101  			op,
   102  			strings.ToLower(ro.Option.String()),
   103  		)
   104  		// Skip PASSWORD option.
   105  		// Since PASSWORD still resides in system.users, we handle setting PASSWORD
   106  		// outside of this set stmt.
   107  		// TODO(richardjcai): migrate password to system.role_options
   108  		if ro.Option == PASSWORD {
   109  			continue
   110  		}
   111  
   112  		stmt := toSQLStmts[ro.Option]
   113  		if ro.HasValue {
   114  			stmts[stmt] = ro.Value
   115  		} else {
   116  			stmts[stmt] = nil
   117  		}
   118  	}
   119  
   120  	return stmts, nil
   121  }
   122  
   123  // ToBitField returns the bitfield representation of
   124  // a list of role options.
   125  func (rol List) ToBitField() (uint32, error) {
   126  	var ret uint32
   127  	for _, p := range rol {
   128  		if ret&p.Option.Mask() != 0 {
   129  			return 0, pgerror.Newf(pgcode.Syntax, "redundant role options")
   130  		}
   131  		ret |= p.Option.Mask()
   132  	}
   133  	return ret, nil
   134  }
   135  
   136  // Contains returns true if List contains option, false otherwise.
   137  func (rol List) Contains(p Option) bool {
   138  	for _, ro := range rol {
   139  		if ro.Option == p {
   140  			return true
   141  		}
   142  	}
   143  
   144  	return false
   145  }
   146  
   147  // CheckRoleOptionConflicts returns an error if two or more options conflict with each other.
   148  func (rol List) CheckRoleOptionConflicts() error {
   149  	roleOptionBits, err := rol.ToBitField()
   150  
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	if (roleOptionBits&CREATEROLE.Mask() != 0 &&
   156  		roleOptionBits&NOCREATEROLE.Mask() != 0) ||
   157  		(roleOptionBits&LOGIN.Mask() != 0 &&
   158  			roleOptionBits&NOLOGIN.Mask() != 0) {
   159  		return pgerror.Newf(pgcode.Syntax, "conflicting role options")
   160  	}
   161  	return nil
   162  }
   163  
   164  // GetHashedPassword returns the value of the password after hashing it.
   165  // Returns error if no password option is found or if password is invalid.
   166  func (rol List) GetHashedPassword() ([]byte, error) {
   167  	var hashedPassword []byte
   168  	for _, ro := range rol {
   169  		if ro.Option == PASSWORD {
   170  			isNull, password, err := ro.Value()
   171  			if isNull {
   172  				// Use empty byte array for hashedPassword.
   173  				return hashedPassword, nil
   174  			}
   175  			if err != nil {
   176  				return hashedPassword, err
   177  			}
   178  			if password == "" {
   179  				return hashedPassword, security.ErrEmptyPassword
   180  			}
   181  			hashedPassword, err = security.HashPassword(password)
   182  			if err != nil {
   183  				return hashedPassword, err
   184  			}
   185  
   186  			return hashedPassword, nil
   187  		}
   188  	}
   189  	// Password option not found.
   190  	return hashedPassword, errors.New("password not found in role options")
   191  }