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"
|
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"strings"
|
2021-04-09 15:38:53 +00:00
|
|
|
|
2021-04-12 18:23:08 +00:00
|
|
|
"github.com/go-ldap/ldap/v3"
|
2021-04-09 15:38:53 +00:00
|
|
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
|
|
)
|
|
|
|
|
2021-04-10 01:49:43 +00:00
|
|
|
const (
|
|
|
|
ldapsScheme = "ldaps"
|
|
|
|
)
|
|
|
|
|
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-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 {
|
|
|
|
Dial(ctx context.Context, hostAndPort string) (Conn, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// LDAPDialerFunc makes it easy to use a func as an LDAPDialer.
|
2021-04-10 01:49:43 +00:00
|
|
|
type LDAPDialerFunc func(ctx context.Context, hostAndPort string) (Conn, error)
|
|
|
|
|
2021-04-12 18:23:08 +00:00
|
|
|
func (f LDAPDialerFunc) Dial(ctx context.Context, hostAndPort string) (Conn, error) {
|
|
|
|
return f(ctx, hostAndPort)
|
|
|
|
}
|
|
|
|
|
2021-04-10 01:49:43 +00:00
|
|
|
// Provider includes all of the settings for connection and searching for users and groups in
|
|
|
|
// the upstream LDAP IDP. It also provides methods for testing the connection and performing logins.
|
|
|
|
type Provider struct {
|
|
|
|
// Name is the unique name of this upstream LDAP IDP.
|
|
|
|
Name string
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
// PEM-encoded CA cert bundle to trust when connecting to the LDAP server.
|
|
|
|
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.
|
|
|
|
UserSearch *UserSearch
|
|
|
|
|
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-04-10 01:49:43 +00:00
|
|
|
}
|
|
|
|
|
2021-04-09 15:38:53 +00:00
|
|
|
// UserSearch contains information about how to search for users in the upstream LDAP IDP.
|
|
|
|
type UserSearch struct {
|
|
|
|
// 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-04-10 01:49:43 +00:00
|
|
|
func (p *Provider) dial(ctx context.Context) (Conn, error) {
|
|
|
|
hostAndPort, err := hostAndPortWithDefaultPort(p.Host, ldap.DefaultLdapsPort)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ldap.NewError(ldap.ErrorNetwork, err)
|
|
|
|
}
|
2021-04-12 18:23:08 +00:00
|
|
|
if p.Dialer != nil {
|
|
|
|
return p.Dialer.Dial(ctx, hostAndPort)
|
2021-04-10 01:49:43 +00:00
|
|
|
}
|
|
|
|
return p.dialTLS(ctx, hostAndPort)
|
|
|
|
}
|
2021-04-09 15:38:53 +00:00
|
|
|
|
2021-04-12 18:23:08 +00:00
|
|
|
// dialTLS is the default implementation of the Dialer, used when Dialer is nil.
|
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.
|
|
|
|
func (p *Provider) dialTLS(ctx context.Context, hostAndPort string) (Conn, error) {
|
|
|
|
rootCAs := x509.NewCertPool()
|
|
|
|
if p.CABundle != nil {
|
|
|
|
if !rootCAs.AppendCertsFromPEM(p.CABundle) {
|
|
|
|
return nil, ldap.NewError(ldap.ErrorNetwork, fmt.Errorf("could not parse CA bundle"))
|
|
|
|
}
|
|
|
|
}
|
2021-04-09 15:38:53 +00:00
|
|
|
|
2021-04-10 01:49:43 +00:00
|
|
|
dialer := &tls.Dialer{Config: &tls.Config{
|
|
|
|
MinVersion: tls.VersionTLS12,
|
|
|
|
RootCAs: rootCAs,
|
|
|
|
}}
|
2021-04-09 15:38:53 +00:00
|
|
|
|
2021-04-10 01:49:43 +00:00
|
|
|
c, err := dialer.DialContext(ctx, "tcp", hostAndPort)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ldap.NewError(ldap.ErrorNetwork, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
conn := ldap.NewConn(c, true)
|
|
|
|
conn.Start()
|
|
|
|
return conn, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Adds the default port if hostAndPort did not already include a port.
|
|
|
|
func hostAndPortWithDefaultPort(hostAndPort string, defaultPort string) (string, error) {
|
|
|
|
host, port, err := net.SplitHostPort(hostAndPort)
|
|
|
|
if err != nil {
|
|
|
|
if strings.HasSuffix(err.Error(), ": missing port in address") { // sad to need to do this string compare
|
|
|
|
host = hostAndPort
|
|
|
|
port = defaultPort
|
|
|
|
} else {
|
|
|
|
return "", err // hostAndPort argument was not parsable
|
|
|
|
}
|
|
|
|
}
|
|
|
|
switch {
|
|
|
|
case port != "" && strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]"):
|
|
|
|
// don't add extra square brackets to an IPv6 address that already has them
|
|
|
|
return host + ":" + port, nil
|
|
|
|
case port != "":
|
|
|
|
return net.JoinHostPort(host, port), nil
|
|
|
|
default:
|
|
|
|
return host, nil
|
|
|
|
}
|
2021-04-09 15:38:53 +00:00
|
|
|
}
|
|
|
|
|
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 {
|
|
|
|
return p.Name
|
|
|
|
}
|
|
|
|
|
2021-04-10 01:49:43 +00:00
|
|
|
// Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234".
|
|
|
|
// 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-04-09 15:38:53 +00:00
|
|
|
func (p *Provider) GetURL() string {
|
2021-04-10 01:49:43 +00:00
|
|
|
return fmt.Sprintf("%s://%s", ldapsScheme, p.Host)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestConnection provides a method for testing the connection and bind settings by dialing and binding.
|
|
|
|
func (p *Provider) TestConnection(ctx context.Context) error {
|
|
|
|
_, _ = p.dial(ctx)
|
|
|
|
// TODO bind using the bind credentials
|
|
|
|
// TODO close
|
|
|
|
// TODO return any dial or bind errors
|
|
|
|
return nil
|
2021-04-09 15:38:53 +00:00
|
|
|
}
|
|
|
|
|
2021-04-10 01:49:43 +00:00
|
|
|
// Authenticate a user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator.
|
2021-04-09 15:38:53 +00:00
|
|
|
func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
2021-04-10 01:49:43 +00:00
|
|
|
_, _ = p.dial(ctx)
|
|
|
|
// TODO bind
|
|
|
|
// TODO user search
|
|
|
|
// TODO user bind
|
|
|
|
// TODO map username and uid attributes
|
|
|
|
// TODO group search
|
|
|
|
// TODO map group attributes
|
|
|
|
// TODO close
|
|
|
|
// TODO return any errors that were encountered along the way
|
2021-04-09 15:38:53 +00:00
|
|
|
return nil, false, nil
|
|
|
|
}
|