提交 00920d3d 编写于 作者: H hongming

improve LDAP identity provider

Signed-off-by: Nhongming <talonwan@yunify.com>
上级 f6fea24a
......@@ -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 {
return nil, err
}
var ldapOptions ldapOptions
err = yaml.Unmarshal(data, &ldapOptions)
if err != nil {
if err := mapstructure.Decode(options, &ldapOptions); 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)
}
/*
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)
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册