improve identity provider plugin

Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
hongming
2020-11-23 15:04:59 +08:00
parent 91c2e05616
commit dfaefa5ffb
63 changed files with 3656 additions and 1746 deletions

View File

@@ -20,15 +20,19 @@ import (
"context"
"encoding/json"
"errors"
"github.com/mitchellh/mapstructure"
"io/ioutil"
"golang.org/x/oauth2"
"gopkg.in/yaml.v3"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
)
type AliyunIDaaS struct {
func init() {
identityprovider.RegisterOAuthProvider(&idaasProviderFactory{})
}
type aliyunIDaaS struct {
// ClientID is the application's ID.
ClientID string `json:"clientID" yaml:"clientID"`
@@ -39,7 +43,7 @@ type AliyunIDaaS struct {
// URLs. These are constants specific to each server and are
// often available via site-specific packages, such as
// google.Endpoint or github.Endpoint.
Endpoint Endpoint `json:"endpoint" yaml:"endpoint"`
Endpoint endpoint `json:"endpoint" yaml:"endpoint"`
// RedirectURL is the URL to redirect users going through
// the OAuth flow, after the resource owner's URLs.
@@ -49,15 +53,15 @@ type AliyunIDaaS struct {
Scopes []string `json:"scopes" yaml:"scopes"`
}
// Endpoint represents an OAuth 2.0 provider's authorization and token
// endpoint represents an OAuth 2.0 provider's authorization and token
// endpoint URLs.
type Endpoint struct {
type endpoint struct {
AuthURL string `json:"authURL" yaml:"authURL"`
TokenURL string `json:"tokenURL" yaml:"tokenURL"`
UserInfoURL string `json:"user_info_url" yaml:"userInfoUrl"`
}
type IDaaSIdentity struct {
type idaasIdentity struct {
Sub string `json:"sub"`
OuID string `json:"ou_id"`
Nickname string `json:"nickname"`
@@ -67,72 +71,73 @@ type IDaaSIdentity struct {
Username string `json:"username"`
}
type UserInfoResp struct {
type userInfoResp struct {
Success bool `json:"success"`
Message string `json:"message"`
Code string `json:"code"`
IDaaSIdentity IDaaSIdentity `json:"data"`
IDaaSIdentity idaasIdentity `json:"data"`
}
func init() {
identityprovider.RegisterOAuthProvider(&AliyunIDaaS{})
type idaasProviderFactory struct {
}
func (a *AliyunIDaaS) Type() string {
func (g *idaasProviderFactory) Type() string {
return "AliyunIDaasProvider"
}
func (a *AliyunIDaaS) Setup(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
data, err := yaml.Marshal(options)
if err != nil {
func (g *idaasProviderFactory) Create(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
var idaas aliyunIDaaS
if err := mapstructure.Decode(options, &idaas); err != nil {
return nil, err
}
var provider AliyunIDaaS
err = yaml.Unmarshal(data, &provider)
if err != nil {
return nil, err
}
return &provider, nil
return &idaas, nil
}
func (a IDaaSIdentity) GetName() string {
func (a idaasIdentity) GetUserID() string {
return a.Sub
}
func (a idaasIdentity) GetUsername() string {
return a.Username
}
func (a IDaaSIdentity) GetEmail() string {
func (a idaasIdentity) GetEmail() string {
return a.Email
}
func (g *AliyunIDaaS) IdentityExchange(code string) (identityprovider.Identity, error) {
func (a idaasIdentity) GetDisplayName() string {
return a.Nickname
}
func (a *aliyunIDaaS) IdentityExchange(code string) (identityprovider.Identity, error) {
config := oauth2.Config{
ClientID: g.ClientID,
ClientSecret: g.ClientSecret,
ClientID: a.ClientID,
ClientSecret: a.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: g.Endpoint.AuthURL,
TokenURL: g.Endpoint.TokenURL,
AuthURL: a.Endpoint.AuthURL,
TokenURL: a.Endpoint.TokenURL,
AuthStyle: oauth2.AuthStyleAutoDetect,
},
RedirectURL: g.RedirectURL,
Scopes: g.Scopes,
RedirectURL: a.RedirectURL,
Scopes: a.Scopes,
}
token, err := config.Exchange(context.Background(), code)
if err != nil {
return nil, err
}
resp, err := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(token)).Get(g.Endpoint.UserInfoURL)
resp, err := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(token)).Get(a.Endpoint.UserInfoURL)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, err
}
defer resp.Body.Close()
var UserInfoResp UserInfoResp
var UserInfoResp userInfoResp
err = json.Unmarshal(data, &UserInfoResp)
if err != nil {
return nil, err

View File

@@ -0,0 +1,48 @@
/*
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 (
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
)
var (
builtinGenericProviders = make(map[string]GenericProviderFactory)
)
type GenericProvider interface {
// Authenticate from remote server
Authenticate(username string, password string) (Identity, error)
}
type GenericProviderFactory interface {
// Type unique type of the provider
Type() string
// Apply the dynamic options from kubesphere-config
Create(options *oauth.DynamicOptions) (GenericProvider, error)
}
func CreateGenericProvider(providerType string, options *oauth.DynamicOptions) (GenericProvider, error) {
if factory, ok := builtinGenericProviders[providerType]; ok {
return factory.Create(options)
}
return nil, identityProviderNotFound
}
func RegisterGenericProvider(factory GenericProviderFactory) {
builtinGenericProviders[factory.Type()] = factory
}

View File

@@ -19,8 +19,8 @@ package github
import (
"context"
"encoding/json"
"github.com/mitchellh/mapstructure"
"golang.org/x/oauth2"
"gopkg.in/yaml.v3"
"io/ioutil"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
@@ -31,7 +31,11 @@ const (
UserInfoURL = "https://api.github.com/user"
)
type Github struct {
func init() {
identityprovider.RegisterOAuthProvider(&githubProviderFactory{})
}
type github struct {
// ClientID is the application's ID.
ClientID string `json:"clientID" yaml:"clientID"`
@@ -41,8 +45,8 @@ type Github struct {
// Endpoint contains the resource server's token endpoint
// URLs. These are constants specific to each server and are
// often available via site-specific packages, such as
// google.Endpoint or github.Endpoint.
Endpoint Endpoint `json:"endpoint" yaml:"endpoint"`
// google.Endpoint or github.endpoint.
Endpoint endpoint `json:"endpoint" yaml:"endpoint"`
// RedirectURL is the URL to redirect users going through
// the OAuth flow, after the resource owner's URLs.
@@ -52,14 +56,14 @@ type Github struct {
Scopes []string `json:"scopes" yaml:"scopes"`
}
// Endpoint represents an OAuth 2.0 provider's authorization and token
// endpoint represents an OAuth 2.0 provider's authorization and token
// endpoint URLs.
type Endpoint struct {
type endpoint struct {
AuthURL string `json:"authURL" yaml:"authURL"`
TokenURL string `json:"tokenURL" yaml:"tokenURL"`
}
type GithubIdentity struct {
type githubIdentity struct {
Login string `json:"login"`
ID int `json:"id"`
NodeID string `json:"node_id"`
@@ -98,36 +102,38 @@ type GithubIdentity struct {
Collaborators int `json:"collaborators"`
}
func init() {
identityprovider.RegisterOAuthProvider(&Github{})
type githubProviderFactory struct {
}
func (g *Github) Type() string {
func (g *githubProviderFactory) Type() string {
return "GitHubIdentityProvider"
}
func (g *Github) Setup(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
data, err := yaml.Marshal(options)
if err != nil {
func (g *githubProviderFactory) Create(options *oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
var github github
if err := mapstructure.Decode(options, &github); err != nil {
return nil, err
}
var provider Github
err = yaml.Unmarshal(data, &provider)
if err != nil {
return nil, err
}
return &provider, nil
return &github, nil
}
func (g GithubIdentity) GetName() string {
func (g githubIdentity) GetUserID() string {
return g.Login
}
func (g GithubIdentity) GetEmail() string {
func (g githubIdentity) GetUsername() string {
return g.Login
}
func (g githubIdentity) GetEmail() string {
return g.Email
}
func (g *Github) IdentityExchange(code string) (identityprovider.Identity, error) {
func (g githubIdentity) GetDisplayName() string {
return ""
}
func (g *github) IdentityExchange(code string) (identityprovider.Identity, error) {
config := oauth2.Config{
ClientID: g.ClientID,
ClientSecret: g.ClientSecret,
@@ -141,27 +147,23 @@ func (g *Github) IdentityExchange(code string) (identityprovider.Identity, error
}
token, err := config.Exchange(context.Background(), code)
if err != nil {
return nil, err
}
resp, err := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(token)).Get(UserInfoURL)
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, err
}
defer resp.Body.Close()
var githubIdentity GithubIdentity
var githubIdentity githubIdentity
err = json.Unmarshal(data, &githubIdentity)
if err != nil {
return nil, err
}

View File

@@ -17,6 +17,12 @@ limitations under the License.
package identityprovider
type Identity interface {
GetName() string
// required
GetUserID() string
// optional
GetUsername() string
// optional
GetDisplayName() string
// optional
GetEmail() string
}

View File

@@ -1,22 +1,20 @@
/*
Copyright 2020 The KubeSphere Authors.
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
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.
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
package ldap
import (
"crypto/tls"
@@ -26,24 +24,23 @@ import (
"github.com/go-ldap/ldap"
"github.com/mitchellh/mapstructure"
"io/ioutil"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/klog"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
"kubesphere.io/kubesphere/pkg/constants"
"time"
)
const (
LdapIdentityProvider = "LDAPIdentityProvider"
ldapIdentityProvider = "LDAPIdentityProvider"
defaultReadTimeout = 15000
)
type LdapProvider interface {
Authenticate(username string, password string) (*iamv1alpha2.User, error)
func init() {
identityprovider.RegisterGenericProvider(&ldapProviderFactory{})
}
type ldapOptions struct {
type ldapProvider struct {
// 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"`
@@ -73,105 +70,125 @@ type ldapOptions struct {
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.
// login attribute used for comparing user entries.
LoginAttribute string `json:"loginAttribute" yaml:"loginAttribute"`
MailAttribute string `json:"mailAttribute" yaml:"mailAttribute"`
DisplayNameAttribute string `json:"displayNameAttribute" yaml:"displayNameAttribute"`
}
type ldapProvider struct {
options ldapOptions
type ldapProviderFactory struct {
}
func NewLdapProvider(options *oauth.DynamicOptions) (LdapProvider, error) {
var ldapOptions ldapOptions
if err := mapstructure.Decode(options, &ldapOptions); err != nil {
func (l *ldapProviderFactory) Type() string {
return ldapIdentityProvider
}
func (l *ldapProviderFactory) Create(options *oauth.DynamicOptions) (identityprovider.GenericProvider, error) {
var ldapProvider ldapProvider
if err := mapstructure.Decode(options, &ldapProvider); err != nil {
return nil, err
}
if ldapOptions.ReadTimeout <= 0 {
ldapOptions.ReadTimeout = defaultReadTimeout
if ldapProvider.ReadTimeout <= 0 {
ldapProvider.ReadTimeout = defaultReadTimeout
}
return &ldapProvider{options: ldapOptions}, nil
return &ldapProvider, nil
}
func (l ldapProvider) Authenticate(username string, password string) (*iamv1alpha2.User, error) {
type ldapIdentity struct {
Username string
Email string
DisplayName string
}
func (l *ldapIdentity) GetUserID() string {
return l.Username
}
func (l *ldapIdentity) GetUsername() string {
return l.Username
}
func (l *ldapIdentity) GetEmail() string {
return l.Email
}
func (l *ldapIdentity) GetDisplayName() string {
return l.DisplayName
}
func (l ldapProvider) Authenticate(username string, password string) (identityprovider.Identity, error) {
conn, err := l.newConn()
if err != nil {
klog.Error(err)
return nil, err
}
conn.SetTimeout(time.Duration(l.options.ReadTimeout) * time.Millisecond)
conn.SetTimeout(time.Duration(l.ReadTimeout) * time.Millisecond)
defer conn.Close()
err = conn.Bind(l.options.ManagerDN, l.options.ManagerPassword)
err = conn.Bind(l.ManagerDN, l.ManagerPassword)
if err != nil {
klog.Error(err)
return nil, err
}
filter := fmt.Sprintf("(&(%s=%s)%s)", l.options.LoginAttribute, username, l.options.UserSearchFilter)
filter := fmt.Sprintf("(&(%s=%s)%s)", l.LoginAttribute, username, l.UserSearchFilter)
result, err := conn.Search(&ldap.SearchRequest{
BaseDN: l.options.UserSearchBase,
BaseDN: l.UserSearchBase,
Scope: ldap.ScopeWholeSubtree,
DerefAliases: ldap.NeverDerefAliases,
SizeLimit: 1,
TimeLimit: 0,
TypesOnly: false,
Filter: filter,
Attributes: []string{l.options.LoginAttribute, l.options.MailAttribute, l.options.DisplayNameAttribute},
Attributes: []string{l.LoginAttribute, l.MailAttribute, l.DisplayNameAttribute},
})
if err != nil {
klog.Error(err)
return nil, err
}
if len(result.Entries) == 1 {
entry := result.Entries[0]
err = conn.Bind(entry.DN, password)
if err != nil {
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: email,
DisplayName: displayName,
},
}, nil
if len(result.Entries) != 1 {
return nil, errors.NewUnauthorized("incorrect password")
}
return nil, ldap.NewError(ldap.LDAPResultNoSuchObject, fmt.Errorf("could not find user %s in LDAP directory", username))
entry := result.Entries[0]
if err = conn.Bind(entry.DN, password); err != nil {
klog.Error(err)
if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) {
return nil, errors.NewUnauthorized("incorrect password")
}
return nil, err
}
email := entry.GetAttributeValue(l.MailAttribute)
displayName := entry.GetAttributeValue(l.DisplayNameAttribute)
return &ldapIdentity{
Username: username,
DisplayName: displayName,
Email: email,
}, nil
}
func (l *ldapProvider) newConn() (*ldap.Conn, error) {
if !l.options.StartTLS {
return ldap.Dial("tcp", l.options.Host)
if !l.StartTLS {
return ldap.Dial("tcp", l.Host)
}
tlsConfig := tls.Config{}
if l.options.InsecureSkipVerify {
if l.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 {
if l.RootCA != "" {
if caCert, err = ioutil.ReadFile(l.RootCA); err != nil {
klog.Error(err)
return nil, err
}
}
if l.options.RootCAData != "" {
if caCert, err = base64.StdEncoding.DecodeString(l.options.RootCAData); err != nil {
if l.RootCAData != "" {
if caCert, err = base64.StdEncoding.DecodeString(l.RootCAData); err != nil {
klog.Error(err)
return nil, err
}
@@ -179,5 +196,5 @@ func (l *ldapProvider) newConn() (*ldap.Conn, error) {
if caCert != nil {
tlsConfig.RootCAs.AppendCertsFromPEM(caCert)
}
return ldap.DialTLS("tcp", l.options.Host, &tlsConfig)
return ldap.DialTLS("tcp", l.Host, &tlsConfig)
}

View File

@@ -1,22 +1,20 @@
/*
Copyright 2020 The KubeSphere Authors.
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
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.
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
package ldap
import (
"github.com/google/go-cmp/cmp"
@@ -42,12 +40,11 @@ mailAttribute: mail
if err != nil {
t.Fatal(err)
}
provider, err := NewLdapProvider(&dynamicOptions)
got, err := new(ldapProviderFactory).Create(&dynamicOptions)
if err != nil {
t.Fatal(err)
}
got := provider.(*ldapProvider).options
expected := ldapOptions{
expected := &ldapProvider{
Host: "test.sn.mynetname.net:389",
StartTLS: false,
InsecureSkipVerify: false,
@@ -81,14 +78,14 @@ func TestLdapProvider_Authenticate(t *testing.T) {
t.Fatal(err)
}
var dynamicOptions oauth.DynamicOptions
if err := yaml.Unmarshal(options, &dynamicOptions); err != nil {
if err = yaml.Unmarshal(options, &dynamicOptions); err != nil {
t.Fatal(err)
}
provider, err := NewLdapProvider(&dynamicOptions)
ldapProvider, err := new(ldapProviderFactory).Create(&dynamicOptions)
if err != nil {
t.Fatal(err)
}
if _, err := provider.Authenticate("test", "test"); err != nil {
if _, err = ldapProvider.Authenticate("test", "test"); err != nil {
t.Fatal(err)
}
}

View File

@@ -1,21 +1,18 @@
/*
Copyright 2020 The KubeSphere Authors.
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
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.
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 (
@@ -24,23 +21,29 @@ import (
)
var (
oauthProviders = make(map[string]OAuthProvider, 0)
ErrorIdentityProviderNotFound = errors.New("the identity provider was not found")
builtinOAuthProviders = make(map[string]OAuthProviderFactory)
identityProviderNotFound = errors.New("identity provider not found")
)
type OAuthProvider interface {
Type() string
Setup(options *oauth.DynamicOptions) (OAuthProvider, error)
// IdentityExchange exchange identity from remote server
IdentityExchange(code string) (Identity, error)
}
func GetOAuthProvider(providerType string, options *oauth.DynamicOptions) (OAuthProvider, error) {
if provider, ok := oauthProviders[providerType]; ok {
return provider.Setup(options)
}
return nil, ErrorIdentityProviderNotFound
type OAuthProviderFactory interface {
// Type unique type of the provider
Type() string
// Apply the dynamic options from kubesphere-config
Create(options *oauth.DynamicOptions) (OAuthProvider, error)
}
func RegisterOAuthProvider(provider OAuthProvider) {
oauthProviders[provider.Type()] = provider
func CreateOAuthProvider(providerType string, options *oauth.DynamicOptions) (OAuthProvider, error) {
if provider, ok := builtinOAuthProviders[providerType]; ok {
return provider.Create(options)
}
return nil, identityProviderNotFound
}
func RegisterOAuthProvider(factory OAuthProviderFactory) {
builtinOAuthProviders[factory.Type()] = factory
}