2021-04-09 15:38:53 +00:00
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package upstreamldap implements an abstraction of upstream LDAP IDP interactions.
package upstreamldap
import (
"context"
2021-04-10 01:49:43 +00:00
"crypto/tls"
"crypto/x509"
2021-05-27 20:47:10 +00:00
"encoding/base64"
2021-04-27 19:43:09 +00:00
"errors"
2021-04-10 01:49:43 +00:00
"fmt"
"net"
2021-05-27 00:04:20 +00:00
"net/url"
2021-08-17 23:53:26 +00:00
"regexp"
2021-05-17 18:10:26 +00:00
"sort"
2021-04-10 01:49:43 +00:00
"strings"
2021-05-12 18:59:48 +00:00
"time"
2021-04-12 18:23:08 +00:00
"github.com/go-ldap/ldap/v3"
2021-08-19 21:21:18 +00:00
"github.com/google/uuid"
2021-10-08 22:48:21 +00:00
"k8s.io/apimachinery/pkg/types"
2021-04-13 00:50:25 +00:00
"k8s.io/apiserver/pkg/authentication/user"
2021-05-27 00:04:20 +00:00
"k8s.io/utils/trace"
2021-04-13 23:22:13 +00:00
2021-05-27 00:04:20 +00:00
"go.pinniped.dev/internal/authenticators"
2021-05-25 19:46:50 +00:00
"go.pinniped.dev/internal/endpointaddr"
2021-10-25 21:25:43 +00:00
"go.pinniped.dev/internal/oidc/downstreamsession"
2021-05-27 00:04:20 +00:00
"go.pinniped.dev/internal/oidc/provider"
2021-04-13 23:22:13 +00:00
"go.pinniped.dev/internal/plog"
2021-04-09 15:38:53 +00:00
)
2021-04-10 01:49:43 +00:00
const (
2021-05-17 18:10:26 +00:00
ldapsScheme = "ldaps"
distinguishedNameAttributeName = "dn"
searchFilterInterpolationLocationMarker = "{}"
groupSearchPageSize = uint32 ( 250 )
2021-05-25 19:46:50 +00:00
defaultLDAPPort = uint16 ( 389 )
defaultLDAPSPort = uint16 ( 636 )
2021-08-19 21:21:18 +00:00
sAMAccountNameAttribute = "sAMAccountName"
2021-04-10 01:49:43 +00:00
)
2021-04-09 15:38:53 +00:00
// Conn abstracts the upstream LDAP communication protocol (mostly for testing).
type Conn interface {
Bind ( username , password string ) error
2021-04-10 01:49:43 +00:00
2021-04-09 15:38:53 +00:00
Search ( searchRequest * ldap . SearchRequest ) ( * ldap . SearchResult , error )
2021-04-10 01:49:43 +00:00
2021-05-17 18:10:26 +00:00
SearchWithPaging ( searchRequest * ldap . SearchRequest , pagingSize uint32 ) ( * ldap . SearchResult , error )
2021-04-09 15:38:53 +00:00
Close ( )
}
2021-04-10 01:49:43 +00:00
// Our Conn type is subset of the ldap.Client interface, which is implemented by ldap.Conn.
var _ Conn = & ldap . Conn { }
2021-04-12 18:23:08 +00:00
// LDAPDialer is a factory of Conn, and the resulting Conn can then be used to interact with an upstream LDAP IDP.
type LDAPDialer interface {
2021-05-25 19:46:50 +00:00
Dial ( ctx context . Context , addr endpointaddr . HostPort ) ( Conn , error )
2021-04-12 18:23:08 +00:00
}
// LDAPDialerFunc makes it easy to use a func as an LDAPDialer.
2021-05-25 19:46:50 +00:00
type LDAPDialerFunc func ( ctx context . Context , addr endpointaddr . HostPort ) ( Conn , error )
2021-04-10 01:49:43 +00:00
2021-04-27 23:54:26 +00:00
var _ LDAPDialer = LDAPDialerFunc ( nil )
2021-04-27 19:43:09 +00:00
2021-05-25 19:46:50 +00:00
func ( f LDAPDialerFunc ) Dial ( ctx context . Context , addr endpointaddr . HostPort ) ( Conn , error ) {
return f ( ctx , addr )
2021-04-12 18:23:08 +00:00
}
2021-05-20 00:17:44 +00:00
type LDAPConnectionProtocol string
const (
StartTLS = LDAPConnectionProtocol ( "StartTLS" )
TLS = LDAPConnectionProtocol ( "TLS" )
)
2021-04-15 17:25:35 +00:00
// ProviderConfig includes all of the settings for connection and searching for users and groups in
2021-04-10 01:49:43 +00:00
// the upstream LDAP IDP. It also provides methods for testing the connection and performing logins.
2021-04-15 17:25:35 +00:00
// The nested structs are not pointer fields to enable deep copy on function params and return values.
type ProviderConfig struct {
2021-04-10 01:49:43 +00:00
// Name is the unique name of this upstream LDAP IDP.
Name string
2021-10-08 22:48:21 +00:00
// ResourceUID is the Kubernetes resource UID of this identity provider.
ResourceUID types . UID
2021-04-10 01:49:43 +00:00
// Host is the hostname or "hostname:port" of the LDAP server. When the port is not specified,
// the default LDAP port will be used.
Host string
2021-05-20 00:17:44 +00:00
// ConnectionProtocol determines how to establish the connection to the server. Either StartTLS or TLS.
ConnectionProtocol LDAPConnectionProtocol
2021-04-14 00:16:57 +00:00
// PEM-encoded CA cert bundle to trust when connecting to the LDAP server. Can be nil.
2021-04-10 01:49:43 +00:00
CABundle [ ] byte
// BindUsername is the username to use when performing a bind with the upstream LDAP IDP.
BindUsername string
// BindPassword is the password to use when performing a bind with the upstream LDAP IDP.
BindPassword string
// UserSearch contains information about how to search for users in the upstream LDAP IDP.
2021-04-15 17:25:35 +00:00
UserSearch UserSearchConfig
2021-04-10 01:49:43 +00:00
2021-05-17 18:10:26 +00:00
// GroupSearch contains information about how to search for group membership in the upstream LDAP IDP.
GroupSearch GroupSearchConfig
2021-04-12 18:23:08 +00:00
// Dialer exists to enable testing. When nil, will use a default appropriate for production use.
Dialer LDAPDialer
2021-07-27 18:08:23 +00:00
2021-08-17 23:53:26 +00:00
// UIDAttributeParsingOverrides are mappings between an attribute name and a way to parse it as a UID when
2021-07-27 18:08:23 +00:00
// it comes out of LDAP.
2021-08-19 21:21:18 +00:00
UIDAttributeParsingOverrides map [ string ] func ( * ldap . Entry ) ( string , error )
2021-08-17 23:53:26 +00:00
// GroupNameMappingOverrides are the mappings between an attribute name and a way to parse it as a group
// name when it comes out of LDAP.
2021-08-19 21:21:18 +00:00
GroupAttributeParsingOverrides map [ string ] func ( * ldap . Entry ) ( string , error )
2021-04-10 01:49:43 +00:00
}
2021-04-15 17:25:35 +00:00
// UserSearchConfig contains information about how to search for users in the upstream LDAP IDP.
type UserSearchConfig struct {
2021-04-09 15:38:53 +00:00
// Base is the base DN to use for the user search in the upstream LDAP IDP.
Base string
2021-04-10 01:49:43 +00:00
2021-04-09 15:38:53 +00:00
// Filter is the filter to use for the user search in the upstream LDAP IDP.
Filter string
2021-04-10 01:49:43 +00:00
2021-04-09 15:38:53 +00:00
// UsernameAttribute is the attribute in the LDAP entry from which the username should be
// retrieved.
UsernameAttribute string
2021-04-10 01:49:43 +00:00
2021-04-09 15:38:53 +00:00
// UIDAttribute is the attribute in the LDAP entry from which the user's unique ID should be
// retrieved.
UIDAttribute string
}
2021-05-17 18:10:26 +00:00
// GroupSearchConfig contains information about how to search for group membership for users in the upstream LDAP IDP.
type GroupSearchConfig struct {
// Base is the base DN to use for the group search in the upstream LDAP IDP. Empty means to skip group search
// entirely, in which case authenticated users will not belong to any groups from the upstream LDAP IDP.
Base string
// Filter is the filter to use for the group search in the upstream LDAP IDP. Empty means to use `member={}`.
Filter string
// GroupNameAttribute is the attribute in the LDAP group entry from which the group name should be
// retrieved. Empty means to use 'cn'.
GroupNameAttribute string
}
2021-04-15 17:25:35 +00:00
type Provider struct {
c ProviderConfig
}
2021-05-27 00:04:20 +00:00
var _ provider . UpstreamLDAPIdentityProviderI = & Provider { }
var _ authenticators . UserAuthenticator = & Provider { }
2021-04-15 17:25:35 +00:00
// Create a Provider. The config is not a pointer to ensure that a copy of the config is created,
// making the resulting Provider use an effectively read-only configuration.
func New ( config ProviderConfig ) * Provider {
return & Provider { c : config }
}
// A reader for the config. Returns a copy of the config to keep the underlying config read-only.
func ( p * Provider ) GetConfig ( ) ProviderConfig {
return p . c
}
2021-11-03 17:33:22 +00:00
func ( p * Provider ) PerformRefresh ( ctx context . Context , userDN , expectedUsername , expectedSubject string ) error {
2021-10-22 20:57:30 +00:00
t := trace . FromContext ( ctx ) . Nest ( "slow ldap refresh attempt" , trace . Field { Key : "providerName" , Value : p . GetName ( ) } )
defer t . LogIfLong ( 500 * time . Millisecond ) // to help users debug slow LDAP searches
2021-11-05 21:18:54 +00:00
searchResult , err := p . performRefresh ( ctx , userDN )
2021-10-22 20:57:30 +00:00
if err != nil {
2021-10-25 23:45:30 +00:00
p . traceRefreshFailure ( t , err )
2021-11-05 21:18:54 +00:00
return err
2021-10-22 20:57:30 +00:00
}
// if any more or less than one entry, error.
// we don't need to worry about logging this because we know it's a dn.
if len ( searchResult . Entries ) != 1 {
return fmt . Errorf ( ` searching for user "%s" resulted in %d search results, but expected 1 result ` ,
userDN , len ( searchResult . Entries ) ,
)
}
2021-10-25 21:25:43 +00:00
userEntry := searchResult . Entries [ 0 ]
if len ( userEntry . DN ) == 0 {
return fmt . Errorf ( ` searching for user with original DN "%s" resulted in search result without DN ` , userDN )
}
newUsername , err := p . getSearchResultAttributeValue ( p . c . UserSearch . UsernameAttribute , userEntry , userDN )
if err != nil {
2021-10-27 00:03:16 +00:00
return err
2021-10-25 21:25:43 +00:00
}
if newUsername != expectedUsername {
return fmt . Errorf ( ` searching for user "%s" returned a different username than the previous value. expected: "%s", actual: "%s" ` ,
userDN , expectedUsername , newUsername ,
)
}
newUID , err := p . getSearchResultAttributeRawValueEncoded ( p . c . UserSearch . UIDAttribute , userEntry , userDN )
if err != nil {
2021-10-27 00:03:16 +00:00
return err
2021-10-25 21:25:43 +00:00
}
newSubject := downstreamsession . DownstreamLDAPSubject ( newUID , * p . GetURL ( ) )
if newSubject != expectedSubject {
return fmt . Errorf ( ` searching for user "%s" produced a different subject than the previous value. expected: "%s", actual: "%s" ` , userDN , expectedSubject , newSubject )
}
2021-10-27 00:03:16 +00:00
// we checked that the user still exists and their information is the same, so just return.
2021-10-22 20:57:30 +00:00
return nil
}
2021-11-05 21:18:54 +00:00
func ( p * Provider ) performRefresh ( ctx context . Context , userDN string ) ( * ldap . SearchResult , error ) {
search := p . refreshUserSearchRequest ( userDN )
conn , err := p . dial ( ctx )
if err != nil {
return nil , fmt . Errorf ( ` error dialing host "%s": %w ` , p . c . Host , err )
}
defer conn . Close ( )
err = conn . Bind ( p . c . BindUsername , p . c . BindPassword )
if err != nil {
return nil , fmt . Errorf ( ` error binding as "%s" before user search: %w ` , p . c . BindUsername , err )
}
searchResult , err := conn . Search ( search )
if err != nil {
return nil , fmt . Errorf ( ` error searching for user "%s": %w ` , userDN , err )
}
return searchResult , nil
}
2021-04-10 01:49:43 +00:00
func ( p * Provider ) dial ( ctx context . Context ) ( Conn , error ) {
2021-05-25 19:46:50 +00:00
tlsAddr , err := endpointaddr . Parse ( p . c . Host , defaultLDAPSPort )
2021-04-10 01:49:43 +00:00
if err != nil {
return nil , ldap . NewError ( ldap . ErrorNetwork , err )
}
2021-05-20 00:17:44 +00:00
2021-05-25 19:46:50 +00:00
startTLSAddr , err := endpointaddr . Parse ( p . c . Host , defaultLDAPPort )
2021-05-20 00:17:44 +00:00
if err != nil {
return nil , ldap . NewError ( ldap . ErrorNetwork , err )
}
2021-05-20 19:46:33 +00:00
// Choose how and where to dial based on TLS vs. StartTLS config option.
var dialFunc LDAPDialerFunc
2021-05-25 19:46:50 +00:00
var addr endpointaddr . HostPort
2021-05-20 00:17:44 +00:00
switch {
case p . c . ConnectionProtocol == TLS :
2021-05-20 19:46:33 +00:00
dialFunc = p . dialTLS
2021-05-25 19:46:50 +00:00
addr = tlsAddr
2021-05-20 00:17:44 +00:00
case p . c . ConnectionProtocol == StartTLS :
2021-05-20 19:46:33 +00:00
dialFunc = p . dialStartTLS
2021-05-25 19:46:50 +00:00
addr = startTLSAddr
2021-05-20 00:17:44 +00:00
default :
return nil , ldap . NewError ( ldap . ErrorNetwork , fmt . Errorf ( "did not specify valid ConnectionProtocol" ) )
2021-04-10 01:49:43 +00:00
}
2021-05-20 19:46:33 +00:00
// Override the real dialer for testing purposes sometimes.
if p . c . Dialer != nil {
dialFunc = p . c . Dialer . Dial
}
2021-05-25 19:46:50 +00:00
return dialFunc ( ctx , addr )
2021-04-10 01:49:43 +00:00
}
2021-04-09 15:38:53 +00:00
2021-05-20 00:17:44 +00:00
// dialTLS is a default implementation of the Dialer, used when Dialer is nil and ConnectionProtocol is TLS.
2021-04-10 01:49:43 +00:00
// Unfortunately, the go-ldap library does not seem to support dialing with a context.Context,
// so we implement it ourselves, heavily inspired by ldap.DialURL.
2021-05-25 19:46:50 +00:00
func ( p * Provider ) dialTLS ( ctx context . Context , addr endpointaddr . HostPort ) ( Conn , error ) {
2021-05-20 00:17:44 +00:00
tlsConfig , err := p . tlsConfig ( )
if err != nil {
return nil , ldap . NewError ( ldap . ErrorNetwork , err )
2021-04-10 01:49:43 +00:00
}
2021-04-09 15:38:53 +00:00
2021-05-20 00:17:44 +00:00
dialer := & tls . Dialer { NetDialer : netDialer ( ) , Config : tlsConfig }
2021-05-25 19:46:50 +00:00
c , err := dialer . DialContext ( ctx , "tcp" , addr . Endpoint ( ) )
2021-04-10 01:49:43 +00:00
if err != nil {
return nil , ldap . NewError ( ldap . ErrorNetwork , err )
}
conn := ldap . NewConn ( c , true )
conn . Start ( )
return conn , nil
}
2021-05-20 00:17:44 +00:00
// dialTLS is a default implementation of the Dialer, used when Dialer is nil and ConnectionProtocol is StartTLS.
// Unfortunately, the go-ldap library does not seem to support dialing with a context.Context,
// so we implement it ourselves, heavily inspired by ldap.DialURL.
2021-05-25 19:46:50 +00:00
func ( p * Provider ) dialStartTLS ( ctx context . Context , addr endpointaddr . HostPort ) ( Conn , error ) {
2021-05-20 00:17:44 +00:00
tlsConfig , err := p . tlsConfig ( )
if err != nil {
return nil , ldap . NewError ( ldap . ErrorNetwork , err )
}
// Unfortunately, this seems to be required for StartTLS, even though it is not needed for regular TLS.
2021-05-25 19:46:50 +00:00
tlsConfig . ServerName = addr . Host
2021-05-20 00:17:44 +00:00
2021-05-25 19:46:50 +00:00
c , err := netDialer ( ) . DialContext ( ctx , "tcp" , addr . Endpoint ( ) )
2021-05-20 00:17:44 +00:00
if err != nil {
return nil , ldap . NewError ( ldap . ErrorNetwork , err )
}
conn := ldap . NewConn ( c , false )
conn . Start ( )
err = conn . StartTLS ( tlsConfig )
if err != nil {
return nil , err
}
return conn , nil
}
func netDialer ( ) * net . Dialer {
return & net . Dialer { Timeout : time . Minute }
}
func ( p * Provider ) tlsConfig ( ) ( * tls . Config , error ) {
var rootCAs * x509 . CertPool
if p . c . CABundle != nil {
rootCAs = x509 . NewCertPool ( )
if ! rootCAs . AppendCertsFromPEM ( p . c . CABundle ) {
return nil , fmt . Errorf ( "could not parse CA bundle" )
}
}
return & tls . Config { MinVersion : tls . VersionTLS12 , RootCAs : rootCAs } , nil
}
2021-04-10 01:49:43 +00:00
// A name for this upstream provider.
2021-04-09 15:38:53 +00:00
func ( p * Provider ) GetName ( ) string {
2021-04-15 17:25:35 +00:00
return p . c . Name
2021-04-09 15:38:53 +00:00
}
2021-10-08 22:48:21 +00:00
func ( p * Provider ) GetResourceUID ( ) types . UID {
return p . c . ResourceUID
}
2021-05-27 00:04:20 +00:00
// Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234?base=user-search-base".
2021-04-10 01:49:43 +00:00
// This URL is not used for connecting to the provider, but rather is used for creating a globally unique user
// identifier by being combined with the user's UID, since user UIDs are only unique within one provider.
2021-05-27 00:04:20 +00:00
func ( p * Provider ) GetURL ( ) * url . URL {
u := & url . URL { Scheme : ldapsScheme , Host : p . c . Host }
q := u . Query ( )
q . Set ( "base" , p . c . UserSearch . Base )
u . RawQuery = q . Encode ( )
return u
2021-04-10 01:49:43 +00:00
}
2021-04-13 00:50:25 +00:00
// TestConnection provides a method for testing the connection and bind settings. It performs a dial and bind
// and returns any errors that we encountered.
2021-04-15 21:44:43 +00:00
func ( p * Provider ) TestConnection ( ctx context . Context ) error {
err := p . validateConfig ( )
if err != nil {
return err
}
conn , err := p . dial ( ctx )
if err != nil {
return fmt . Errorf ( ` error dialing host "%s": %w ` , p . c . Host , err )
}
defer conn . Close ( )
err = conn . Bind ( p . c . BindUsername , p . c . BindPassword )
if err != nil {
return fmt . Errorf ( ` error binding as "%s": %w ` , p . c . BindUsername , err )
}
return nil
2021-04-13 00:50:25 +00:00
}
2021-04-16 21:04:05 +00:00
// DryRunAuthenticateUser provides a method for testing all of the Provider settings in a kind of dry run of
// authentication for a given end user's username. It runs the same logic as AuthenticateUser except it does
// not bind as that user, so it does not test their password. It returns the same values that a real call to
2021-04-13 00:50:25 +00:00
// AuthenticateUser with the correct password would return.
2021-11-03 17:33:22 +00:00
func ( p * Provider ) DryRunAuthenticateUser ( ctx context . Context , username string ) ( * authenticators . Response , bool , error ) {
2021-04-16 21:04:05 +00:00
endUserBindFunc := func ( conn Conn , foundUserDN string ) error {
// Act as if the end user bind always succeeds.
return nil
}
return p . authenticateUserImpl ( ctx , username , endUserBindFunc )
2021-04-09 15:38:53 +00:00
}
2021-04-16 21:04:05 +00:00
// Authenticate an end user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator.
2021-11-03 17:33:22 +00:00
func ( p * Provider ) AuthenticateUser ( ctx context . Context , username , password string ) ( * authenticators . Response , bool , error ) {
2021-04-16 21:04:05 +00:00
endUserBindFunc := func ( conn Conn , foundUserDN string ) error {
return conn . Bind ( foundUserDN , password )
}
return p . authenticateUserImpl ( ctx , username , endUserBindFunc )
}
2021-11-03 17:33:22 +00:00
func ( p * Provider ) authenticateUserImpl ( ctx context . Context , username string , bindFunc func ( conn Conn , foundUserDN string ) error ) ( * authenticators . Response , bool , error ) {
2021-05-12 18:59:48 +00:00
t := trace . FromContext ( ctx ) . Nest ( "slow ldap authenticate user attempt" , trace . Field { Key : "providerName" , Value : p . GetName ( ) } )
defer t . LogIfLong ( 500 * time . Millisecond ) // to help users debug slow LDAP searches
2021-04-15 21:44:43 +00:00
err := p . validateConfig ( )
if err != nil {
2021-05-12 18:59:48 +00:00
p . traceAuthFailure ( t , err )
2021-04-15 21:44:43 +00:00
return nil , false , err
2021-04-13 22:23:14 +00:00
}
2021-04-13 23:22:13 +00:00
if len ( username ) == 0 {
// Empty passwords are already handled by go-ldap.
2021-05-12 18:59:48 +00:00
p . traceAuthFailure ( t , fmt . Errorf ( "empty username" ) )
2021-04-13 23:22:13 +00:00
return nil , false , nil
}
2021-04-13 00:50:25 +00:00
conn , err := p . dial ( ctx )
if err != nil {
2021-05-12 18:59:48 +00:00
p . traceAuthFailure ( t , err )
2021-04-15 17:25:35 +00:00
return nil , false , fmt . Errorf ( ` error dialing host "%s": %w ` , p . c . Host , err )
2021-04-13 00:50:25 +00:00
}
defer conn . Close ( )
2021-04-15 17:25:35 +00:00
err = conn . Bind ( p . c . BindUsername , p . c . BindPassword )
2021-04-13 00:50:25 +00:00
if err != nil {
2021-05-12 18:59:48 +00:00
p . traceAuthFailure ( t , err )
2021-04-15 17:25:35 +00:00
return nil , false , fmt . Errorf ( ` error binding as "%s" before user search: %w ` , p . c . BindUsername , err )
2021-04-13 00:50:25 +00:00
}
2021-11-03 22:17:50 +00:00
response , err := p . searchAndBindUser ( conn , username , bindFunc )
2021-04-13 00:50:25 +00:00
if err != nil {
2021-05-12 18:59:48 +00:00
p . traceAuthFailure ( t , err )
2021-04-13 00:50:25 +00:00
return nil , false , err
}
2021-11-03 22:17:50 +00:00
if response == nil {
2021-05-12 18:59:48 +00:00
p . traceAuthFailure ( t , fmt . Errorf ( "bad username or password" ) )
2021-04-13 23:22:13 +00:00
return nil , false , nil
}
2021-04-13 00:50:25 +00:00
2021-05-12 18:59:48 +00:00
p . traceAuthSuccess ( t )
2021-04-13 00:50:25 +00:00
return response , true , nil
}
2021-05-17 18:10:26 +00:00
func ( p * Provider ) searchGroupsForUserDN ( conn Conn , userDN string ) ( [ ] string , error ) {
searchResult , err := conn . SearchWithPaging ( p . groupSearchRequest ( userDN ) , groupSearchPageSize )
if err != nil {
return nil , fmt . Errorf ( ` error searching for group memberships for user with DN %q: %w ` , userDN , err )
}
groupAttributeName := p . c . GroupSearch . GroupNameAttribute
if len ( groupAttributeName ) == 0 {
2021-05-28 20:27:11 +00:00
groupAttributeName = distinguishedNameAttributeName
2021-05-17 18:10:26 +00:00
}
groups := [ ] string { }
2021-08-18 17:11:18 +00:00
entries :
2021-05-17 18:10:26 +00:00
for _ , groupEntry := range searchResult . Entries {
if len ( groupEntry . DN ) == 0 {
return nil , fmt . Errorf ( ` searching for group memberships for user with DN %q resulted in search result without DN ` , userDN )
}
2021-08-19 21:21:18 +00:00
if overrideFunc := p . c . GroupAttributeParsingOverrides [ groupAttributeName ] ; overrideFunc != nil {
overrideGroupName , err := overrideFunc ( groupEntry )
if err != nil {
return nil , fmt . Errorf ( "error finding groups for user %s: %w" , userDN , err )
2021-08-17 23:53:26 +00:00
}
2021-08-19 21:21:18 +00:00
groups = append ( groups , overrideGroupName )
continue entries
2021-08-17 23:53:26 +00:00
}
2021-08-18 17:11:18 +00:00
// if none of the overrides matched, use the default behavior (no mapping)
mappedGroupName , err := p . getSearchResultAttributeValue ( groupAttributeName , groupEntry , userDN )
if err != nil {
return nil , fmt . Errorf ( ` error searching for group memberships for user with DN %q: %w ` , userDN , err )
}
2021-05-17 18:10:26 +00:00
groups = append ( groups , mappedGroupName )
}
return groups , nil
}
2021-04-15 21:44:43 +00:00
func ( p * Provider ) validateConfig ( ) error {
if p . c . UserSearch . UsernameAttribute == distinguishedNameAttributeName && len ( p . c . UserSearch . Filter ) == 0 {
// LDAP search filters do not allow searching by DN, so we would have no reasonable default for Filter.
return fmt . Errorf ( ` must specify UserSearch Filter when UserSearch UsernameAttribute is "dn" ` )
}
return nil
}
2021-07-21 20:24:54 +00:00
func ( p * Provider ) SearchForDefaultNamingContext ( ctx context . Context ) ( string , error ) {
2021-07-26 23:03:12 +00:00
t := trace . FromContext ( ctx ) . Nest ( "slow ldap attempt when searching for default naming context" , trace . Field { Key : "providerName" , Value : p . GetName ( ) } )
2021-07-21 20:24:54 +00:00
defer t . LogIfLong ( 500 * time . Millisecond ) // to help users debug slow LDAP searches
conn , err := p . dial ( ctx )
if err != nil {
2021-07-26 23:32:46 +00:00
p . traceSearchBaseDiscoveryFailure ( t , err )
2021-07-21 20:24:54 +00:00
return "" , fmt . Errorf ( ` error dialing host "%s": %w ` , p . c . Host , err )
}
defer conn . Close ( )
err = conn . Bind ( p . c . BindUsername , p . c . BindPassword )
if err != nil {
2021-07-26 23:32:46 +00:00
p . traceSearchBaseDiscoveryFailure ( t , err )
2021-07-26 23:03:12 +00:00
return "" , fmt . Errorf ( ` error binding as "%s" before querying for defaultNamingContext: %w ` , p . c . BindUsername , err )
2021-07-21 20:24:54 +00:00
}
searchResult , err := conn . Search ( p . defaultNamingContextRequest ( ) )
if err != nil {
return "" , fmt . Errorf ( ` error querying RootDSE for defaultNamingContext: %w ` , err )
}
2021-07-21 22:02:59 +00:00
if len ( searchResult . Entries ) != 1 {
return "" , fmt . Errorf ( ` error querying RootDSE for defaultNamingContext: expected to find 1 entry but found %d ` , len ( searchResult . Entries ) )
}
searchBase := searchResult . Entries [ 0 ] . GetAttributeValue ( "defaultNamingContext" )
if searchBase == "" {
// if we get an empty search base back, treat it like an error. Otherwise we might make too broad of a search.
return "" , fmt . Errorf ( ` error querying RootDSE for defaultNamingContext: empty search base DN found ` )
}
return searchBase , nil
2021-07-21 20:24:54 +00:00
}
2021-11-03 22:17:50 +00:00
func ( p * Provider ) searchAndBindUser ( conn Conn , username string , bindFunc func ( conn Conn , foundUserDN string ) error ) ( * authenticators . Response , error ) {
2021-04-13 00:50:25 +00:00
searchResult , err := conn . Search ( p . userSearchRequest ( username ) )
if err != nil {
2021-05-28 21:37:31 +00:00
plog . All ( ` error searching for user ` ,
"upstreamName" , p . GetName ( ) ,
"username" , username ,
"err" , err ,
)
2021-11-03 22:17:50 +00:00
return nil , fmt . Errorf ( ` error searching for user: %w ` , err )
2021-04-13 00:50:25 +00:00
}
2021-04-13 23:22:13 +00:00
if len ( searchResult . Entries ) == 0 {
2021-05-28 21:37:31 +00:00
if plog . Enabled ( plog . LevelAll ) {
plog . All ( "error finding user: user not found (if this username is valid, please check the user search configuration)" ,
"upstreamName" , p . GetName ( ) ,
"username" , username ,
)
} else {
plog . Debug ( "error finding user: user not found (cowardly avoiding printing username because log level is not 'all')" , "upstreamName" , p . GetName ( ) )
}
2021-11-03 22:17:50 +00:00
return nil , nil
2021-04-13 23:22:13 +00:00
}
2021-05-28 21:37:31 +00:00
// At this point, we have matched at least one entry, so we can be confident that the username is not actually
// someone's password mistakenly entered into the username field, so we can log it without concern.
2021-04-13 23:22:13 +00:00
if len ( searchResult . Entries ) > 1 {
2021-11-03 22:17:50 +00:00
return nil , fmt . Errorf ( ` searching for user "%s" resulted in %d search results, but expected 1 result ` ,
2021-04-13 00:50:25 +00:00
username , len ( searchResult . Entries ) ,
)
}
userEntry := searchResult . Entries [ 0 ]
if len ( userEntry . DN ) == 0 {
2021-11-03 22:17:50 +00:00
return nil , fmt . Errorf ( ` searching for user "%s" resulted in search result without DN ` , username )
2021-04-13 00:50:25 +00:00
}
2021-04-15 17:25:35 +00:00
mappedUsername , err := p . getSearchResultAttributeValue ( p . c . UserSearch . UsernameAttribute , userEntry , username )
2021-04-13 00:50:25 +00:00
if err != nil {
2021-11-03 22:17:50 +00:00
return nil , err
2021-04-13 00:50:25 +00:00
}
2021-05-27 20:47:10 +00:00
// We would like to support binary typed attributes for UIDs, so always read them as binary and encode them,
// even when the attribute may not be binary.
mappedUID , err := p . getSearchResultAttributeRawValueEncoded ( p . c . UserSearch . UIDAttribute , userEntry , username )
2021-04-13 00:50:25 +00:00
if err != nil {
2021-11-03 22:17:50 +00:00
return nil , err
2021-05-17 18:10:26 +00:00
}
mappedGroupNames := [ ] string { }
if len ( p . c . GroupSearch . Base ) > 0 {
mappedGroupNames , err = p . searchGroupsForUserDN ( conn , userEntry . DN )
if err != nil {
2021-11-03 22:17:50 +00:00
return nil , err
2021-05-17 18:10:26 +00:00
}
2021-04-13 00:50:25 +00:00
}
2021-05-17 18:10:26 +00:00
sort . Strings ( mappedGroupNames )
2021-04-13 00:50:25 +00:00
2021-04-13 23:22:13 +00:00
// Caution: Note that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername!
2021-04-16 21:04:05 +00:00
err = bindFunc ( conn , userEntry . DN )
2021-04-13 00:50:25 +00:00
if err != nil {
2021-04-13 23:22:13 +00:00
plog . DebugErr ( "error binding for user (if this is not the expected dn for this username, please check the user search configuration)" ,
err , "upstreamName" , p . GetName ( ) , "username" , username , "dn" , userEntry . DN )
2021-04-27 19:43:09 +00:00
ldapErr := & ldap . Error { }
if errors . As ( err , & ldapErr ) && ldapErr . ResultCode == ldap . LDAPResultInvalidCredentials {
2021-11-03 22:17:50 +00:00
return nil , nil
2021-04-13 23:22:13 +00:00
}
2021-11-03 22:17:50 +00:00
return nil , fmt . Errorf ( ` error binding for user "%s" using provided password against DN "%s": %w ` , username , userEntry . DN , err )
}
if len ( mappedUsername ) == 0 || len ( mappedUID ) == 0 {
// Couldn't find the username or couldn't bind using the password.
return nil , nil
}
response := & authenticators . Response {
User : & user . DefaultInfo {
Name : mappedUsername ,
UID : mappedUID ,
Groups : mappedGroupNames ,
} ,
DN : userEntry . DN ,
2021-04-13 00:50:25 +00:00
}
2021-11-03 22:17:50 +00:00
return response , nil
2021-04-13 00:50:25 +00:00
}
2021-07-21 20:24:54 +00:00
func ( p * Provider ) defaultNamingContextRequest ( ) * ldap . SearchRequest {
return & ldap . SearchRequest {
BaseDN : "" ,
Scope : ldap . ScopeBaseObject ,
DerefAliases : ldap . NeverDerefAliases ,
SizeLimit : 2 ,
TimeLimit : 90 ,
TypesOnly : false ,
Filter : "(objectClass=*)" ,
Attributes : [ ] string { "defaultNamingContext" } ,
Controls : nil , // don't need paging because we set the SizeLimit so small
}
}
2021-04-13 00:50:25 +00:00
func ( p * Provider ) userSearchRequest ( username string ) * ldap . SearchRequest {
// See https://ldap.com/the-ldap-search-operation for general documentation of LDAP search options.
return & ldap . SearchRequest {
2021-04-15 17:25:35 +00:00
BaseDN : p . c . UserSearch . Base ,
2021-04-13 00:50:25 +00:00
Scope : ldap . ScopeWholeSubtree ,
2021-04-27 19:43:09 +00:00
DerefAliases : ldap . NeverDerefAliases ,
2021-04-13 00:50:25 +00:00
SizeLimit : 2 ,
TimeLimit : 90 ,
TypesOnly : false ,
Filter : p . userSearchFilter ( username ) ,
Attributes : p . userSearchRequestedAttributes ( ) ,
Controls : nil , // this could be used to enable paging, but we're already limiting the result max size
}
}
2021-05-17 18:10:26 +00:00
func ( p * Provider ) groupSearchRequest ( userDN string ) * ldap . SearchRequest {
// See https://ldap.com/the-ldap-search-operation for general documentation of LDAP search options.
return & ldap . SearchRequest {
BaseDN : p . c . GroupSearch . Base ,
Scope : ldap . ScopeWholeSubtree ,
DerefAliases : ldap . NeverDerefAliases ,
SizeLimit : 0 , // unlimited size because we will search with paging
TimeLimit : 90 ,
TypesOnly : false ,
Filter : p . groupSearchFilter ( userDN ) ,
Attributes : p . groupSearchRequestedAttributes ( ) ,
Controls : nil , // nil because ldap.SearchWithPaging() will set the appropriate controls for us
}
}
2021-10-22 20:57:30 +00:00
func ( p * Provider ) refreshUserSearchRequest ( dn string ) * ldap . SearchRequest {
// See https://ldap.com/the-ldap-search-operation for general documentation of LDAP search options.
return & ldap . SearchRequest {
BaseDN : dn ,
Scope : ldap . ScopeBaseObject ,
DerefAliases : ldap . NeverDerefAliases ,
SizeLimit : 2 ,
TimeLimit : 90 ,
TypesOnly : false ,
2021-10-26 22:01:09 +00:00
Filter : "(objectClass=*)" , // we already have the dn, so the filter doesn't matter
Attributes : p . userSearchRequestedAttributes ( ) ,
Controls : nil , // this could be used to enable paging, but we're already limiting the result max size
2021-10-22 20:57:30 +00:00
}
}
2021-04-13 00:50:25 +00:00
func ( p * Provider ) userSearchRequestedAttributes ( ) [ ] string {
attributes := [ ] string { }
2021-04-15 17:25:35 +00:00
if p . c . UserSearch . UsernameAttribute != distinguishedNameAttributeName {
attributes = append ( attributes , p . c . UserSearch . UsernameAttribute )
2021-04-13 00:50:25 +00:00
}
2021-04-15 17:25:35 +00:00
if p . c . UserSearch . UIDAttribute != distinguishedNameAttributeName {
attributes = append ( attributes , p . c . UserSearch . UIDAttribute )
2021-04-13 00:50:25 +00:00
}
return attributes
}
2021-05-17 18:10:26 +00:00
func ( p * Provider ) groupSearchRequestedAttributes ( ) [ ] string {
switch p . c . GroupSearch . GroupNameAttribute {
case "" :
2021-05-28 20:27:11 +00:00
return [ ] string { }
2021-05-17 18:10:26 +00:00
case distinguishedNameAttributeName :
return [ ] string { }
default :
return [ ] string { p . c . GroupSearch . GroupNameAttribute }
}
}
2021-04-13 00:50:25 +00:00
func ( p * Provider ) userSearchFilter ( username string ) string {
safeUsername := p . escapeUsernameForSearchFilter ( username )
2021-04-15 17:25:35 +00:00
if len ( p . c . UserSearch . Filter ) == 0 {
return fmt . Sprintf ( "(%s=%s)" , p . c . UserSearch . UsernameAttribute , safeUsername )
2021-04-13 22:23:14 +00:00
}
2021-05-17 18:10:26 +00:00
return interpolateSearchFilter ( p . c . UserSearch . Filter , safeUsername )
}
func ( p * Provider ) groupSearchFilter ( userDN string ) string {
if len ( p . c . GroupSearch . Filter ) == 0 {
return fmt . Sprintf ( "(member=%s)" , userDN )
}
return interpolateSearchFilter ( p . c . GroupSearch . Filter , userDN )
}
func interpolateSearchFilter ( filterFormat , valueToInterpolateIntoFilter string ) string {
filter := strings . ReplaceAll ( filterFormat , searchFilterInterpolationLocationMarker , valueToInterpolateIntoFilter )
2021-04-13 22:23:14 +00:00
if strings . HasPrefix ( filter , "(" ) && strings . HasSuffix ( filter , ")" ) {
return filter
2021-04-13 00:50:25 +00:00
}
2021-04-13 22:23:14 +00:00
return "(" + filter + ")"
2021-04-13 00:50:25 +00:00
}
func ( p * Provider ) escapeUsernameForSearchFilter ( username string ) string {
2021-04-16 21:04:05 +00:00
// The username is end user input, so it should be escaped before being included in a search to prevent query injection.
2021-04-13 00:50:25 +00:00
return ldap . EscapeFilter ( username )
}
2021-05-27 20:47:10 +00:00
// Returns the (potentially) binary data of the attribute's value, base64 URL encoded.
func ( p * Provider ) getSearchResultAttributeRawValueEncoded ( attributeName string , entry * ldap . Entry , username string ) ( string , error ) {
if attributeName == distinguishedNameAttributeName {
return base64 . RawURLEncoding . EncodeToString ( [ ] byte ( entry . DN ) ) , nil
}
attributeValues := entry . GetRawAttributeValues ( attributeName )
if len ( attributeValues ) != 1 {
return "" , fmt . Errorf ( ` found %d values for attribute "%s" while searching for user "%s", but expected 1 result ` ,
len ( attributeValues ) , attributeName , username ,
)
}
attributeValue := attributeValues [ 0 ]
if len ( attributeValue ) == 0 {
return "" , fmt . Errorf ( ` found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty ` ,
attributeName , username ,
)
}
2021-08-19 21:21:18 +00:00
if overrideFunc := p . c . UIDAttributeParsingOverrides [ attributeName ] ; overrideFunc != nil {
return overrideFunc ( entry )
2021-07-27 18:08:23 +00:00
}
2021-05-27 20:47:10 +00:00
return base64 . RawURLEncoding . EncodeToString ( attributeValue ) , nil
}
2021-05-17 18:10:26 +00:00
func ( p * Provider ) getSearchResultAttributeValue ( attributeName string , entry * ldap . Entry , username string ) ( string , error ) {
2021-04-13 00:50:25 +00:00
if attributeName == distinguishedNameAttributeName {
2021-05-17 18:10:26 +00:00
return entry . DN , nil
2021-04-13 00:50:25 +00:00
}
2021-05-17 18:10:26 +00:00
attributeValues := entry . GetAttributeValues ( attributeName )
2021-04-13 00:50:25 +00:00
if len ( attributeValues ) != 1 {
return "" , fmt . Errorf ( ` found %d values for attribute "%s" while searching for user "%s", but expected 1 result ` ,
len ( attributeValues ) , attributeName , username ,
)
}
attributeValue := attributeValues [ 0 ]
if len ( attributeValue ) == 0 {
return "" , fmt . Errorf ( ` found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty ` ,
attributeName , username ,
)
}
return attributeValue , nil
2021-04-09 15:38:53 +00:00
}
2021-05-12 18:59:48 +00:00
func ( p * Provider ) traceAuthFailure ( t * trace . Trace , err error ) {
t . Step ( "authentication failed" ,
trace . Field { Key : "authenticated" , Value : false } ,
trace . Field { Key : "reason" , Value : err . Error ( ) } ,
)
}
func ( p * Provider ) traceAuthSuccess ( t * trace . Trace ) {
t . Step ( "authentication succeeded" ,
trace . Field { Key : "authenticated" , Value : true } ,
)
}
2021-07-26 23:32:46 +00:00
func ( p * Provider ) traceSearchBaseDiscoveryFailure ( t * trace . Trace , err error ) {
t . Step ( "search base discovery failed" ,
trace . Field { Key : "reason" , Value : err . Error ( ) } )
}
2021-07-27 18:08:23 +00:00
2021-10-25 23:45:30 +00:00
func ( p * Provider ) traceRefreshFailure ( t * trace . Trace , err error ) {
t . Step ( "refresh failed" ,
trace . Field { Key : "reason" , Value : err . Error ( ) } ,
)
}
2021-08-17 23:53:26 +00:00
func MicrosoftUUIDFromBinary ( attributeName string ) func ( entry * ldap . Entry ) ( string , error ) {
// validation has already been done so we can just get the attribute...
return func ( entry * ldap . Entry ) ( string , error ) {
binaryUUID := entry . GetRawAttributeValue ( attributeName )
return microsoftUUIDFromBinary ( binaryUUID )
}
}
func microsoftUUIDFromBinary ( binaryUUID [ ] byte ) ( string , error ) {
2021-07-27 18:08:23 +00:00
uuidVal , err := uuid . FromBytes ( binaryUUID ) // start out with the RFC4122 version
if err != nil {
return "" , err
}
// then swap it because AD stores the first 3 fields little-endian rather than the expected
// big-endian.
uuidVal [ 0 ] , uuidVal [ 1 ] , uuidVal [ 2 ] , uuidVal [ 3 ] = uuidVal [ 3 ] , uuidVal [ 2 ] , uuidVal [ 1 ] , uuidVal [ 0 ]
uuidVal [ 4 ] , uuidVal [ 5 ] = uuidVal [ 5 ] , uuidVal [ 4 ]
uuidVal [ 6 ] , uuidVal [ 7 ] = uuidVal [ 7 ] , uuidVal [ 6 ]
return uuidVal . String ( ) , nil
}
2021-08-17 23:53:26 +00:00
func GroupSAMAccountNameWithDomainSuffix ( entry * ldap . Entry ) ( string , error ) {
2021-08-18 17:11:18 +00:00
sAMAccountNameAttributeValues := entry . GetAttributeValues ( sAMAccountNameAttribute )
if len ( sAMAccountNameAttributeValues ) != 1 {
return "" , fmt . Errorf ( ` found %d values for attribute "%s", but expected 1 result ` ,
len ( sAMAccountNameAttributeValues ) , sAMAccountNameAttribute ,
)
}
sAMAccountName := sAMAccountNameAttributeValues [ 0 ]
if len ( sAMAccountName ) == 0 {
return "" , fmt . Errorf ( ` found empty value for attribute "%s", but expected value to be non-empty ` ,
sAMAccountNameAttribute ,
)
}
2021-08-17 23:53:26 +00:00
distinguishedName := entry . DN
domain , err := getDomainFromDistinguishedName ( distinguishedName )
if err != nil {
return "" , err
}
return sAMAccountName + "@" + domain , nil
}
2021-08-19 21:21:18 +00:00
var domainComponentsRegexp = regexp . MustCompile ( ",DC=|,dc=" )
2021-08-17 23:53:26 +00:00
func getDomainFromDistinguishedName ( distinguishedName string ) ( string , error ) {
2021-08-19 21:21:18 +00:00
domainComponents := domainComponentsRegexp . Split ( distinguishedName , - 1 )
2021-08-17 23:53:26 +00:00
if len ( domainComponents ) == 1 {
return "" , fmt . Errorf ( "did not find domain components in group dn: %s" , distinguishedName )
}
return strings . Join ( domainComponents [ 1 : ] , "." ) , nil
}