improve LDAP identity provider
Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user