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 }