github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/config/identity/ldap/ldap.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package ldap
    19  
    20  import (
    21  	"errors"
    22  	"fmt"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	ldap "github.com/go-ldap/ldap/v3"
    28  	"github.com/minio/minio-go/v7/pkg/set"
    29  	"github.com/minio/minio/internal/auth"
    30  	xldap "github.com/minio/pkg/v2/ldap"
    31  )
    32  
    33  // LookupUserDN searches for the full DN and groups of a given username
    34  func (l *Config) LookupUserDN(username string) (string, []string, error) {
    35  	conn, err := l.LDAP.Connect()
    36  	if err != nil {
    37  		return "", nil, err
    38  	}
    39  	defer conn.Close()
    40  
    41  	// Bind to the lookup user account
    42  	if err = l.LDAP.LookupBind(conn); err != nil {
    43  		return "", nil, err
    44  	}
    45  
    46  	// Lookup user DN
    47  	bindDN, err := l.LDAP.LookupUserDN(conn, username)
    48  	if err != nil {
    49  		errRet := fmt.Errorf("Unable to find user DN: %w", err)
    50  		return "", nil, errRet
    51  	}
    52  
    53  	groups, err := l.LDAP.SearchForUserGroups(conn, username, bindDN)
    54  	if err != nil {
    55  		return "", nil, err
    56  	}
    57  
    58  	return bindDN, groups, nil
    59  }
    60  
    61  // GetValidatedDNForUsername checks if the given username exists in the LDAP directory.
    62  // The given username could be just the short "login" username or the full DN.
    63  //
    64  // When the username/DN is found, the full DN returned by the **server** is
    65  // returned, otherwise the returned string is empty. The value returned here is
    66  // the value sent by the LDAP server and is used in minio as the server performs
    67  // LDAP specific normalization (including Unicode normalization).
    68  //
    69  // If the user is not found, err = nil, otherwise, err != nil.
    70  func (l *Config) GetValidatedDNForUsername(username string) (string, error) {
    71  	conn, err := l.LDAP.Connect()
    72  	if err != nil {
    73  		return "", err
    74  	}
    75  	defer conn.Close()
    76  
    77  	// Bind to the lookup user account
    78  	if err = l.LDAP.LookupBind(conn); err != nil {
    79  		return "", err
    80  	}
    81  
    82  	// Check if the passed in username is a valid DN.
    83  	parsedUsernameDN, err := ldap.ParseDN(username)
    84  	if err != nil {
    85  		// Since the passed in username was not a DN, we consider it as a login
    86  		// username and attempt to check it exists in the directory.
    87  		bindDN, err := l.LDAP.LookupUserDN(conn, username)
    88  		if err != nil {
    89  			if strings.Contains(err.Error(), "not found") {
    90  				return "", nil
    91  			}
    92  			return "", fmt.Errorf("Unable to find user DN: %w", err)
    93  		}
    94  		return bindDN, nil
    95  	}
    96  
    97  	// Since the username is a valid DN, check that it is under a configured
    98  	// base DN in the LDAP directory.
    99  	var foundDistName []string
   100  	for _, baseDN := range l.LDAP.UserDNSearchBaseDistNames {
   101  		// BaseDN should not fail to parse.
   102  		baseDNParsed, _ := ldap.ParseDN(baseDN)
   103  		if baseDNParsed.AncestorOf(parsedUsernameDN) {
   104  			searchRequest := ldap.NewSearchRequest(username, ldap.ScopeBaseObject, ldap.NeverDerefAliases,
   105  				0, 0, false, "(objectClass=*)", nil, nil)
   106  			searchResult, err := conn.Search(searchRequest)
   107  			if err != nil {
   108  				// Check if there is no matching result.
   109  				// Ref: https://ldap.com/ldap-result-code-reference/
   110  				if ldap.IsErrorWithCode(err, 32) {
   111  					continue
   112  				}
   113  				return "", err
   114  			}
   115  			for _, entry := range searchResult.Entries {
   116  				normDN, err := xldap.NormalizeDN(entry.DN)
   117  				if err != nil {
   118  					return "", err
   119  				}
   120  				foundDistName = append(foundDistName, normDN)
   121  			}
   122  		}
   123  	}
   124  
   125  	if len(foundDistName) == 1 {
   126  		return foundDistName[0], nil
   127  	} else if len(foundDistName) > 1 {
   128  		// FIXME: This error would happen if the multiple base DNs are given and
   129  		// some base DNs are subtrees of other base DNs - we should validate
   130  		// and error out in such cases.
   131  		return "", fmt.Errorf("found multiple DNs for the given username")
   132  	}
   133  	return "", nil
   134  }
   135  
   136  // GetValidatedGroupDN checks if the given group DN exists in the LDAP directory
   137  // and returns the group DN sent by the LDAP server. The value returned by the
   138  // server may not be equal to the input group DN, as LDAP equality is not a
   139  // simple Golang string equality. However, we assume the value returned by the
   140  // LDAP server is canonical.
   141  //
   142  // If the group is not found in the LDAP directory, the returned string is empty
   143  // and err = nil.
   144  func (l *Config) GetValidatedGroupDN(groupDN string) (string, error) {
   145  	if len(l.LDAP.GroupSearchBaseDistNames) == 0 {
   146  		return "", errors.New("no group search Base DNs given")
   147  	}
   148  
   149  	gdn, err := ldap.ParseDN(groupDN)
   150  	if err != nil {
   151  		return "", fmt.Errorf("Given group DN could not be parsed: %s", err)
   152  	}
   153  
   154  	conn, err := l.LDAP.Connect()
   155  	if err != nil {
   156  		return "", err
   157  	}
   158  	defer conn.Close()
   159  
   160  	// Bind to the lookup user account
   161  	if err = l.LDAP.LookupBind(conn); err != nil {
   162  		return "", err
   163  	}
   164  
   165  	var foundDistName []string
   166  	for _, baseDN := range l.LDAP.GroupSearchBaseDistNames {
   167  		// BaseDN should not fail to parse.
   168  		baseDNParsed, _ := ldap.ParseDN(baseDN)
   169  		if baseDNParsed.AncestorOf(gdn) {
   170  			searchRequest := ldap.NewSearchRequest(groupDN, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=*)", nil, nil)
   171  			searchResult, err := conn.Search(searchRequest)
   172  			if err != nil {
   173  				// Check if there is no matching result.
   174  				// Ref: https://ldap.com/ldap-result-code-reference/
   175  				if ldap.IsErrorWithCode(err, 32) {
   176  					continue
   177  				}
   178  				return "", err
   179  			}
   180  			for _, entry := range searchResult.Entries {
   181  				normDN, err := xldap.NormalizeDN(entry.DN)
   182  				if err != nil {
   183  					return "", err
   184  				}
   185  				foundDistName = append(foundDistName, normDN)
   186  			}
   187  		}
   188  	}
   189  	if len(foundDistName) == 1 {
   190  		return foundDistName[0], nil
   191  	} else if len(foundDistName) > 1 {
   192  		// FIXME: This error would happen if the multiple base DNs are given and
   193  		// some base DNs are subtrees of other base DNs - we should validate
   194  		// and error out in such cases.
   195  		return "", fmt.Errorf("found multiple DNs for the given group DN")
   196  	}
   197  	return "", nil
   198  }
   199  
   200  // Bind - binds to ldap, searches LDAP and returns the distinguished name of the
   201  // user and the list of groups.
   202  func (l *Config) Bind(username, password string) (string, []string, error) {
   203  	conn, err := l.LDAP.Connect()
   204  	if err != nil {
   205  		return "", nil, err
   206  	}
   207  	defer conn.Close()
   208  
   209  	var bindDN string
   210  	// Bind to the lookup user account
   211  	if err = l.LDAP.LookupBind(conn); err != nil {
   212  		return "", nil, err
   213  	}
   214  
   215  	// Lookup user DN
   216  	bindDN, err = l.LDAP.LookupUserDN(conn, username)
   217  	if err != nil {
   218  		errRet := fmt.Errorf("Unable to find user DN: %w", err)
   219  		return "", nil, errRet
   220  	}
   221  
   222  	// Authenticate the user credentials.
   223  	err = conn.Bind(bindDN, password)
   224  	if err != nil {
   225  		errRet := fmt.Errorf("LDAP auth failed for DN %s: %w", bindDN, err)
   226  		return "", nil, errRet
   227  	}
   228  
   229  	// Bind to the lookup user account again to perform group search.
   230  	if err = l.LDAP.LookupBind(conn); err != nil {
   231  		return "", nil, err
   232  	}
   233  
   234  	// User groups lookup.
   235  	groups, err := l.LDAP.SearchForUserGroups(conn, username, bindDN)
   236  	if err != nil {
   237  		return "", nil, err
   238  	}
   239  
   240  	return bindDN, groups, nil
   241  }
   242  
   243  // GetExpiryDuration - return parsed expiry duration.
   244  func (l Config) GetExpiryDuration(dsecs string) (time.Duration, error) {
   245  	if dsecs == "" {
   246  		return l.stsExpiryDuration, nil
   247  	}
   248  
   249  	d, err := strconv.Atoi(dsecs)
   250  	if err != nil {
   251  		return 0, auth.ErrInvalidDuration
   252  	}
   253  
   254  	dur := time.Duration(d) * time.Second
   255  
   256  	if dur < minLDAPExpiry || dur > maxLDAPExpiry {
   257  		return 0, auth.ErrInvalidDuration
   258  	}
   259  	return dur, nil
   260  }
   261  
   262  // IsLDAPUserDN determines if the given string could be a user DN from LDAP.
   263  func (l Config) IsLDAPUserDN(user string) bool {
   264  	for _, baseDN := range l.LDAP.UserDNSearchBaseDistNames {
   265  		if strings.HasSuffix(user, ","+baseDN) {
   266  			return true
   267  		}
   268  	}
   269  	return false
   270  }
   271  
   272  // IsLDAPGroupDN determines if the given string could be a group DN from LDAP.
   273  func (l Config) IsLDAPGroupDN(user string) bool {
   274  	for _, baseDN := range l.LDAP.GroupSearchBaseDistNames {
   275  		if strings.HasSuffix(user, ","+baseDN) {
   276  			return true
   277  		}
   278  	}
   279  	return false
   280  }
   281  
   282  // GetNonEligibleUserDistNames - find user accounts (DNs) that are no longer
   283  // present in the LDAP server or do not meet filter criteria anymore
   284  func (l *Config) GetNonEligibleUserDistNames(userDistNames []string) ([]string, error) {
   285  	conn, err := l.LDAP.Connect()
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  	defer conn.Close()
   290  
   291  	// Bind to the lookup user account
   292  	if err = l.LDAP.LookupBind(conn); err != nil {
   293  		return nil, err
   294  	}
   295  
   296  	// Evaluate the filter again with generic wildcard instead of  specific values
   297  	filter := strings.ReplaceAll(l.LDAP.UserDNSearchFilter, "%s", "*")
   298  
   299  	nonExistentUsers := []string{}
   300  	for _, dn := range userDistNames {
   301  		searchRequest := ldap.NewSearchRequest(
   302  			dn,
   303  			ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
   304  			filter,
   305  			[]string{}, // only need DN, so pass no attributes here
   306  			nil,
   307  		)
   308  
   309  		searchResult, err := conn.Search(searchRequest)
   310  		if err != nil {
   311  			// Object does not exist error?
   312  			if ldap.IsErrorWithCode(err, 32) {
   313  				nonExistentUsers = append(nonExistentUsers, dn)
   314  				continue
   315  			}
   316  			return nil, err
   317  		}
   318  		if len(searchResult.Entries) == 0 {
   319  			// DN was not found - this means this user account is
   320  			// expired.
   321  			nonExistentUsers = append(nonExistentUsers, dn)
   322  		}
   323  	}
   324  	return nonExistentUsers, nil
   325  }
   326  
   327  // LookupGroupMemberships - for each DN finds the set of LDAP groups they are a
   328  // member of.
   329  func (l *Config) LookupGroupMemberships(userDistNames []string, userDNToUsernameMap map[string]string) (map[string]set.StringSet, error) {
   330  	conn, err := l.LDAP.Connect()
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  	defer conn.Close()
   335  
   336  	// Bind to the lookup user account
   337  	if err = l.LDAP.LookupBind(conn); err != nil {
   338  		return nil, err
   339  	}
   340  
   341  	res := make(map[string]set.StringSet, len(userDistNames))
   342  	for _, userDistName := range userDistNames {
   343  		username := userDNToUsernameMap[userDistName]
   344  		groups, err := l.LDAP.SearchForUserGroups(conn, username, userDistName)
   345  		if err != nil {
   346  			return nil, err
   347  		}
   348  		res[userDistName] = set.CreateStringSet(groups...)
   349  	}
   350  
   351  	return res, nil
   352  }