diff --git a/pkg/apiserver/authentication/identityprovider/ldap_provider.go b/pkg/apiserver/authentication/identityprovider/ldap_provider.go index 3085a8fdf..1ab6e6aae 100644 --- a/pkg/apiserver/authentication/identityprovider/ldap_provider.go +++ b/pkg/apiserver/authentication/identityprovider/ldap_provider.go @@ -19,27 +19,62 @@ package identityprovider import ( + "crypto/tls" + "crypto/x509" + "encoding/base64" "fmt" "github.com/go-ldap/ldap" - "gopkg.in/yaml.v3" + "github.com/mitchellh/mapstructure" + "io/ioutil" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog" iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" + "kubesphere.io/kubesphere/pkg/constants" + "time" ) -const LdapIdentityProvider = "LDAPIdentityProvider" +const ( + LdapIdentityProvider = "LDAPIdentityProvider" + defaultReadTimeout = 15000 +) type LdapProvider interface { Authenticate(username string, password string) (*iamv1alpha2.User, error) } type ldapOptions struct { - Host string `json:"host" yaml:"host"` - ManagerDN string `json:"managerDN" yaml:"managerDN"` - ManagerPassword string `json:"-" yaml:"managerPassword"` - UserSearchBase string `json:"userSearchBase" yaml:"userSearchBase"` - //This is typically uid + // Host and optional port of the LDAP server in the form "host:port". + // If the port is not supplied, 389 for insecure or StartTLS connections, 636 + Host string `json:"host,omitempty" yaml:"managerDN"` + // Timeout duration when reading data from remote server. Default to 15s. + ReadTimeout int `json:"readTimeout" yaml:"readTimeout"` + // If specified, connections will use the ldaps:// protocol + StartTLS bool `json:"startTLS,omitempty" yaml:"startTLS"` + // Used to turn off TLS certificate checks + InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"` + // Path to a trusted root certificate file. Default: use the host's root CA. + RootCA string `json:"rootCA,omitempty" yaml:"rootCA"` + // A raw certificate file can also be provided inline. Base64 encoded PEM file + RootCAData string `json:"rootCAData,omitempty" yaml:"rootCAData"` + // Username (DN) of the "manager" user identity. + ManagerDN string `json:"managerDN,omitempty" yaml:"managerDN"` + // The password for the manager DN. + ManagerPassword string `json:"-,omitempty" yaml:"managerPassword"` + // User search scope. + UserSearchBase string `json:"userSearchBase,omitempty" yaml:"userSearchBase"` + // LDAP filter used to identify objects of type user. e.g. (objectClass=person) + UserSearchFilter string `json:"userSearchFilter,omitempty" yaml:"userSearchFilter"` + // Group search scope. + GroupSearchBase string `json:"groupSearchBase,omitempty" yaml:"groupSearchBase"` + // LDAP filter used to identify objects of type group. e.g. (objectclass=group) + GroupSearchFilter string `json:"groupSearchFilter,omitempty" yaml:"groupSearchFilter"` + // Attribute on a user object storing the groups the user is a member of. + UserMemberAttribute string `json:"userMemberAttribute,omitempty" yaml:"userMemberAttribute"` + // Attribute on a group object storing the information for primary group membership. + GroupMemberAttribute string `json:"groupMemberAttribute,omitempty" yaml:"groupMemberAttribute"` + // login attribute used for comparing user entries. + // The following three fields are direct mappings of attributes on the user entry. LoginAttribute string `json:"loginAttribute" yaml:"loginAttribute"` MailAttribute string `json:"mailAttribute" yaml:"mailAttribute"` DisplayNameAttribute string `json:"displayNameAttribute" yaml:"displayNameAttribute"` @@ -50,24 +85,23 @@ type ldapProvider struct { } func NewLdapProvider(options *oauth.DynamicOptions) (LdapProvider, error) { - data, err := yaml.Marshal(options) - if err != nil { + var ldapOptions ldapOptions + if err := mapstructure.Decode(options, &ldapOptions); err != nil { return nil, err } - var ldapOptions ldapOptions - err = yaml.Unmarshal(data, &ldapOptions) - if err != nil { - return nil, err + if ldapOptions.ReadTimeout <= 0 { + ldapOptions.ReadTimeout = defaultReadTimeout } return &ldapProvider{options: ldapOptions}, nil } func (l ldapProvider) Authenticate(username string, password string) (*iamv1alpha2.User, error) { - conn, err := ldap.Dial("tcp", l.options.Host) + conn, err := l.newConn() if err != nil { klog.Error(err) return nil, err } + conn.SetTimeout(time.Duration(l.options.ReadTimeout) * time.Millisecond) defer conn.Close() err = conn.Bind(l.options.ManagerDN, l.options.ManagerPassword) @@ -76,8 +110,7 @@ func (l ldapProvider) Authenticate(username string, password string) (*iamv1alph return nil, err } - filter := fmt.Sprintf("(&(%s=%s))", l.options.LoginAttribute, username) - + filter := fmt.Sprintf("(&(%s=%s)%s)", l.options.LoginAttribute, username, l.options.UserSearchFilter) result, err := conn.Search(&ldap.SearchRequest{ BaseDN: l.options.UserSearchBase, Scope: ldap.ScopeWholeSubtree, @@ -88,7 +121,6 @@ func (l ldapProvider) Authenticate(username string, password string) (*iamv1alph Filter: filter, Attributes: []string{l.options.LoginAttribute, l.options.MailAttribute, l.options.DisplayNameAttribute}, }) - if err != nil { klog.Error(err) return nil, err @@ -101,17 +133,51 @@ func (l ldapProvider) Authenticate(username string, password string) (*iamv1alph klog.Error(err) return nil, err } - + email := entry.GetAttributeValue(l.options.MailAttribute) + displayName := entry.GetAttributeValue(l.options.DisplayNameAttribute) return &iamv1alpha2.User{ ObjectMeta: metav1.ObjectMeta{ Name: username, + Annotations: map[string]string{ + constants.DisplayNameAnnotationKey: displayName, + }, }, Spec: iamv1alpha2.UserSpec{ - Email: entry.GetAttributeValue(l.options.MailAttribute), - DisplayName: entry.GetAttributeValue(l.options.DisplayNameAttribute), + Email: email, + DisplayName: displayName, }, }, nil } - return nil, ldap.NewError(ldap.LDAPResultNoSuchObject, fmt.Errorf(" could not find user %s in LDAP directory", username)) + return nil, ldap.NewError(ldap.LDAPResultNoSuchObject, fmt.Errorf("could not find user %s in LDAP directory", username)) +} + +func (l *ldapProvider) newConn() (*ldap.Conn, error) { + if !l.options.StartTLS { + return ldap.Dial("tcp", l.options.Host) + } + tlsConfig := tls.Config{} + if l.options.InsecureSkipVerify { + tlsConfig.InsecureSkipVerify = true + } + tlsConfig.RootCAs = x509.NewCertPool() + var caCert []byte + var err error + // Load CA cert + if l.options.RootCA != "" { + if caCert, err = ioutil.ReadFile(l.options.RootCA); err != nil { + klog.Error(err) + return nil, err + } + } + if l.options.RootCAData != "" { + if caCert, err = base64.StdEncoding.DecodeString(l.options.RootCAData); err != nil { + klog.Error(err) + return nil, err + } + } + if caCert != nil { + tlsConfig.RootCAs.AppendCertsFromPEM(caCert) + } + return ldap.DialTLS("tcp", l.options.Host, &tlsConfig) } diff --git a/pkg/apiserver/authentication/identityprovider/ldap_provider_test.go b/pkg/apiserver/authentication/identityprovider/ldap_provider_test.go new file mode 100644 index 000000000..c88fc79ee --- /dev/null +++ b/pkg/apiserver/authentication/identityprovider/ldap_provider_test.go @@ -0,0 +1,94 @@ +/* + + Copyright 2020 The KubeSphere Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +package identityprovider + +import ( + "github.com/google/go-cmp/cmp" + "gopkg.in/yaml.v3" + "io/ioutil" + "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" + "os" + "testing" +) + +func TestNewLdapProvider(t *testing.T) { + options := ` +host: test.sn.mynetname.net:389 +managerDN: uid=root,cn=users,dc=test,dc=sn,dc=mynetname,dc=net +managerPassword: test +startTLS: false +userSearchBase: dc=test,dc=sn,dc=mynetname,dc=net +loginAttribute: uid +mailAttribute: mail +` + var dynamicOptions oauth.DynamicOptions + err := yaml.Unmarshal([]byte(options), &dynamicOptions) + if err != nil { + t.Fatal(err) + } + provider, err := NewLdapProvider(&dynamicOptions) + if err != nil { + t.Fatal(err) + } + got := provider.(*ldapProvider).options + expected := ldapOptions{ + Host: "test.sn.mynetname.net:389", + StartTLS: false, + InsecureSkipVerify: false, + ReadTimeout: 15000, + RootCA: "", + RootCAData: "", + ManagerDN: "uid=root,cn=users,dc=test,dc=sn,dc=mynetname,dc=net", + ManagerPassword: "test", + UserSearchBase: "dc=test,dc=sn,dc=mynetname,dc=net", + UserSearchFilter: "", + GroupSearchBase: "", + GroupSearchFilter: "", + UserMemberAttribute: "", + GroupMemberAttribute: "", + LoginAttribute: "uid", + MailAttribute: "mail", + DisplayNameAttribute: "", + } + if diff := cmp.Diff(got, expected); diff != "" { + t.Errorf("%T differ (-got, +want): %s", expected, diff) + } +} + +func TestLdapProvider_Authenticate(t *testing.T) { + configFile := os.Getenv("LDAP_TEST_FILE") + if configFile == "" { + t.Skip("Skipped") + } + options, err := ioutil.ReadFile(configFile) + if err != nil { + t.Fatal(err) + } + var dynamicOptions oauth.DynamicOptions + if err := yaml.Unmarshal(options, &dynamicOptions); err != nil { + t.Fatal(err) + } + provider, err := NewLdapProvider(&dynamicOptions) + if err != nil { + t.Fatal(err) + } + if _, err := provider.Authenticate("test", "test"); err != nil { + t.Fatal(err) + } +}