storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/config/identity/ldap/config.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2019 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package ldap
    18  
    19  import (
    20  	"crypto/tls"
    21  	"crypto/x509"
    22  	"errors"
    23  	"fmt"
    24  	"math/rand"
    25  	"net"
    26  	"strings"
    27  	"time"
    28  
    29  	ldap "github.com/go-ldap/ldap/v3"
    30  
    31  	"storj.io/minio/cmd/config"
    32  	"storj.io/minio/pkg/env"
    33  )
    34  
    35  const (
    36  	defaultLDAPExpiry = time.Hour * 1
    37  
    38  	dnDelimiter = ";"
    39  )
    40  
    41  // Config contains AD/LDAP server connectivity information.
    42  type Config struct {
    43  	Enabled bool `json:"enabled"`
    44  
    45  	// E.g. "ldap.minio.io:636"
    46  	ServerAddr string `json:"serverAddr"`
    47  
    48  	// STS credentials expiry duration
    49  	STSExpiryDuration string `json:"stsExpiryDuration"`
    50  
    51  	// Format string for usernames
    52  	UsernameFormat  string   `json:"usernameFormat"`
    53  	UsernameFormats []string `json:"-"`
    54  
    55  	// User DN search parameters
    56  	UserDNSearchBaseDN string `json:"userDNSearchBaseDN"`
    57  	UserDNSearchFilter string `json:"userDNSearchFilter"`
    58  
    59  	// Group search parameters
    60  	GroupSearchBaseDistName  string   `json:"groupSearchBaseDN"`
    61  	GroupSearchBaseDistNames []string `json:"-"`
    62  	GroupSearchFilter        string   `json:"groupSearchFilter"`
    63  
    64  	// Lookup bind LDAP service account
    65  	LookupBindDN       string `json:"lookupBindDN"`
    66  	LookupBindPassword string `json:"lookupBindPassword"`
    67  
    68  	stsExpiryDuration time.Duration // contains converted value
    69  	tlsSkipVerify     bool          // allows skipping TLS verification
    70  	serverInsecure    bool          // allows plain text connection to LDAP server
    71  	serverStartTLS    bool          // allows using StartTLS connection to LDAP server
    72  	isUsingLookupBind bool
    73  	rootCAs           *x509.CertPool
    74  }
    75  
    76  // LDAP keys and envs.
    77  const (
    78  	ServerAddr         = "server_addr"
    79  	STSExpiry          = "sts_expiry"
    80  	LookupBindDN       = "lookup_bind_dn"
    81  	LookupBindPassword = "lookup_bind_password"
    82  	UserDNSearchBaseDN = "user_dn_search_base_dn"
    83  	UserDNSearchFilter = "user_dn_search_filter"
    84  	UsernameFormat     = "username_format"
    85  	GroupSearchFilter  = "group_search_filter"
    86  	GroupSearchBaseDN  = "group_search_base_dn"
    87  	TLSSkipVerify      = "tls_skip_verify"
    88  	ServerInsecure     = "server_insecure"
    89  	ServerStartTLS     = "server_starttls"
    90  
    91  	EnvServerAddr         = "MINIO_IDENTITY_LDAP_SERVER_ADDR"
    92  	EnvSTSExpiry          = "MINIO_IDENTITY_LDAP_STS_EXPIRY"
    93  	EnvTLSSkipVerify      = "MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY"
    94  	EnvServerInsecure     = "MINIO_IDENTITY_LDAP_SERVER_INSECURE"
    95  	EnvServerStartTLS     = "MINIO_IDENTITY_LDAP_SERVER_STARTTLS"
    96  	EnvUsernameFormat     = "MINIO_IDENTITY_LDAP_USERNAME_FORMAT"
    97  	EnvUserDNSearchBaseDN = "MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN"
    98  	EnvUserDNSearchFilter = "MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER"
    99  	EnvGroupSearchFilter  = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER"
   100  	EnvGroupSearchBaseDN  = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN"
   101  	EnvLookupBindDN       = "MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN"
   102  	EnvLookupBindPassword = "MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD"
   103  )
   104  
   105  var removedKeys = []string{
   106  	"username_search_filter",
   107  	"username_search_base_dn",
   108  	"group_name_attribute",
   109  }
   110  
   111  // DefaultKVS - default config for LDAP config
   112  var (
   113  	DefaultKVS = config.KVS{
   114  		config.KV{
   115  			Key:   ServerAddr,
   116  			Value: "",
   117  		},
   118  		config.KV{
   119  			Key:   UsernameFormat,
   120  			Value: "",
   121  		},
   122  		config.KV{
   123  			Key:   UserDNSearchBaseDN,
   124  			Value: "",
   125  		},
   126  		config.KV{
   127  			Key:   UserDNSearchFilter,
   128  			Value: "",
   129  		},
   130  		config.KV{
   131  			Key:   GroupSearchFilter,
   132  			Value: "",
   133  		},
   134  		config.KV{
   135  			Key:   GroupSearchBaseDN,
   136  			Value: "",
   137  		},
   138  		config.KV{
   139  			Key:   STSExpiry,
   140  			Value: "1h",
   141  		},
   142  		config.KV{
   143  			Key:   TLSSkipVerify,
   144  			Value: config.EnableOff,
   145  		},
   146  		config.KV{
   147  			Key:   ServerInsecure,
   148  			Value: config.EnableOff,
   149  		},
   150  		config.KV{
   151  			Key:   ServerStartTLS,
   152  			Value: config.EnableOff,
   153  		},
   154  		config.KV{
   155  			Key:   LookupBindDN,
   156  			Value: "",
   157  		},
   158  		config.KV{
   159  			Key:   LookupBindPassword,
   160  			Value: "",
   161  		},
   162  	}
   163  )
   164  
   165  func getGroups(conn *ldap.Conn, sreq *ldap.SearchRequest) ([]string, error) {
   166  	var groups []string
   167  	sres, err := conn.Search(sreq)
   168  	if err != nil {
   169  		// Check if there is no matching result and return empty slice.
   170  		// Ref: https://ldap.com/ldap-result-code-reference/
   171  		if ldap.IsErrorWithCode(err, 32) {
   172  			return nil, nil
   173  		}
   174  		return nil, err
   175  	}
   176  	for _, entry := range sres.Entries {
   177  		// We only queried one attribute,
   178  		// so we only look up the first one.
   179  		groups = append(groups, entry.DN)
   180  	}
   181  	return groups, nil
   182  }
   183  
   184  func (l *Config) lookupBind(conn *ldap.Conn) error {
   185  	var err error
   186  	if l.LookupBindPassword == "" {
   187  		err = conn.UnauthenticatedBind(l.LookupBindDN)
   188  	} else {
   189  		err = conn.Bind(l.LookupBindDN, l.LookupBindPassword)
   190  	}
   191  	if ldap.IsErrorWithCode(err, 49) {
   192  		return fmt.Errorf("LDAP Lookup Bind user invalid credentials error: %v", err)
   193  	}
   194  	return err
   195  }
   196  
   197  // usernameFormatsBind - Iterates over all given username formats and expects
   198  // that only one will succeed if the credentials are valid. The succeeding
   199  // bindDN is returned or an error.
   200  //
   201  // In the rare case that multiple username formats succeed, implying that two
   202  // (or more) distinct users in the LDAP directory have the same username and
   203  // password, we return an error as we cannot identify the account intended by
   204  // the user.
   205  func (l *Config) usernameFormatsBind(conn *ldap.Conn, username, password string) (string, error) {
   206  	var bindDistNames []string
   207  	var errs = make([]error, len(l.UsernameFormats))
   208  	var successCount = 0
   209  	for i, usernameFormat := range l.UsernameFormats {
   210  		bindDN := fmt.Sprintf(usernameFormat, username)
   211  		// Bind with user credentials to validate the password
   212  		errs[i] = conn.Bind(bindDN, password)
   213  		if errs[i] == nil {
   214  			bindDistNames = append(bindDistNames, bindDN)
   215  			successCount++
   216  		} else if !ldap.IsErrorWithCode(errs[i], 49) {
   217  			return "", fmt.Errorf("LDAP Bind request failed with unexpected error: %v", errs[i])
   218  		}
   219  	}
   220  	if successCount == 0 {
   221  		var errStrings []string
   222  		for _, err := range errs {
   223  			if err != nil {
   224  				errStrings = append(errStrings, err.Error())
   225  			}
   226  		}
   227  		outErr := fmt.Sprintf("All username formats failed due to invalid credentials: %s", strings.Join(errStrings, "; "))
   228  		return "", errors.New(outErr)
   229  	}
   230  	if successCount > 1 {
   231  		successDistNames := strings.Join(bindDistNames, ", ")
   232  		errMsg := fmt.Sprintf("Multiple username formats succeeded - ambiguous user login (succeeded for: %s)", successDistNames)
   233  		return "", errors.New(errMsg)
   234  	}
   235  	return bindDistNames[0], nil
   236  }
   237  
   238  // lookupUserDN searches for the DN of the user given their username. conn is
   239  // assumed to be using the lookup bind service account. It is required that the
   240  // search result in at most one result.
   241  func (l *Config) lookupUserDN(conn *ldap.Conn, username string) (string, error) {
   242  	filter := strings.Replace(l.UserDNSearchFilter, "%s", ldap.EscapeFilter(username), -1)
   243  	searchRequest := ldap.NewSearchRequest(
   244  		l.UserDNSearchBaseDN,
   245  		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
   246  		filter,
   247  		[]string{}, // only need DN, so no pass no attributes here
   248  		nil,
   249  	)
   250  
   251  	searchResult, err := conn.Search(searchRequest)
   252  	if err != nil {
   253  		return "", err
   254  	}
   255  	if len(searchResult.Entries) == 0 {
   256  		return "", fmt.Errorf("User DN for %s not found", username)
   257  	}
   258  	if len(searchResult.Entries) != 1 {
   259  		return "", fmt.Errorf("Multiple DNs for %s found - please fix the search filter", username)
   260  	}
   261  	return searchResult.Entries[0].DN, nil
   262  }
   263  
   264  func (l *Config) searchForUserGroups(conn *ldap.Conn, username, bindDN string) ([]string, error) {
   265  	// User groups lookup.
   266  	var groups []string
   267  	if l.GroupSearchFilter != "" {
   268  		for _, groupSearchBase := range l.GroupSearchBaseDistNames {
   269  			filter := strings.Replace(l.GroupSearchFilter, "%s", ldap.EscapeFilter(username), -1)
   270  			filter = strings.Replace(filter, "%d", ldap.EscapeFilter(bindDN), -1)
   271  			searchRequest := ldap.NewSearchRequest(
   272  				groupSearchBase,
   273  				ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
   274  				filter,
   275  				nil,
   276  				nil,
   277  			)
   278  
   279  			var newGroups []string
   280  			newGroups, err := getGroups(conn, searchRequest)
   281  			if err != nil {
   282  				errRet := fmt.Errorf("Error finding groups of %s: %v", bindDN, err)
   283  				return nil, errRet
   284  			}
   285  
   286  			groups = append(groups, newGroups...)
   287  		}
   288  	}
   289  
   290  	return groups, nil
   291  }
   292  
   293  // LookupUserDN searches for the full DN ang groups of a given username
   294  func (l *Config) LookupUserDN(username string) (string, []string, error) {
   295  	if !l.isUsingLookupBind {
   296  		return "", nil, errors.New("current lookup mode does not support searching for User DN")
   297  	}
   298  
   299  	conn, err := l.Connect()
   300  	if err != nil {
   301  		return "", nil, err
   302  	}
   303  	defer conn.Close()
   304  
   305  	// Bind to the lookup user account
   306  	if err = l.lookupBind(conn); err != nil {
   307  		return "", nil, err
   308  	}
   309  
   310  	// Lookup user DN
   311  	bindDN, err := l.lookupUserDN(conn, username)
   312  	if err != nil {
   313  		errRet := fmt.Errorf("Unable to find user DN: %w", err)
   314  		return "", nil, errRet
   315  	}
   316  
   317  	groups, err := l.searchForUserGroups(conn, username, bindDN)
   318  	if err != nil {
   319  		return "", nil, err
   320  	}
   321  
   322  	return bindDN, groups, nil
   323  }
   324  
   325  // Bind - binds to ldap, searches LDAP and returns the distinguished name of the
   326  // user and the list of groups.
   327  func (l *Config) Bind(username, password string) (string, []string, error) {
   328  	conn, err := l.Connect()
   329  	if err != nil {
   330  		return "", nil, err
   331  	}
   332  	defer conn.Close()
   333  
   334  	var bindDN string
   335  	if l.isUsingLookupBind {
   336  		// Bind to the lookup user account
   337  		if err = l.lookupBind(conn); err != nil {
   338  			return "", nil, err
   339  		}
   340  
   341  		// Lookup user DN
   342  		bindDN, err = l.lookupUserDN(conn, username)
   343  		if err != nil {
   344  			errRet := fmt.Errorf("Unable to find user DN: %s", err)
   345  			return "", nil, errRet
   346  		}
   347  
   348  		// Authenticate the user credentials.
   349  		err = conn.Bind(bindDN, password)
   350  		if err != nil {
   351  			errRet := fmt.Errorf("LDAP auth failed for DN %s: %v", bindDN, err)
   352  			return "", nil, errRet
   353  		}
   354  
   355  		// Bind to the lookup user account again to perform group search.
   356  		if err = l.lookupBind(conn); err != nil {
   357  			return "", nil, err
   358  		}
   359  	} else {
   360  		// Verify login credentials by checking the username formats.
   361  		bindDN, err = l.usernameFormatsBind(conn, username, password)
   362  		if err != nil {
   363  			return "", nil, err
   364  		}
   365  
   366  		// Bind to the successful bindDN again.
   367  		err = conn.Bind(bindDN, password)
   368  		if err != nil {
   369  			errRet := fmt.Errorf("LDAP conn failed though auth for DN %s succeeded: %v", bindDN, err)
   370  			return "", nil, errRet
   371  		}
   372  	}
   373  
   374  	// User groups lookup.
   375  	groups, err := l.searchForUserGroups(conn, username, bindDN)
   376  	if err != nil {
   377  		return "", nil, err
   378  	}
   379  
   380  	return bindDN, groups, nil
   381  }
   382  
   383  // Connect connect to ldap server.
   384  func (l *Config) Connect() (ldapConn *ldap.Conn, err error) {
   385  	if l == nil {
   386  		return nil, errors.New("LDAP is not configured")
   387  	}
   388  
   389  	if _, _, err = net.SplitHostPort(l.ServerAddr); err != nil {
   390  		// User default LDAP port if none specified "636"
   391  		l.ServerAddr = net.JoinHostPort(l.ServerAddr, "636")
   392  	}
   393  
   394  	if l.serverInsecure {
   395  		return ldap.Dial("tcp", l.ServerAddr)
   396  	}
   397  
   398  	if l.serverStartTLS {
   399  		conn, err := ldap.Dial("tcp", l.ServerAddr)
   400  		if err != nil {
   401  			return nil, err
   402  		}
   403  		err = conn.StartTLS(&tls.Config{
   404  			InsecureSkipVerify: l.tlsSkipVerify,
   405  			RootCAs:            l.rootCAs,
   406  		})
   407  		return conn, err
   408  	}
   409  
   410  	return ldap.DialTLS("tcp", l.ServerAddr, &tls.Config{
   411  		InsecureSkipVerify: l.tlsSkipVerify,
   412  		RootCAs:            l.rootCAs,
   413  	})
   414  }
   415  
   416  // GetExpiryDuration - return parsed expiry duration.
   417  func (l Config) GetExpiryDuration() time.Duration {
   418  	return l.stsExpiryDuration
   419  }
   420  
   421  func (l Config) testConnection() error {
   422  	conn, err := l.Connect()
   423  	if err != nil {
   424  		return fmt.Errorf("Error creating connection to LDAP server: %v", err)
   425  	}
   426  	defer conn.Close()
   427  	if l.isUsingLookupBind {
   428  		if err = l.lookupBind(conn); err != nil {
   429  			return fmt.Errorf("Error connecting as LDAP Lookup Bind user: %v", err)
   430  		}
   431  		return nil
   432  	}
   433  
   434  	// Generate some random user credentials for username formats mode test.
   435  	username := fmt.Sprintf("sometestuser%09d", rand.Int31n(1000000000))
   436  	charset := []byte("abcdefghijklmnopqrstuvwxyz" +
   437  		"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
   438  	rand.Shuffle(len(charset), func(i, j int) {
   439  		charset[i], charset[j] = charset[j], charset[i]
   440  	})
   441  	password := string(charset[:20])
   442  	_, err = l.usernameFormatsBind(conn, username, password)
   443  	if err == nil {
   444  		// We don't expect to successfully guess a credential in this
   445  		// way.
   446  		return fmt.Errorf("Unexpected random credentials success for user=%s password=%s", username, password)
   447  	} else if strings.HasPrefix(err.Error(), "All username formats failed due to invalid credentials: ") {
   448  		return nil
   449  	}
   450  	return fmt.Errorf("LDAP connection test error: %v", err)
   451  }
   452  
   453  // Enabled returns if jwks is enabled.
   454  func Enabled(kvs config.KVS) bool {
   455  	return kvs.Get(ServerAddr) != ""
   456  }
   457  
   458  // Lookup - initializes LDAP config, overrides config, if any ENV values are set.
   459  func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
   460  	l = Config{}
   461  
   462  	// Purge all removed keys first
   463  	for _, k := range removedKeys {
   464  		kvs.Delete(k)
   465  	}
   466  
   467  	if err = config.CheckValidKeys(config.IdentityLDAPSubSys, kvs, DefaultKVS); err != nil {
   468  		return l, err
   469  	}
   470  	ldapServer := env.Get(EnvServerAddr, kvs.Get(ServerAddr))
   471  	if ldapServer == "" {
   472  		return l, nil
   473  	}
   474  	l.Enabled = true
   475  	l.ServerAddr = ldapServer
   476  	l.stsExpiryDuration = defaultLDAPExpiry
   477  	if v := env.Get(EnvSTSExpiry, kvs.Get(STSExpiry)); v != "" {
   478  		expDur, err := time.ParseDuration(v)
   479  		if err != nil {
   480  			return l, errors.New("LDAP expiry time err:" + err.Error())
   481  		}
   482  		if expDur <= 0 {
   483  			return l, errors.New("LDAP expiry time has to be positive")
   484  		}
   485  		l.STSExpiryDuration = v
   486  		l.stsExpiryDuration = expDur
   487  	}
   488  
   489  	// LDAP connection configuration
   490  	if v := env.Get(EnvServerInsecure, kvs.Get(ServerInsecure)); v != "" {
   491  		l.serverInsecure, err = config.ParseBool(v)
   492  		if err != nil {
   493  			return l, err
   494  		}
   495  	}
   496  	if v := env.Get(EnvServerStartTLS, kvs.Get(ServerStartTLS)); v != "" {
   497  		l.serverStartTLS, err = config.ParseBool(v)
   498  		if err != nil {
   499  			return l, err
   500  		}
   501  	}
   502  	if v := env.Get(EnvTLSSkipVerify, kvs.Get(TLSSkipVerify)); v != "" {
   503  		l.tlsSkipVerify, err = config.ParseBool(v)
   504  		if err != nil {
   505  			return l, err
   506  		}
   507  	}
   508  
   509  	// Lookup bind user configuration
   510  	lookupBindDN := env.Get(EnvLookupBindDN, kvs.Get(LookupBindDN))
   511  	lookupBindPassword := env.Get(EnvLookupBindPassword, kvs.Get(LookupBindPassword))
   512  	if lookupBindDN != "" {
   513  		l.LookupBindDN = lookupBindDN
   514  		l.LookupBindPassword = lookupBindPassword
   515  		l.isUsingLookupBind = true
   516  
   517  		// User DN search configuration
   518  		userDNSearchBaseDN := env.Get(EnvUserDNSearchBaseDN, kvs.Get(UserDNSearchBaseDN))
   519  		userDNSearchFilter := env.Get(EnvUserDNSearchFilter, kvs.Get(UserDNSearchFilter))
   520  		if userDNSearchFilter == "" || userDNSearchBaseDN == "" {
   521  			return l, errors.New("In lookup bind mode, userDN search base DN and userDN search filter are both required")
   522  		}
   523  		l.UserDNSearchBaseDN = userDNSearchBaseDN
   524  		l.UserDNSearchFilter = userDNSearchFilter
   525  	}
   526  
   527  	// Username format configuration.
   528  	if v := env.Get(EnvUsernameFormat, kvs.Get(UsernameFormat)); v != "" {
   529  		if !strings.Contains(v, "%s") {
   530  			return l, errors.New("LDAP username format doesn't have '%s' substitution")
   531  		}
   532  		l.UsernameFormats = strings.Split(v, dnDelimiter)
   533  	}
   534  
   535  	// Either lookup bind mode or username format is supported, but not
   536  	// both.
   537  	if l.isUsingLookupBind && len(l.UsernameFormats) > 0 {
   538  		return l, errors.New("Lookup Bind mode and Username Format mode are not supported at the same time")
   539  	}
   540  
   541  	// At least one of bind mode or username format must be used.
   542  	if !l.isUsingLookupBind && len(l.UsernameFormats) == 0 {
   543  		return l, errors.New("Either Lookup Bind mode or Username Format mode is required.")
   544  	}
   545  
   546  	// Test connection to LDAP server.
   547  	if err := l.testConnection(); err != nil {
   548  		return l, fmt.Errorf("Connection test for LDAP server failed: %v", err)
   549  	}
   550  
   551  	// Group search params configuration
   552  	grpSearchFilter := env.Get(EnvGroupSearchFilter, kvs.Get(GroupSearchFilter))
   553  	grpSearchBaseDN := env.Get(EnvGroupSearchBaseDN, kvs.Get(GroupSearchBaseDN))
   554  
   555  	// Either all group params must be set or none must be set.
   556  	if (grpSearchFilter != "" && grpSearchBaseDN == "") || (grpSearchFilter == "" && grpSearchBaseDN != "") {
   557  		return l, errors.New("All group related parameters must be set")
   558  	}
   559  
   560  	if grpSearchFilter != "" {
   561  		l.GroupSearchFilter = grpSearchFilter
   562  		l.GroupSearchBaseDistName = grpSearchBaseDN
   563  		l.GroupSearchBaseDistNames = strings.Split(l.GroupSearchBaseDistName, dnDelimiter)
   564  	}
   565  
   566  	l.rootCAs = rootCAs
   567  	return l, nil
   568  }