ldap: Add user DN attributes list config param (#19758)

This change uses the updated ldap library in minio/pkg (bumped
up to v3). A new config parameter is added for LDAP configuration to
specify extra user attributes to load from the LDAP server and to store
them as additional claims for the user.

A test is added in sts_handlers.go that shows how to access the LDAP
attributes as a claim.

This is in preparation for adding SSH pubkey authentication to MinIO's SFTP
integration.
This commit is contained in:
Aditya Manthramurthy
2024-05-24 16:05:23 -07:00
committed by GitHub
parent a591e06ae5
commit 5f78691fcf
179 changed files with 524 additions and 362 deletions

View File

@@ -27,7 +27,7 @@ import (
"time"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// API sub-system constants

View File

@@ -22,7 +22,7 @@ import (
"time"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// Batch job environment variables

View File

@@ -23,7 +23,7 @@ import (
"sync"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// Browser sub-system constants

View File

@@ -29,7 +29,7 @@ import (
"github.com/dustin/go-humanize"
"github.com/minio/minio/internal/config"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
"github.com/tinylib/msgp/msgp"
)

View File

@@ -22,7 +22,7 @@ import (
"time"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// Callhome related keys

View File

@@ -25,7 +25,7 @@ import (
"errors"
"os"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// EnvCertPassword is the environment variable which contains the password used

View File

@@ -22,7 +22,7 @@ import (
"strings"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// Config represents the compression settings.

View File

@@ -29,7 +29,7 @@ import (
"github.com/minio/madmin-go/v3"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/auth"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// ErrorConfig holds the config error types

View File

@@ -22,7 +22,7 @@ import (
"time"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// Drive specific timeout environment variables

View File

@@ -24,8 +24,8 @@ import (
"time"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
xnet "github.com/minio/pkg/v2/net"
"github.com/minio/pkg/v3/env"
xnet "github.com/minio/pkg/v3/net"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/namespace"
"go.uber.org/zap"

View File

@@ -26,7 +26,7 @@ import (
"time"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// Compression environment variables

View File

@@ -25,7 +25,7 @@ import (
"github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/ldap"
"github.com/minio/pkg/v3/ldap"
)
const (
@@ -67,6 +67,7 @@ const (
LookupBindPassword = "lookup_bind_password"
UserDNSearchBaseDN = "user_dn_search_base_dn"
UserDNSearchFilter = "user_dn_search_filter"
UserDNAttributes = "user_dn_attributes"
GroupSearchFilter = "group_search_filter"
GroupSearchBaseDN = "group_search_base_dn"
TLSSkipVerify = "tls_skip_verify"
@@ -81,6 +82,7 @@ const (
EnvUsernameFormat = "MINIO_IDENTITY_LDAP_USERNAME_FORMAT"
EnvUserDNSearchBaseDN = "MINIO_IDENTITY_LDAP_USER_DN_SEARCH_BASE_DN"
EnvUserDNSearchFilter = "MINIO_IDENTITY_LDAP_USER_DN_SEARCH_FILTER"
EnvUserDNAttributes = "MINIO_IDENTITY_LDAP_USER_DN_ATTRIBUTES"
EnvGroupSearchFilter = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER"
EnvGroupSearchBaseDN = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN"
EnvLookupBindDN = "MINIO_IDENTITY_LDAP_LOOKUP_BIND_DN"
@@ -118,6 +120,10 @@ var (
Key: UserDNSearchFilter,
Value: "",
},
config.KV{
Key: UserDNAttributes,
Value: "",
},
config.KV{
Key: GroupSearchFilter,
Value: "",
@@ -227,6 +233,7 @@ func Lookup(s config.Config, rootCAs *x509.CertPool) (l Config, err error) {
// User DN search configuration
l.LDAP.UserDNSearchFilter = getCfgVal(UserDNSearchFilter)
l.LDAP.UserDNSearchBaseDistName = getCfgVal(UserDNSearchBaseDN)
l.LDAP.UserDNAttributes = getCfgVal(UserDNAttributes)
// Group search params configuration
l.LDAP.GroupSearchFilter = getCfgVal(GroupSearchFilter)

View File

@@ -66,6 +66,12 @@ var (
Optional: true,
Type: "string",
},
config.HelpKV{
Key: UserDNAttributes,
Description: `"," separated list of user DN attributes e.g. "uid,cn,mail,sshPublicKey"` + defaultHelpPostfix(UserDNAttributes),
Optional: true,
Type: "list",
},
config.HelpKV{
Key: GroupSearchFilter,
Description: `search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"` + defaultHelpPostfix(GroupSearchFilter),

View File

@@ -27,36 +27,36 @@ import (
ldap "github.com/go-ldap/ldap/v3"
"github.com/minio/minio-go/v7/pkg/set"
"github.com/minio/minio/internal/auth"
xldap "github.com/minio/pkg/v2/ldap"
xldap "github.com/minio/pkg/v3/ldap"
)
// LookupUserDN searches for the full DN and groups of a given short/login
// username.
func (l *Config) LookupUserDN(username string) (string, []string, error) {
func (l *Config) LookupUserDN(username string) (*xldap.DNSearchResult, []string, error) {
conn, err := l.LDAP.Connect()
if err != nil {
return "", nil, err
return nil, nil, err
}
defer conn.Close()
// Bind to the lookup user account
if err = l.LDAP.LookupBind(conn); err != nil {
return "", nil, err
return nil, nil, err
}
// Lookup user DN
bindDN, err := l.LDAP.LookupUserDN(conn, username)
lookupRes, err := l.LDAP.LookupUsername(conn, username)
if err != nil {
errRet := fmt.Errorf("Unable to find user DN: %w", err)
return "", nil, errRet
return nil, nil, errRet
}
groups, err := l.LDAP.SearchForUserGroups(conn, username, bindDN)
groups, err := l.LDAP.SearchForUserGroups(conn, username, lookupRes.NormDN)
if err != nil {
return "", nil, err
return nil, nil, err
}
return bindDN, groups, nil
return lookupRes, groups, nil
}
// GetValidatedDNForUsername checks if the given username exists in the LDAP directory.
@@ -68,28 +68,28 @@ func (l *Config) LookupUserDN(username string) (string, []string, error) {
// LDAP specific normalization (including Unicode normalization).
//
// If the user is not found, err = nil, otherwise, err != nil.
func (l *Config) GetValidatedDNForUsername(username string) (string, error) {
func (l *Config) GetValidatedDNForUsername(username string) (*xldap.DNSearchResult, error) {
conn, err := l.LDAP.Connect()
if err != nil {
return "", err
return nil, err
}
defer conn.Close()
// Bind to the lookup user account
if err = l.LDAP.LookupBind(conn); err != nil {
return "", err
return nil, err
}
// Check if the passed in username is a valid DN.
if !l.ParsesAsDN(username) {
// We consider it as a login username and attempt to check it exists in
// the directory.
bindDN, err := l.LDAP.LookupUserDN(conn, username)
bindDN, err := l.LDAP.LookupUsername(conn, username)
if err != nil {
if strings.Contains(err.Error(), "User DN not found for") {
return "", nil
return nil, nil
}
return "", fmt.Errorf("Unable to find user DN: %w", err)
return nil, fmt.Errorf("Unable to find user DN: %w", err)
}
return bindDN, nil
}
@@ -98,124 +98,126 @@ func (l *Config) GetValidatedDNForUsername(username string) (string, error) {
// under a configured base DN in the LDAP directory.
validDN, isUnderBaseDN, err := l.GetValidatedUserDN(conn, username)
if err == nil && !isUnderBaseDN {
return "", fmt.Errorf("Unable to find user DN: %w", err)
return nil, fmt.Errorf("Unable to find user DN: %w", err)
}
return validDN, err
}
// GetValidatedUserDN validates the given user DN. Will error out if conn is nil. The returned
// boolean is true iff the user DN is found under one of the LDAP user base DNs.
func (l *Config) GetValidatedUserDN(conn *ldap.Conn, userDN string) (string, bool, error) {
return l.GetValidatedDNUnderBaseDN(conn, userDN, l.LDAP.UserDNSearchBaseDistNames)
func (l *Config) GetValidatedUserDN(conn *ldap.Conn, userDN string) (*xldap.DNSearchResult, bool, error) {
return l.GetValidatedDNUnderBaseDN(conn, userDN,
l.LDAP.GetUserDNSearchBaseDistNames(), l.LDAP.GetUserDNAttributesList())
}
// GetValidatedGroupDN validates the given group DN. If conn is nil, creates a
// connection. The returned boolean is true iff the group DN is found under one
// of the configured LDAP base DNs.
func (l *Config) GetValidatedGroupDN(conn *ldap.Conn, groupDN string) (string, bool, error) {
func (l *Config) GetValidatedGroupDN(conn *ldap.Conn, groupDN string) (*xldap.DNSearchResult, bool, error) {
if conn == nil {
var err error
conn, err = l.LDAP.Connect()
if err != nil {
return "", false, err
return nil, false, err
}
defer conn.Close()
// Bind to the lookup user account
if err = l.LDAP.LookupBind(conn); err != nil {
return "", false, err
return nil, false, err
}
}
return l.GetValidatedDNUnderBaseDN(conn, groupDN, l.LDAP.GroupSearchBaseDistNames)
return l.GetValidatedDNUnderBaseDN(conn, groupDN,
l.LDAP.GetGroupSearchBaseDistNames(), nil)
}
// GetValidatedDNUnderBaseDN checks if the given DN exists in the LDAP directory
// and returns the DN value sent by the LDAP server. The value returned by the
// server may not be equal to the input DN, as LDAP equality is not a simple
// Golang string equality. However, we assume the value returned by the LDAP
// server is canonical. Additionally, the attribute type names in the DN are
// lower-cased.
// GetValidatedDNUnderBaseDN checks if the given DN exists in the LDAP
// directory.
//
// The `NormDN` value returned here in the search result may not be equal to the
// input DN, as LDAP equality is not a simple Golang string equality. However,
// we assume the value returned by the LDAP server is canonical. Additionally,
// the attribute type names in the DN are lower-cased.
//
// Return values:
//
// If the DN is found, the normalized (string) value is returned and error is
// nil.
// If the DN is found, the normalized (string) value and any requested
// attributes are returned and error is nil.
//
// If the DN is not found, the string returned is empty and the error is nil.
// If the DN is not found, a nil result and error are returned.
//
// The returned boolean is true iff the DN is found under one of the LDAP
// subtrees listed in `baseDNList`.
func (l *Config) GetValidatedDNUnderBaseDN(conn *ldap.Conn, dn string, baseDNList []xldap.BaseDNInfo) (string, bool, error) {
func (l *Config) GetValidatedDNUnderBaseDN(conn *ldap.Conn, dn string, baseDNList []xldap.BaseDNInfo, attrs []string) (*xldap.DNSearchResult, bool, error) {
if len(baseDNList) == 0 {
return "", false, errors.New("no Base DNs given")
return nil, false, errors.New("no Base DNs given")
}
// Check that DN exists in the LDAP directory.
validatedDN, err := xldap.LookupDN(conn, dn)
searchRes, err := xldap.LookupDN(conn, dn, attrs)
if err != nil {
return "", false, fmt.Errorf("Error looking up DN %s: %w", dn, err)
return nil, false, fmt.Errorf("Error looking up DN %s: %w", dn, err)
}
if validatedDN == "" {
return "", false, nil
if searchRes == nil {
return nil, false, nil
}
// This will not return an error as the argument is validated to be a DN.
pdn, _ := ldap.ParseDN(validatedDN)
pdn, _ := ldap.ParseDN(searchRes.NormDN)
// Check that the DN is under a configured base DN in the LDAP
// directory.
for _, baseDN := range baseDNList {
if baseDN.Parsed.AncestorOf(pdn) {
return validatedDN, true, nil
return searchRes, true, nil
}
}
// Not under any configured base DN so return false.
return validatedDN, false, nil
return searchRes, false, nil
}
// Bind - binds to ldap, searches LDAP and returns the distinguished name of the
// user and the list of groups.
func (l *Config) Bind(username, password string) (string, []string, error) {
func (l *Config) Bind(username, password string) (*xldap.DNSearchResult, []string, error) {
conn, err := l.LDAP.Connect()
if err != nil {
return "", nil, err
return nil, nil, err
}
defer conn.Close()
var bindDN string
// Bind to the lookup user account
if err = l.LDAP.LookupBind(conn); err != nil {
return "", nil, err
return nil, nil, err
}
// Lookup user DN
bindDN, err = l.LDAP.LookupUserDN(conn, username)
lookupResult, err := l.LDAP.LookupUsername(conn, username)
if err != nil {
errRet := fmt.Errorf("Unable to find user DN: %w", err)
return "", nil, errRet
return nil, nil, errRet
}
// Authenticate the user credentials.
err = conn.Bind(bindDN, password)
err = conn.Bind(lookupResult.NormDN, password)
if err != nil {
errRet := fmt.Errorf("LDAP auth failed for DN %s: %w", bindDN, err)
return "", nil, errRet
errRet := fmt.Errorf("LDAP auth failed for DN %s: %w", lookupResult.NormDN, err)
return nil, nil, errRet
}
// Bind to the lookup user account again to perform group search.
if err = l.LDAP.LookupBind(conn); err != nil {
return "", nil, err
return nil, nil, err
}
// User groups lookup.
groups, err := l.LDAP.SearchForUserGroups(conn, username, bindDN)
groups, err := l.LDAP.SearchForUserGroups(conn, username, lookupResult.NormDN)
if err != nil {
return "", nil, err
return nil, nil, err
}
return bindDN, groups, nil
return lookupResult, groups, nil
}
// GetExpiryDuration - return parsed expiry duration.
@@ -250,7 +252,7 @@ func (l Config) IsLDAPUserDN(user string) bool {
if err != nil {
return false
}
for _, baseDN := range l.LDAP.UserDNSearchBaseDistNames {
for _, baseDN := range l.LDAP.GetUserDNSearchBaseDistNames() {
if baseDN.Parsed.AncestorOf(udn) {
return true
}
@@ -264,7 +266,7 @@ func (l Config) IsLDAPGroupDN(group string) bool {
if err != nil {
return false
}
for _, baseDN := range l.LDAP.GroupSearchBaseDistNames {
for _, baseDN := range l.LDAP.GetGroupSearchBaseDistNames() {
if baseDN.Parsed.AncestorOf(gdn) {
return true
}

View File

@@ -30,8 +30,8 @@ import (
jwtgo "github.com/golang-jwt/jwt/v4"
"github.com/minio/minio/internal/arn"
"github.com/minio/minio/internal/auth"
xnet "github.com/minio/pkg/v2/net"
"github.com/minio/pkg/v2/policy"
xnet "github.com/minio/pkg/v3/net"
"github.com/minio/pkg/v3/policy"
)
type publicKeys struct {

View File

@@ -35,7 +35,7 @@ import (
"github.com/minio/minio/internal/arn"
"github.com/minio/minio/internal/config"
jwtm "github.com/minio/minio/internal/jwt"
xnet "github.com/minio/pkg/v2/net"
xnet "github.com/minio/pkg/v3/net"
)
func TestUpdateClaimsExpiry(t *testing.T) {

View File

@@ -36,9 +36,9 @@ import (
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/config/identity/openid/provider"
"github.com/minio/minio/internal/hash/sha256"
"github.com/minio/pkg/v2/env"
xnet "github.com/minio/pkg/v2/net"
"github.com/minio/pkg/v2/policy"
"github.com/minio/pkg/v3/env"
xnet "github.com/minio/pkg/v3/net"
"github.com/minio/pkg/v3/policy"
)
// OpenID keys and envs.

View File

@@ -28,7 +28,7 @@ import (
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/config/identity/openid/provider"
xhttp "github.com/minio/minio/internal/http"
xnet "github.com/minio/pkg/v2/net"
xnet "github.com/minio/pkg/v3/net"
)
type providerCfg struct {

View File

@@ -34,8 +34,8 @@ import (
"github.com/minio/minio/internal/arn"
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/v2/env"
xnet "github.com/minio/pkg/v2/net"
"github.com/minio/pkg/v3/env"
xnet "github.com/minio/pkg/v3/net"
)
func authNLogIf(ctx context.Context, err error) {

View File

@@ -23,7 +23,7 @@ import (
"github.com/minio/minio/internal/auth"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
const (

View File

@@ -21,7 +21,7 @@ import (
"strconv"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// DefaultKVS default configuration values for ILM subsystem

View File

@@ -27,8 +27,8 @@ import (
"github.com/minio/minio/internal/config/lambda/event"
"github.com/minio/minio/internal/config/lambda/target"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/v2/env"
xnet "github.com/minio/pkg/v2/net"
"github.com/minio/pkg/v3/env"
xnet "github.com/minio/pkg/v3/net"
)
const (

View File

@@ -32,8 +32,8 @@ import (
"github.com/minio/minio/internal/config/lambda/event"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/v2/certs"
xnet "github.com/minio/pkg/v2/net"
"github.com/minio/pkg/v3/certs"
xnet "github.com/minio/pkg/v3/net"
)
// Webhook constants

View File

@@ -32,8 +32,8 @@ import (
"github.com/minio/minio/internal/event"
"github.com/minio/minio/internal/event/target"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/v2/env"
xnet "github.com/minio/pkg/v2/net"
"github.com/minio/pkg/v3/env"
xnet "github.com/minio/pkg/v3/net"
)
const (

View File

@@ -24,9 +24,9 @@ import (
"net/http"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
xnet "github.com/minio/pkg/v2/net"
"github.com/minio/pkg/v2/policy"
"github.com/minio/pkg/v3/env"
xnet "github.com/minio/pkg/v3/net"
"github.com/minio/pkg/v3/policy"
)
// Env IAM OPA URL

View File

@@ -26,8 +26,8 @@ import (
"github.com/minio/minio/internal/config"
xhttp "github.com/minio/minio/internal/http"
xnet "github.com/minio/pkg/v2/net"
"github.com/minio/pkg/v2/policy"
xnet "github.com/minio/pkg/v3/net"
"github.com/minio/pkg/v3/policy"
)
// Authorization Plugin config and env variables

View File

@@ -23,7 +23,7 @@ import (
"time"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// Compression environment variables

View File

@@ -28,7 +28,7 @@ import (
"github.com/dustin/go-humanize"
"github.com/minio/minio/internal/config"
"github.com/minio/minio/internal/logger"
"github.com/minio/pkg/v2/env"
"github.com/minio/pkg/v3/env"
)
// Standard constants for all storage class

View File

@@ -25,8 +25,8 @@ import (
"sync"
"github.com/minio/minio/internal/config"
"github.com/minio/pkg/v2/env"
xnet "github.com/minio/pkg/v2/net"
"github.com/minio/pkg/v3/env"
xnet "github.com/minio/pkg/v3/net"
)
const (