mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
336526065f
BREAKING CHANGE: repository paths are now specified under a new config key called "repositories" under "accessControl" section in order to handle "groups" feature. Previously the repository paths were specified directly under "accessControl". This PR adds the ability to create groups of users which can be used for authZ policies, instead of just users. { "http": { "accessControl": { "groups": { Just like the users, groups can be part of repository policies/default policies/admin policies. The 'groups' field in accessControl can be missing if there are no groups. The permissions priority is user>group>default>admin policy, verified in this order (in authz.go), and permissions are cumulative. It works with LDAP too, and the group attribute name is configurable. The DN of the group is used as the group name and the functionality is the same. All groups for the given user are added to the context in authn.go. Repository paths are now specified under a new keyword called "repositories" under "accessControl" section in order to handle "groups" feature. Signed-off-by: Ana-Roberta Lisca <ana.kagome@yahoo.com>
226 lines
5.4 KiB
Go
226 lines
5.4 KiB
Go
// Package ldap provides a simple ldap client to authenticate,
|
|
// retrieve basic information and groups for a user.
|
|
package api
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-ldap/ldap/v3"
|
|
|
|
"zotregistry.io/zot/errors"
|
|
"zotregistry.io/zot/pkg/log"
|
|
)
|
|
|
|
type LDAPClient struct {
|
|
InsecureSkipVerify bool
|
|
UseSSL bool
|
|
SkipTLS bool
|
|
SubtreeSearch bool
|
|
Port int
|
|
Attributes []string
|
|
Base string
|
|
BindDN string
|
|
BindPassword string
|
|
GroupFilter string // e.g. "(memberUid=%s)"
|
|
UserGroupAttribute string // e.g. "memberOf"
|
|
Host string
|
|
ServerName string
|
|
UserFilter string // e.g. "(uid=%s)"
|
|
Conn *ldap.Conn
|
|
ClientCertificates []tls.Certificate // Adding client certificates
|
|
ClientCAs *x509.CertPool
|
|
Log log.Logger
|
|
lock sync.Mutex
|
|
}
|
|
|
|
// Connect connects to the ldap backend.
|
|
func (lc *LDAPClient) Connect() error {
|
|
if lc.Conn == nil {
|
|
var l *ldap.Conn
|
|
|
|
var err error
|
|
|
|
address := fmt.Sprintf("%s:%d", lc.Host, lc.Port)
|
|
|
|
if !lc.UseSSL {
|
|
l, err = ldap.Dial("tcp", address)
|
|
if err != nil {
|
|
lc.Log.Error().Err(err).Str("address", address).Msg("non-TLS connection failed")
|
|
|
|
return err
|
|
}
|
|
|
|
// Reconnect with TLS
|
|
if !lc.SkipTLS {
|
|
config := &tls.Config{
|
|
InsecureSkipVerify: lc.InsecureSkipVerify, //nolint: gosec // InsecureSkipVerify is not true by default
|
|
RootCAs: lc.ClientCAs,
|
|
}
|
|
|
|
if lc.ClientCertificates != nil && len(lc.ClientCertificates) > 0 {
|
|
config.Certificates = lc.ClientCertificates
|
|
}
|
|
|
|
err = l.StartTLS(config)
|
|
|
|
if err != nil {
|
|
lc.Log.Error().Err(err).Str("address", address).Msg("TLS connection failed")
|
|
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
config := &tls.Config{
|
|
InsecureSkipVerify: lc.InsecureSkipVerify, //nolint: gosec // InsecureSkipVerify is not true by default
|
|
ServerName: lc.ServerName,
|
|
RootCAs: lc.ClientCAs,
|
|
}
|
|
if lc.ClientCertificates != nil && len(lc.ClientCertificates) > 0 {
|
|
config.Certificates = lc.ClientCertificates
|
|
// config.BuildNameToCertificate()
|
|
}
|
|
l, err = ldap.DialTLS("tcp", address, config)
|
|
if err != nil {
|
|
lc.Log.Error().Err(err).Str("address", address).Msg("TLS connection failed")
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
lc.Conn = l
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close closes the ldap backend connection.
|
|
func (lc *LDAPClient) Close() {
|
|
if lc.Conn != nil {
|
|
lc.Conn.Close()
|
|
lc.Conn = nil
|
|
}
|
|
}
|
|
|
|
const maxRetries = 8
|
|
|
|
func sleepAndRetry(retries, maxRetries int) bool {
|
|
if retries > maxRetries {
|
|
return false
|
|
}
|
|
|
|
if retries < maxRetries {
|
|
time.Sleep(time.Duration(retries) * time.Second) // gradually backoff
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Authenticate authenticates the user against the ldap backend.
|
|
func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string]string, []string, error) {
|
|
// serialize LDAP calls since some LDAP servers don't allow searches when binds are in flight
|
|
lc.lock.Lock()
|
|
defer lc.lock.Unlock()
|
|
|
|
if password == "" {
|
|
// RFC 4513 section 5.1.2
|
|
return false, nil, nil, errors.ErrLDAPEmptyPassphrase
|
|
}
|
|
|
|
connected := false
|
|
for retries := 0; !connected && sleepAndRetry(retries, maxRetries); retries++ {
|
|
err := lc.Connect()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// First bind with a read only user
|
|
if lc.BindDN != "" && lc.BindPassword != "" {
|
|
err := lc.Conn.Bind(lc.BindDN, lc.BindPassword)
|
|
if err != nil {
|
|
lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Msg("bind failed")
|
|
// clean up the cached conn, so we can retry
|
|
lc.Conn.Close()
|
|
lc.Conn = nil
|
|
|
|
continue
|
|
}
|
|
}
|
|
|
|
connected = true
|
|
}
|
|
|
|
// exhausted all retries?
|
|
if !connected {
|
|
lc.Log.Error().Err(errors.ErrLDAPBadConn).Msg("exhausted all retries")
|
|
|
|
return false, nil, nil, errors.ErrLDAPBadConn
|
|
}
|
|
|
|
attributes := lc.Attributes
|
|
attributes = append(attributes, "dn")
|
|
attributes = append(attributes, lc.UserGroupAttribute)
|
|
|
|
searchScope := ldap.ScopeSingleLevel
|
|
|
|
if lc.SubtreeSearch {
|
|
searchScope = ldap.ScopeWholeSubtree
|
|
}
|
|
// Search for the given username
|
|
searchRequest := ldap.NewSearchRequest(
|
|
lc.Base,
|
|
searchScope, ldap.NeverDerefAliases, 0, 0, false,
|
|
fmt.Sprintf(lc.UserFilter, username),
|
|
attributes,
|
|
nil,
|
|
)
|
|
|
|
search, err := lc.Conn.Search(searchRequest)
|
|
if err != nil {
|
|
fmt.Printf("%v\n", err)
|
|
lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Str("username", username).
|
|
Str("baseDN", lc.Base).Msg("search failed")
|
|
|
|
return false, nil, nil, err
|
|
}
|
|
|
|
if len(search.Entries) < 1 {
|
|
err := errors.ErrBadUser
|
|
lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Str("username", username).
|
|
Str("baseDN", lc.Base).Msg("entries not found")
|
|
|
|
return false, nil, nil, err
|
|
}
|
|
|
|
if len(search.Entries) > 1 {
|
|
err := errors.ErrEntriesExceeded
|
|
lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Str("username", username).
|
|
Str("baseDN", lc.Base).Msg("too many entries")
|
|
|
|
return false, nil, nil, err
|
|
}
|
|
|
|
userDN := search.Entries[0].DN
|
|
userAttributes := search.Entries[0].Attributes[0]
|
|
userGroups := userAttributes.Values
|
|
user := map[string]string{}
|
|
|
|
for _, attr := range lc.Attributes {
|
|
user[attr] = search.Entries[0].GetAttributeValue(attr)
|
|
}
|
|
|
|
// Bind as the user to verify their password
|
|
err = lc.Conn.Bind(userDN, password)
|
|
if err != nil {
|
|
lc.Log.Error().Err(err).Str("bindDN", userDN).Msg("user bind failed")
|
|
|
|
return false, user, userGroups, err
|
|
}
|
|
|
|
return true, user, userGroups, nil
|
|
}
|