zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/api/ldap.go (about)

     1  // Package ldap provides a simple ldap client to authenticate,
     2  // retrieve basic information and groups for a user.
     3  package api
     4  
     5  import (
     6  	"crypto/tls"
     7  	"crypto/x509"
     8  	"fmt"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/go-ldap/ldap/v3"
    13  
    14  	"zotregistry.io/zot/errors"
    15  	"zotregistry.io/zot/pkg/log"
    16  )
    17  
    18  type LDAPClient struct {
    19  	InsecureSkipVerify bool
    20  	UseSSL             bool
    21  	SkipTLS            bool
    22  	SubtreeSearch      bool
    23  	Port               int
    24  	Attributes         []string
    25  	Base               string
    26  	BindDN             string
    27  	BindPassword       string
    28  	GroupFilter        string // e.g. "(memberUid=%s)"
    29  	UserGroupAttribute string // e.g. "memberOf"
    30  	Host               string
    31  	ServerName         string
    32  	UserFilter         string // e.g. "(uid=%s)"
    33  	Conn               *ldap.Conn
    34  	ClientCertificates []tls.Certificate // Adding client certificates
    35  	ClientCAs          *x509.CertPool
    36  	Log                log.Logger
    37  	lock               sync.Mutex
    38  }
    39  
    40  // Connect connects to the ldap backend.
    41  func (lc *LDAPClient) Connect() error {
    42  	if lc.Conn == nil {
    43  		var l *ldap.Conn
    44  
    45  		var err error
    46  
    47  		address := fmt.Sprintf("%s:%d", lc.Host, lc.Port)
    48  
    49  		if !lc.UseSSL {
    50  			l, err = ldap.Dial("tcp", address)
    51  			if err != nil {
    52  				lc.Log.Error().Err(err).Str("address", address).Msg("non-TLS connection failed")
    53  
    54  				return err
    55  			}
    56  
    57  			// Reconnect with TLS
    58  			if !lc.SkipTLS {
    59  				config := &tls.Config{
    60  					InsecureSkipVerify: lc.InsecureSkipVerify, //nolint: gosec // InsecureSkipVerify is not true by default
    61  					RootCAs:            lc.ClientCAs,
    62  				}
    63  
    64  				if lc.ClientCertificates != nil && len(lc.ClientCertificates) > 0 {
    65  					config.Certificates = lc.ClientCertificates
    66  				}
    67  
    68  				err = l.StartTLS(config)
    69  
    70  				if err != nil {
    71  					lc.Log.Error().Err(err).Str("address", address).Msg("TLS connection failed")
    72  
    73  					return err
    74  				}
    75  			}
    76  		} else {
    77  			config := &tls.Config{
    78  				InsecureSkipVerify: lc.InsecureSkipVerify, //nolint: gosec // InsecureSkipVerify is not true by default
    79  				ServerName:         lc.ServerName,
    80  				RootCAs:            lc.ClientCAs,
    81  			}
    82  			if lc.ClientCertificates != nil && len(lc.ClientCertificates) > 0 {
    83  				config.Certificates = lc.ClientCertificates
    84  				// config.BuildNameToCertificate()
    85  			}
    86  			l, err = ldap.DialTLS("tcp", address, config)
    87  			if err != nil {
    88  				lc.Log.Error().Err(err).Str("address", address).Msg("TLS connection failed")
    89  
    90  				return err
    91  			}
    92  		}
    93  
    94  		lc.Conn = l
    95  	}
    96  
    97  	return nil
    98  }
    99  
   100  // Close closes the ldap backend connection.
   101  func (lc *LDAPClient) Close() {
   102  	if lc.Conn != nil {
   103  		lc.Conn.Close()
   104  		lc.Conn = nil
   105  	}
   106  }
   107  
   108  const maxRetries = 8
   109  
   110  func sleepAndRetry(retries, maxRetries int) bool {
   111  	if retries > maxRetries {
   112  		return false
   113  	}
   114  
   115  	if retries < maxRetries {
   116  		time.Sleep(time.Duration(retries) * time.Second) // gradually backoff
   117  
   118  		return true
   119  	}
   120  
   121  	return false
   122  }
   123  
   124  // Authenticate authenticates the user against the ldap backend.
   125  func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string]string, []string, error) {
   126  	// serialize LDAP calls since some LDAP servers don't allow searches when binds are in flight
   127  	lc.lock.Lock()
   128  	defer lc.lock.Unlock()
   129  
   130  	if password == "" {
   131  		// RFC 4513 section 5.1.2
   132  		return false, nil, nil, errors.ErrLDAPEmptyPassphrase
   133  	}
   134  
   135  	connected := false
   136  	for retries := 0; !connected && sleepAndRetry(retries, maxRetries); retries++ {
   137  		err := lc.Connect()
   138  		if err != nil {
   139  			continue
   140  		}
   141  
   142  		// First bind with a read only user
   143  		if lc.BindPassword != "" {
   144  			err := lc.Conn.Bind(lc.BindDN, lc.BindPassword)
   145  			if err != nil {
   146  				lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Msg("bind failed")
   147  				// clean up the cached conn, so we can retry
   148  				lc.Conn.Close()
   149  				lc.Conn = nil
   150  
   151  				continue
   152  			}
   153  		} else {
   154  			err := lc.Conn.UnauthenticatedBind(lc.BindDN)
   155  			if err != nil {
   156  				lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Msg("bind failed")
   157  				// clean up the cached conn, so we can retry
   158  				lc.Conn.Close()
   159  				lc.Conn = nil
   160  
   161  				continue
   162  			}
   163  		}
   164  
   165  		connected = true
   166  	}
   167  
   168  	// exhausted all retries?
   169  	if !connected {
   170  		lc.Log.Error().Err(errors.ErrLDAPBadConn).Msg("exhausted all retries")
   171  
   172  		return false, nil, nil, errors.ErrLDAPBadConn
   173  	}
   174  
   175  	attributes := lc.Attributes
   176  	attributes = append(attributes, "dn")
   177  	attributes = append(attributes, lc.UserGroupAttribute)
   178  
   179  	searchScope := ldap.ScopeSingleLevel
   180  
   181  	if lc.SubtreeSearch {
   182  		searchScope = ldap.ScopeWholeSubtree
   183  	}
   184  	// Search for the given username
   185  	searchRequest := ldap.NewSearchRequest(
   186  		lc.Base,
   187  		searchScope, ldap.NeverDerefAliases, 0, 0, false,
   188  		fmt.Sprintf(lc.UserFilter, username),
   189  		attributes,
   190  		nil,
   191  	)
   192  
   193  	search, err := lc.Conn.Search(searchRequest)
   194  	if err != nil {
   195  		fmt.Printf("%v\n", err)
   196  		lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Str("username", username).
   197  			Str("baseDN", lc.Base).Msg("search failed")
   198  
   199  		return false, nil, nil, err
   200  	}
   201  
   202  	if len(search.Entries) < 1 {
   203  		err := errors.ErrBadUser
   204  		lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Str("username", username).
   205  			Str("baseDN", lc.Base).Msg("entries not found")
   206  
   207  		return false, nil, nil, err
   208  	}
   209  
   210  	if len(search.Entries) > 1 {
   211  		err := errors.ErrEntriesExceeded
   212  		lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Str("username", username).
   213  			Str("baseDN", lc.Base).Msg("too many entries")
   214  
   215  		return false, nil, nil, err
   216  	}
   217  
   218  	userDN := search.Entries[0].DN
   219  	userAttributes := search.Entries[0].Attributes[0]
   220  	userGroups := userAttributes.Values
   221  	user := map[string]string{}
   222  
   223  	for _, attr := range lc.Attributes {
   224  		user[attr] = search.Entries[0].GetAttributeValue(attr)
   225  	}
   226  
   227  	// Bind as the user to verify their password
   228  	err = lc.Conn.Bind(userDN, password)
   229  	if err != nil {
   230  		lc.Log.Error().Err(err).Str("bindDN", userDN).Msg("user bind failed")
   231  
   232  		return false, user, userGroups, err
   233  	}
   234  
   235  	return true, user, userGroups, nil
   236  }