Merge pull request #3140 from wansir/identity-provider

improve identity provider plugin
This commit is contained in:
KubeSphere CI Bot
2020-12-10 19:39:44 +08:00
committed by GitHub
63 changed files with 3656 additions and 1746 deletions

View File

@@ -0,0 +1,259 @@
/*
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 auth
import (
"fmt"
"golang.org/x/crypto/bcrypt"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
"kubesphere.io/kubesphere/pkg/constants"
"net/mail"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
authuser "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/klog"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
)
var (
RateLimitExceededError = fmt.Errorf("auth rate limit exceeded")
IncorrectPasswordError = fmt.Errorf("incorrect password")
AccountIsNotActiveError = fmt.Errorf("account is not active")
)
type PasswordAuthenticator interface {
Authenticate(username, password string) (authuser.Info, string, error)
}
type OAuth2Authenticator interface {
Authenticate(provider, code string) (authuser.Info, string, error)
}
type passwordAuthenticator struct {
ksClient kubesphere.Interface
userGetter *userGetter
authOptions *authoptions.AuthenticationOptions
}
type oauth2Authenticator struct {
ksClient kubesphere.Interface
userGetter *userGetter
authOptions *authoptions.AuthenticationOptions
}
type userGetter struct {
userLister iamv1alpha2listers.UserLister
}
func NewPasswordAuthenticator(ksClient kubesphere.Interface,
userLister iamv1alpha2listers.UserLister,
options *authoptions.AuthenticationOptions) PasswordAuthenticator {
passwordAuthenticator := &passwordAuthenticator{
ksClient: ksClient,
userGetter: &userGetter{userLister: userLister},
authOptions: options,
}
return passwordAuthenticator
}
func NewOAuth2Authenticator(ksClient kubesphere.Interface,
userLister iamv1alpha2listers.UserLister,
options *authoptions.AuthenticationOptions) OAuth2Authenticator {
oauth2Authenticator := &oauth2Authenticator{
ksClient: ksClient,
userGetter: &userGetter{userLister: userLister},
authOptions: options,
}
return oauth2Authenticator
}
func (p *passwordAuthenticator) Authenticate(username, password string) (authuser.Info, string, error) {
// empty username or password are not allowed
if username == "" || password == "" {
return nil, "", IncorrectPasswordError
}
// generic identity provider has higher priority
for _, providerOptions := range p.authOptions.OAuthOptions.IdentityProviders {
// the admin account in kubesphere has the highest priority
if username == constants.AdminUserName {
break
}
if genericProvider, _ := identityprovider.CreateGenericProvider(providerOptions.Type, providerOptions.Provider); genericProvider != nil {
authenticated, err := genericProvider.Authenticate(username, password)
if err != nil {
if errors.IsUnauthorized(err) {
continue
}
return nil, providerOptions.Name, err
}
linkedAccount, err := p.userGetter.findLinkedAccount(providerOptions.Name, authenticated.GetUserID())
// using this method requires you to manually provision users.
if providerOptions.MappingMethod == oauth.MappingMethodLookup && linkedAccount == nil {
continue
}
if linkedAccount != nil {
return &authuser.DefaultInfo{Name: linkedAccount.GetName()}, providerOptions.Name, nil
}
// the user will automatically create and mapping when login successful.
if providerOptions.MappingMethod == oauth.MappingMethodAuto {
return preRegistrationUser(providerOptions.Name, authenticated), providerOptions.Name, nil
}
}
}
// kubesphere account
user, err := p.userGetter.findUser(username)
if err != nil {
// ignore not found error
if !errors.IsNotFound(err) {
klog.Error(err)
return nil, "", err
}
}
// check user status
if user != nil && (user.Status.State == nil || *user.Status.State != iamv1alpha2.UserActive) {
if user.Status.State != nil && *user.Status.State == iamv1alpha2.UserAuthLimitExceeded {
klog.Errorf("%s, username: %s", RateLimitExceededError, username)
return nil, "", RateLimitExceededError
} else {
// state not active
klog.Errorf("%s, username: %s", AccountIsNotActiveError, username)
return nil, "", AccountIsNotActiveError
}
}
// if the password is not empty, means that the password has been reset, even if the user was mapping from IDP
if user != nil && user.Spec.EncryptedPassword != "" {
if err = PasswordVerify(user.Spec.EncryptedPassword, password); err != nil {
klog.Error(err)
return nil, "", err
}
u := &authuser.DefaultInfo{
Name: user.Name,
}
// check if the password is initialized
if uninitialized := user.Annotations[iamv1alpha2.UninitializedAnnotation]; uninitialized != "" {
u.Extra = map[string][]string{
iamv1alpha2.ExtraUninitialized: {uninitialized},
}
}
return u, "", nil
}
return nil, "", IncorrectPasswordError
}
func preRegistrationUser(idp string, identity identityprovider.Identity) authuser.Info {
return &authuser.DefaultInfo{
Name: iamv1alpha2.PreRegistrationUser,
Extra: map[string][]string{
iamv1alpha2.ExtraIdentityProvider: {idp},
iamv1alpha2.ExtraUID: {identity.GetUserID()},
iamv1alpha2.ExtraUsername: {identity.GetUsername()},
iamv1alpha2.ExtraEmail: {identity.GetEmail()},
iamv1alpha2.ExtraDisplayName: {identity.GetDisplayName()},
},
Groups: []string{iamv1alpha2.PreRegistrationUserGroup},
}
}
func (o oauth2Authenticator) Authenticate(provider, code string) (authuser.Info, string, error) {
providerOptions, err := o.authOptions.OAuthOptions.IdentityProviderOptions(provider)
// identity provider not registered
if err != nil {
klog.Error(err)
return nil, "", err
}
oauthIdentityProvider, err := identityprovider.CreateOAuthProvider(providerOptions.Type, providerOptions.Provider)
if err != nil {
klog.Error(err)
return nil, "", err
}
authenticated, err := oauthIdentityProvider.IdentityExchange(code)
if err != nil {
klog.Error(err)
return nil, "", err
}
user, err := o.userGetter.findLinkedAccount(providerOptions.Name, authenticated.GetUserID())
if user == nil && providerOptions.MappingMethod == oauth.MappingMethodLookup {
klog.Error(err)
return nil, "", err
}
// the user will automatically create and mapping when login successful.
if user == nil && providerOptions.MappingMethod == oauth.MappingMethodAuto {
return preRegistrationUser(providerOptions.Name, authenticated), providerOptions.Name, nil
}
if user != nil {
return &authuser.DefaultInfo{Name: user.GetName()}, providerOptions.Name, nil
}
return nil, "", errors.NewNotFound(iamv1alpha2.Resource("user"), authenticated.GetUsername())
}
func PasswordVerify(encryptedPassword, password string) error {
if err := bcrypt.CompareHashAndPassword([]byte(encryptedPassword), []byte(password)); err != nil {
return IncorrectPasswordError
}
return nil
}
// findUser
func (u *userGetter) findUser(username string) (*iamv1alpha2.User, error) {
if _, err := mail.ParseAddress(username); err != nil {
return u.userLister.Get(username)
} else {
users, err := u.userLister.List(labels.Everything())
if err != nil {
klog.Error(err)
return nil, err
}
for _, find := range users {
if find.Spec.Email == username {
return find, nil
}
}
}
return nil, errors.NewNotFound(iamv1alpha2.Resource("user"), username)
}
func (u *userGetter) findLinkedAccount(idp, uid string) (*iamv1alpha2.User, error) {
selector := labels.SelectorFromSet(labels.Set{
iamv1alpha2.IdentifyProviderLabel: idp,
iamv1alpha2.OriginUIDLabel: uid,
})
users, err := u.userLister.List(selector)
if err != nil {
klog.Error(err)
return nil, err
}
if len(users) != 1 {
return nil, errors.NewNotFound(iamv1alpha2.Resource("user"), uid)
}
return users[0], err
}

View File

@@ -0,0 +1,40 @@
/*
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 auth
import (
"golang.org/x/crypto/bcrypt"
"testing"
)
func TestEncryptPassword(t *testing.T) {
password := "P@88w0rd"
encryptedPassword, err := hashPassword(password)
if err != nil {
t.Fatal(err)
}
if err = PasswordVerify(encryptedPassword, password); err != nil {
t.Fatal(err)
}
}
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
return string(bytes), err
}

View File

@@ -1,22 +1,20 @@
/*
*
* 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.
* /
*/
Copyright 2020 KubeSphere Authors
package im
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 auth
import (
"fmt"
@@ -24,13 +22,11 @@ import (
"k8s.io/klog"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
"kubesphere.io/kubesphere/pkg/utils/net"
"net/http"
"strings"
)
type LoginRecorder interface {
RecordLogin(username string, loginType iamv1alpha2.LoginType, provider string, authErr error, req *http.Request) error
RecordLogin(username string, loginType iamv1alpha2.LoginType, provider string, sourceIP string, userAgent string, authErr error) error
}
type loginRecorder struct {
@@ -43,7 +39,7 @@ func NewLoginRecorder(ksClient kubesphere.Interface) LoginRecorder {
}
}
func (l *loginRecorder) RecordLogin(username string, loginType iamv1alpha2.LoginType, provider string, authErr error, req *http.Request) error {
func (l *loginRecorder) RecordLogin(username string, loginType iamv1alpha2.LoginType, provider string, sourceIP string, userAgent string, authErr error) error {
// This is a temporary solution in case of user login with email,
// '@' is not allowed in Kubernetes object name.
username = strings.Replace(username, "@", "-", -1)
@@ -60,8 +56,8 @@ func (l *loginRecorder) RecordLogin(username string, loginType iamv1alpha2.Login
Provider: provider,
Success: true,
Reason: iamv1alpha2.AuthenticatedSuccessfully,
SourceIP: net.GetRequestIP(req),
UserAgent: req.UserAgent(),
SourceIP: sourceIP,
UserAgent: userAgent,
},
}

View File

@@ -16,7 +16,7 @@
*/
package im
package auth
import (
"fmt"

View File

@@ -18,22 +18,30 @@ package am
import (
"encoding/json"
"fmt"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
listersv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api"
devopsv1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1"
"kubesphere.io/kubesphere/pkg/apiserver/query"
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
devopslisters "kubesphere.io/kubesphere/pkg/client/listers/devops/v1alpha3"
"kubesphere.io/kubesphere/pkg/informers"
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/clusterrole"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/clusterrolebinding"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/globalrole"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/globalrolebinding"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/role"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/rolebinding"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/workspacerole"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/workspacerolebinding"
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
)
@@ -87,45 +95,55 @@ type AccessManagementInterface interface {
}
type amOperator struct {
resourceGetter *resourcev1alpha3.ResourceGetter
ksclient kubesphere.Interface
k8sclient kubernetes.Interface
globalRoleBindingGetter resourcev1alpha3.Interface
workspaceRoleBindingGetter resourcev1alpha3.Interface
clusterRoleBindingGetter resourcev1alpha3.Interface
roleBindingGetter resourcev1alpha3.Interface
globalRoleGetter resourcev1alpha3.Interface
workspaceRoleGetter resourcev1alpha3.Interface
clusterRoleGetter resourcev1alpha3.Interface
roleGetter resourcev1alpha3.Interface
devopsProjectLister devopslisters.DevOpsProjectLister
namespaceLister listersv1.NamespaceLister
ksclient kubesphere.Interface
k8sclient kubernetes.Interface
}
func NewReadOnlyOperator(factory informers.InformerFactory) AccessManagementInterface {
return &amOperator{
resourceGetter: resourcev1alpha3.NewResourceGetter(factory),
globalRoleBindingGetter: globalrolebinding.New(factory.KubeSphereSharedInformerFactory()),
workspaceRoleBindingGetter: workspacerolebinding.New(factory.KubeSphereSharedInformerFactory()),
clusterRoleBindingGetter: clusterrolebinding.New(factory.KubernetesSharedInformerFactory()),
roleBindingGetter: rolebinding.New(factory.KubernetesSharedInformerFactory()),
globalRoleGetter: globalrole.New(factory.KubeSphereSharedInformerFactory()),
workspaceRoleGetter: workspacerole.New(factory.KubeSphereSharedInformerFactory()),
clusterRoleGetter: clusterrole.New(factory.KubernetesSharedInformerFactory()),
roleGetter: role.New(factory.KubernetesSharedInformerFactory()),
devopsProjectLister: factory.KubeSphereSharedInformerFactory().Devops().V1alpha3().DevOpsProjects().Lister(),
namespaceLister: factory.KubernetesSharedInformerFactory().Core().V1().Namespaces().Lister(),
}
}
func NewOperator(factory informers.InformerFactory, ksclient kubesphere.Interface, k8sclient kubernetes.Interface) AccessManagementInterface {
return &amOperator{
resourceGetter: resourcev1alpha3.NewResourceGetter(factory),
ksclient: ksclient,
k8sclient: k8sclient,
}
func NewOperator(ksClient kubesphere.Interface, k8sClient kubernetes.Interface, factory informers.InformerFactory) AccessManagementInterface {
amOperator := NewReadOnlyOperator(factory).(*amOperator)
amOperator.ksclient = ksClient
amOperator.k8sclient = k8sClient
return amOperator
}
func (am *amOperator) GetGlobalRoleOfUser(username string) (*iamv1alpha2.GlobalRole, error) {
userRoleBindings, err := am.ListGlobalRoleBindings(username)
if len(userRoleBindings) > 0 {
role, err := am.GetGlobalRole(userRoleBindings[0].RoleRef.Name)
globalRoleBindings, err := am.ListGlobalRoleBindings(username)
if len(globalRoleBindings) > 0 {
// Usually, only one globalRoleBinding will be found which is created from ks-console.
if len(globalRoleBindings) > 1 {
klog.Warningf("conflict global role binding, username: %s", username)
}
globalRole, err := am.GetGlobalRole(globalRoleBindings[0].RoleRef.Name)
if err != nil {
klog.Error(err)
return nil, err
}
if len(userRoleBindings) > 1 {
klog.Warningf("conflict global role binding, username: %s", username)
}
out := role.DeepCopy()
if out.Annotations == nil {
out.Annotations = make(map[string]string, 0)
}
out.Annotations[iamv1alpha2.GlobalRoleAnnotation] = role.Name
return out, nil
return globalRole, nil
}
err = errors.NewNotFound(iamv1alpha2.Resource(iamv1alpha2.ResourcesSingularGlobalRoleBinding), username)
@@ -244,7 +262,7 @@ func (am *amOperator) GetClusterRoleOfUser(username string) (*rbacv1.ClusterRole
}
func (am *amOperator) ListWorkspaceRoleBindings(username string, groups []string, workspace string) ([]*iamv1alpha2.WorkspaceRoleBinding, error) {
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralWorkspaceRoleBinding, "", query.New())
roleBindings, err := am.workspaceRoleBindingGetter.List("", query.New())
if err != nil {
return nil, err
@@ -265,8 +283,7 @@ func (am *amOperator) ListWorkspaceRoleBindings(username string, groups []string
func (am *amOperator) ListClusterRoleBindings(username string) ([]*rbacv1.ClusterRoleBinding, error) {
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralClusterRoleBinding, "", query.New())
roleBindings, err := am.clusterRoleBindingGetter.List("", query.New())
if err != nil {
return nil, err
}
@@ -283,14 +300,12 @@ func (am *amOperator) ListClusterRoleBindings(username string) ([]*rbacv1.Cluste
}
func (am *amOperator) ListGlobalRoleBindings(username string) ([]*iamv1alpha2.GlobalRoleBinding, error) {
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralGlobalRoleBinding, "", query.New())
roleBindings, err := am.globalRoleBindingGetter.List("", query.New())
if err != nil {
return nil, err
}
result := make([]*iamv1alpha2.GlobalRoleBinding, 0)
for _, obj := range roleBindings.Items {
roleBinding := obj.(*iamv1alpha2.GlobalRoleBinding)
if contains(roleBinding.Subjects, username, nil) {
@@ -302,7 +317,7 @@ func (am *amOperator) ListGlobalRoleBindings(username string) ([]*iamv1alpha2.Gl
}
func (am *amOperator) ListRoleBindings(username string, groups []string, namespace string) ([]*rbacv1.RoleBinding, error) {
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralRoleBinding, namespace, query.New())
roleBindings, err := am.roleBindingGetter.List(namespace, query.New())
if err != nil {
klog.Error(err)
return nil, err
@@ -335,23 +350,23 @@ func contains(subjects []rbacv1.Subject, username string, groups []string) bool
}
func (am *amOperator) ListRoles(namespace string, query *query.Query) (*api.ListResult, error) {
return am.resourceGetter.List(iamv1alpha2.ResourcesPluralRole, namespace, query)
return am.roleGetter.List(namespace, query)
}
func (am *amOperator) ListClusterRoles(query *query.Query) (*api.ListResult, error) {
return am.resourceGetter.List(iamv1alpha2.ResourcesPluralClusterRole, "", query)
return am.clusterRoleGetter.List("", query)
}
func (am *amOperator) ListWorkspaceRoles(queryParam *query.Query) (*api.ListResult, error) {
return am.resourceGetter.List(iamv1alpha2.ResourcesPluralWorkspaceRole, "", queryParam)
return am.workspaceRoleGetter.List("", queryParam)
}
func (am *amOperator) ListGlobalRoles(query *query.Query) (*api.ListResult, error) {
return am.resourceGetter.List(iamv1alpha2.ResourcesPluralGlobalRole, "", query)
return am.globalRoleGetter.List("", query)
}
func (am *amOperator) GetGlobalRole(globalRole string) (*iamv1alpha2.GlobalRole, error) {
obj, err := am.resourceGetter.Get(iamv1alpha2.ResourcesPluralGlobalRole, "", globalRole)
obj, err := am.globalRoleGetter.Get("", globalRole)
if err != nil {
klog.Error(err)
return nil, err
@@ -938,7 +953,7 @@ func (am *amOperator) GetRoleReferenceRules(roleRef rbacv1.RoleRef, namespace st
}
func (am *amOperator) GetWorkspaceRole(workspace string, name string) (*iamv1alpha2.WorkspaceRole, error) {
obj, err := am.resourceGetter.Get(iamv1alpha2.ResourcesPluralWorkspaceRole, "", name)
obj, err := am.workspaceRoleGetter.Get("", name)
if err != nil {
klog.Error(err)
return nil, err
@@ -956,7 +971,7 @@ func (am *amOperator) GetWorkspaceRole(workspace string, name string) (*iamv1alp
}
func (am *amOperator) GetNamespaceRole(namespace string, name string) (*rbacv1.Role, error) {
obj, err := am.resourceGetter.Get(iamv1alpha2.ResourcesPluralRole, namespace, name)
obj, err := am.roleGetter.Get(namespace, name)
if err != nil {
klog.Error(err)
return nil, err
@@ -965,7 +980,7 @@ func (am *amOperator) GetNamespaceRole(namespace string, name string) (*rbacv1.R
}
func (am *amOperator) GetClusterRole(name string) (*rbacv1.ClusterRole, error) {
obj, err := am.resourceGetter.Get(iamv1alpha2.ResourcesPluralClusterRole, "", name)
obj, err := am.clusterRoleGetter.Get("", name)
if err != nil {
klog.Error(err)
return nil, err
@@ -973,28 +988,25 @@ func (am *amOperator) GetClusterRole(name string) (*rbacv1.ClusterRole, error) {
return obj.(*rbacv1.ClusterRole), nil
}
func (am *amOperator) GetDevOpsRelatedNamespace(devops string) (string, error) {
obj, err := am.resourceGetter.Get(devopsv1alpha3.ResourcePluralDevOpsProject, "", devops)
devopsProject, err := am.devopsProjectLister.Get(devops)
if err != nil {
klog.Error(err)
return "", err
}
devopsProject := obj.(*devopsv1alpha3.DevOpsProject)
return devopsProject.Status.AdminNamespace, nil
}
func (am *amOperator) GetDevOpsControlledWorkspace(devops string) (string, error) {
obj, err := am.resourceGetter.Get(devopsv1alpha3.ResourcePluralDevOpsProject, "", devops)
devopsProject, err := am.devopsProjectLister.Get(devops)
if err != nil {
klog.Error(err)
return "", err
}
devopsProject := obj.(*devopsv1alpha3.DevOpsProject)
return devopsProject.Labels[tenantv1alpha1.WorkspaceLabel], nil
}
func (am *amOperator) GetNamespaceControlledWorkspace(namespace string) (string, error) {
obj, err := am.resourceGetter.Get("namespaces", "", namespace)
ns, err := am.namespaceLister.Get(namespace)
if err != nil {
if errors.IsNotFound(err) {
return "", nil
@@ -1002,24 +1014,22 @@ func (am *amOperator) GetNamespaceControlledWorkspace(namespace string) (string,
klog.Error(err)
return "", err
}
ns := obj.(*corev1.Namespace)
return ns.Labels[tenantv1alpha1.WorkspaceLabel], nil
}
func (am *amOperator) ListGroupWorkspaceRoleBindings(workspace, group string) ([]*iamv1alpha2.WorkspaceRoleBinding, error) {
q := workspaceQuery(workspace)
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralWorkspaceRoleBinding, "", q)
queryParam := query.New()
queryParam.LabelSelector = labels.FormatLabels(map[string]string{tenantv1alpha1.WorkspaceLabel: workspace})
roleBindings, err := am.workspaceRoleBindingGetter.List("", queryParam)
if err != nil {
return nil, err
}
result := make([]*iamv1alpha2.WorkspaceRoleBinding, 0)
for _, obj := range roleBindings.Items {
roleBinding := obj.(*iamv1alpha2.WorkspaceRoleBinding)
inSpecifiedWorkspace := workspace == "" || roleBinding.Labels[tenantv1alpha1.WorkspaceLabel] == workspace
if containsgroup(roleBinding.Subjects, group) && inSpecifiedWorkspace {
if containsGroup(roleBinding.Subjects, group) && inSpecifiedWorkspace {
result = append(result, roleBinding)
}
}
@@ -1062,15 +1072,13 @@ func (am *amOperator) DeleteWorkspaceRoleBinding(workspaceName, name string) err
}
func (am *amOperator) ListGroupRoleBindings(workspace, group string) ([]*rbacv1.RoleBinding, error) {
q := workspaceQuery(workspace)
namespaces, err := am.resourceGetter.List("namespaces", "", q)
namespaces, err := am.namespaceLister.List(labels.SelectorFromSet(labels.Set{tenantv1alpha1.WorkspaceLabel: workspace}))
if err != nil {
return nil, err
}
result := make([]*rbacv1.RoleBinding, 0)
for _, ns := range namespaces.Items {
namespace := ns.(*corev1.Namespace)
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralRoleBinding, namespace.Name, query.New())
for _, namespace := range namespaces {
roleBindings, err := am.roleBindingGetter.List(namespace.Name, query.New())
if err != nil {
klog.Error(err)
return nil, err
@@ -1078,7 +1086,7 @@ func (am *amOperator) ListGroupRoleBindings(workspace, group string) ([]*rbacv1.
for _, obj := range roleBindings.Items {
roleBinding := obj.(*rbacv1.RoleBinding)
if containsgroup(roleBinding.Subjects, group) {
if containsGroup(roleBinding.Subjects, group) {
result = append(result, roleBinding)
}
}
@@ -1087,23 +1095,20 @@ func (am *amOperator) ListGroupRoleBindings(workspace, group string) ([]*rbacv1.
}
func (am *amOperator) ListGroupDevOpsRoleBindings(workspace, group string) ([]*rbacv1.RoleBinding, error) {
q := workspaceQuery(workspace)
namespaces, err := am.resourceGetter.List(devopsv1alpha3.ResourcePluralDevOpsProject, "", q)
devOpsProjects, err := am.devopsProjectLister.List(labels.SelectorFromSet(labels.Set{tenantv1alpha1.WorkspaceLabel: workspace}))
if err != nil {
return nil, err
}
result := make([]*rbacv1.RoleBinding, 0)
for _, ns := range namespaces.Items {
namespace := ns.(*devopsv1alpha3.DevOpsProject)
roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralRoleBinding, namespace.Name, query.New())
for _, devOpsProject := range devOpsProjects {
roleBindings, err := am.roleBindingGetter.List(devOpsProject.Name, query.New())
if err != nil {
klog.Error(err)
return nil, err
}
for _, obj := range roleBindings.Items {
roleBinding := obj.(*rbacv1.RoleBinding)
if containsgroup(roleBinding.Subjects, group) {
if containsGroup(roleBinding.Subjects, group) {
result = append(result, roleBinding)
}
}
@@ -1143,7 +1148,7 @@ func (am *amOperator) DeleteRoleBinding(namespace, name string) error {
return am.k8sclient.RbacV1().RoleBindings(namespace).Delete(name, metav1.NewDeleteOptions(0))
}
func containsgroup(subjects []rbacv1.Subject, group string) bool {
func containsGroup(subjects []rbacv1.Subject, group string) bool {
for _, subject := range subjects {
if subject.Kind == rbacv1.GroupKind && subject.Name == group {
return true
@@ -1151,9 +1156,3 @@ func containsgroup(subjects []rbacv1.Subject, group string) bool {
}
return false
}
func workspaceQuery(workspace string) *query.Query {
q := query.New()
q.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s=%s", tenantv1alpha1.WorkspaceLabel, workspace))
return q
}

View File

@@ -1,175 +0,0 @@
/*
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 im
import (
"fmt"
"net/mail"
"github.com/go-ldap/ldap"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
authuser "k8s.io/apiserver/pkg/authentication/user"
"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"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/constants"
)
var (
AuthRateLimitExceeded = fmt.Errorf("auth rate limit exceeded")
AuthFailedIncorrectPassword = fmt.Errorf("incorrect password")
AuthFailedAccountIsNotActive = fmt.Errorf("account is not active")
AuthFailedIdentityMappingNotMatch = fmt.Errorf("identity mapping not match")
)
type PasswordAuthenticator interface {
Authenticate(username, password string) (authuser.Info, error)
}
type passwordAuthenticator struct {
ksClient kubesphere.Interface
userLister iamv1alpha2listers.UserLister
options *authoptions.AuthenticationOptions
}
func NewPasswordAuthenticator(ksClient kubesphere.Interface,
userLister iamv1alpha2listers.UserLister,
options *authoptions.AuthenticationOptions) PasswordAuthenticator {
return &passwordAuthenticator{
ksClient: ksClient,
userLister: userLister,
options: options}
}
func (im *passwordAuthenticator) Authenticate(username, password string) (authuser.Info, error) {
user, err := im.searchUser(username)
if err != nil {
// internal error
if !errors.IsNotFound(err) {
klog.Error(err)
return nil, err
}
}
providerOptions, ldapProvider := im.getLdapProvider()
// no identity provider
// even auth failed, still return username to record login attempt
if user == nil && (providerOptions == nil || providerOptions.MappingMethod != oauth.MappingMethodAuto) {
return nil, AuthFailedIncorrectPassword
}
if user != nil && user.Status.State != iamv1alpha2.UserActive {
if user.Status.State == iamv1alpha2.UserAuthLimitExceeded {
klog.Errorf("%s, username: %s", AuthRateLimitExceeded, username)
return nil, AuthRateLimitExceeded
} else {
klog.Errorf("%s, username: %s", AuthFailedAccountIsNotActive, username)
return nil, AuthFailedAccountIsNotActive
}
}
// able to login using the locally principal admin account and password in case of a disruption of LDAP services.
if ldapProvider != nil && username != constants.AdminUserName {
if providerOptions.MappingMethod == oauth.MappingMethodLookup &&
(user == nil || user.Labels[iamv1alpha2.IdentifyProviderLabel] != providerOptions.Name) {
klog.Error(AuthFailedIdentityMappingNotMatch)
return nil, AuthFailedIdentityMappingNotMatch
}
if providerOptions.MappingMethod == oauth.MappingMethodAuto &&
user != nil && user.Labels[iamv1alpha2.IdentifyProviderLabel] != providerOptions.Name {
klog.Error(AuthFailedIdentityMappingNotMatch)
return nil, AuthFailedIdentityMappingNotMatch
}
authenticated, err := ldapProvider.Authenticate(username, password)
if err != nil {
klog.Error(err)
if ldap.IsErrorWithCode(err, ldap.LDAPResultInvalidCredentials) || ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
return nil, AuthFailedIncorrectPassword
} else {
return nil, err
}
}
if authenticated != nil && user == nil {
authenticated.Labels = map[string]string{iamv1alpha2.IdentifyProviderLabel: providerOptions.Name}
if authenticated, err = im.ksClient.IamV1alpha2().Users().Create(authenticated); err != nil {
klog.Error(err)
return nil, err
}
}
if authenticated != nil {
return &authuser.DefaultInfo{
Name: authenticated.Name,
UID: string(authenticated.UID),
}, nil
}
}
if checkPasswordHash(password, user.Spec.EncryptedPassword) {
return &authuser.DefaultInfo{
Name: user.Name,
UID: string(user.UID),
Groups: user.Spec.Groups,
}, nil
}
return nil, AuthFailedIncorrectPassword
}
func (im *passwordAuthenticator) searchUser(username string) (*iamv1alpha2.User, error) {
if _, err := mail.ParseAddress(username); err != nil {
return im.userLister.Get(username)
} else {
users, err := im.userLister.List(labels.Everything())
if err != nil {
klog.Error(err)
return nil, err
}
for _, find := range users {
if find.Spec.Email == username {
return find, nil
}
}
}
return nil, errors.NewNotFound(iamv1alpha2.Resource("user"), username)
}
func (im *passwordAuthenticator) getLdapProvider() (*oauth.IdentityProviderOptions, identityprovider.LdapProvider) {
for _, options := range im.options.OAuthOptions.IdentityProviders {
if options.Type == identityprovider.LdapIdentityProvider {
if provider, err := identityprovider.NewLdapProvider(options.Provider); err != nil {
klog.Error(err)
} else {
return &options, provider
}
}
}
return nil, nil
}

View File

@@ -16,7 +16,7 @@ limitations under the License.
package im
import (
"golang.org/x/crypto/bcrypt"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api"
@@ -24,8 +24,8 @@ import (
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/apiserver/query"
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
"kubesphere.io/kubesphere/pkg/informers"
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
"kubesphere.io/kubesphere/pkg/models/auth"
resources "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3"
)
type IdentityManagementInterface interface {
@@ -35,41 +35,40 @@ type IdentityManagementInterface interface {
UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error)
DescribeUser(username string) (*iamv1alpha2.User, error)
ModifyPassword(username string, password string) error
ListLoginRecords(query *query.Query) (*api.ListResult, error)
ListLoginRecords(username string, query *query.Query) (*api.ListResult, error)
PasswordVerify(username string, password string) error
}
func NewOperator(ksClient kubesphere.Interface, factory informers.InformerFactory, options *authoptions.AuthenticationOptions) IdentityManagementInterface {
im := &defaultIMOperator{
ksClient: ksClient,
resourceGetter: resourcev1alpha3.NewResourceGetter(factory),
options: options,
func NewOperator(ksClient kubesphere.Interface, userGetter resources.Interface, loginRecordGetter resources.Interface, options *authoptions.AuthenticationOptions) IdentityManagementInterface {
im := &imOperator{
ksClient: ksClient,
userGetter: userGetter,
loginRecordGetter: loginRecordGetter,
options: options,
}
return im
}
type defaultIMOperator struct {
ksClient kubesphere.Interface
resourceGetter *resourcev1alpha3.ResourceGetter
options *authoptions.AuthenticationOptions
type imOperator struct {
ksClient kubesphere.Interface
userGetter resources.Interface
loginRecordGetter resources.Interface
options *authoptions.AuthenticationOptions
}
func (im *defaultIMOperator) UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", user.Name)
// UpdateUser returns user information after update.
func (im *imOperator) UpdateUser(new *iamv1alpha2.User) (*iamv1alpha2.User, error) {
old, err := im.fetch(new.Name)
if err != nil {
klog.Error(err)
return nil, err
}
old := obj.(*iamv1alpha2.User).DeepCopy()
if user.Annotations == nil {
user.Annotations = make(map[string]string, 0)
if old.Annotations == nil {
old.Annotations = make(map[string]string, 0)
}
user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation] = old.Annotations[iamv1alpha2.PasswordEncryptedAnnotation]
user.Spec.EncryptedPassword = old.Spec.EncryptedPassword
user.Status = old.Status
updated, err := im.ksClient.IamV1alpha2().Users().Update(user)
// keep encrypted password
new.Spec.EncryptedPassword = old.Spec.EncryptedPassword
updated, err := im.ksClient.IamV1alpha2().Users().Update(old)
if err != nil {
klog.Error(err)
return nil, err
@@ -77,18 +76,23 @@ func (im *defaultIMOperator) UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.Us
return ensurePasswordNotOutput(updated), nil
}
func (im *defaultIMOperator) ModifyPassword(username string, password string) error {
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", username)
func (im *imOperator) fetch(username string) (*iamv1alpha2.User, error) {
obj, err := im.userGetter.Get("", username)
if err != nil {
klog.Error(err)
return nil, err
}
user := obj.(*iamv1alpha2.User).DeepCopy()
return user, nil
}
func (im *imOperator) ModifyPassword(username string, password string) error {
user, err := im.fetch(username)
if err != nil {
klog.Error(err)
return err
}
user := obj.(*iamv1alpha2.User).DeepCopy()
delete(user.Annotations, iamv1alpha2.PasswordEncryptedAnnotation)
user.Spec.EncryptedPassword = password
_, err = im.ksClient.IamV1alpha2().Users().Update(user)
if err != nil {
klog.Error(err)
@@ -97,13 +101,12 @@ func (im *defaultIMOperator) ModifyPassword(username string, password string) er
return nil
}
func (im *defaultIMOperator) ListUsers(query *query.Query) (result *api.ListResult, err error) {
result, err = im.resourceGetter.List(iamv1alpha2.ResourcesPluralUser, "", query)
func (im *imOperator) ListUsers(query *query.Query) (result *api.ListResult, err error) {
result, err = im.userGetter.List("", query)
if err != nil {
klog.Error(err)
return nil, err
}
items := make([]interface{}, 0)
for _, item := range result.Items {
user := item.(*iamv1alpha2.User)
@@ -114,40 +117,34 @@ func (im *defaultIMOperator) ListUsers(query *query.Query) (result *api.ListResu
return result, nil
}
func (im *defaultIMOperator) PasswordVerify(username string, password string) error {
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", username)
func (im *imOperator) PasswordVerify(username string, password string) error {
obj, err := im.userGetter.Get("", username)
if err != nil {
klog.Error(err)
return err
}
user := obj.(*iamv1alpha2.User)
if checkPasswordHash(password, user.Spec.EncryptedPassword) {
return nil
if err = auth.PasswordVerify(user.Spec.EncryptedPassword, password); err != nil {
return err
}
return AuthFailedIncorrectPassword
return nil
}
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func (im *defaultIMOperator) DescribeUser(username string) (*iamv1alpha2.User, error) {
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", username)
func (im *imOperator) DescribeUser(username string) (*iamv1alpha2.User, error) {
obj, err := im.userGetter.Get("", username)
if err != nil {
klog.Error(err)
return nil, err
}
user := obj.(*iamv1alpha2.User)
return ensurePasswordNotOutput(user), nil
}
func (im *defaultIMOperator) DeleteUser(username string) error {
func (im *imOperator) DeleteUser(username string) error {
return im.ksClient.IamV1alpha2().Users().Delete(username, metav1.NewDeleteOptions(0))
}
func (im *defaultIMOperator) CreateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
func (im *imOperator) CreateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
user, err := im.ksClient.IamV1alpha2().Users().Create(user)
if err != nil {
klog.Error(err)
@@ -156,8 +153,9 @@ func (im *defaultIMOperator) CreateUser(user *iamv1alpha2.User) (*iamv1alpha2.Us
return user, nil
}
func (im *defaultIMOperator) ListLoginRecords(query *query.Query) (*api.ListResult, error) {
result, err := im.resourceGetter.List(iamv1alpha2.ResourcesPluralLoginRecord, "", query)
func (im *imOperator) ListLoginRecords(username string, q *query.Query) (*api.ListResult, error) {
q.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s=%s", iamv1alpha2.UserReferenceLabel, username))
result, err := im.loginRecordGetter.List("", q)
if err != nil {
klog.Error(err)
return nil, err

View File

@@ -15,25 +15,3 @@ limitations under the License.
*/
package im
import (
"golang.org/x/crypto/bcrypt"
"testing"
)
func TestEncryptPassword(t *testing.T) {
password := "P@88w0rd"
encryptedPassword, err := hashPassword(password)
if err != nil {
t.Fatal(err)
}
if !checkPasswordHash(password, encryptedPassword) {
t.Fatal(err)
}
t.Log(encryptedPassword)
}
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
return string(bytes), err
}

View File

@@ -133,9 +133,9 @@ func NewResourceGetter(factory informers.InformerFactory) *ResourceGetter {
}
}
// tryResource will retrieve a getter with resource name, it doesn't guarantee find resource with correct group version
// TryResource will retrieve a getter with resource name, it doesn't guarantee find resource with correct group version
// need to refactor this use schema.GroupVersionResource
func (r *ResourceGetter) tryResource(resource string) v1alpha3.Interface {
func (r *ResourceGetter) TryResource(resource string) v1alpha3.Interface {
for k, v := range r.getters {
if k.Resource == resource {
return v
@@ -145,7 +145,7 @@ func (r *ResourceGetter) tryResource(resource string) v1alpha3.Interface {
}
func (r *ResourceGetter) Get(resource, namespace, name string) (runtime.Object, error) {
getter := r.tryResource(resource)
getter := r.TryResource(resource)
if getter == nil {
return nil, ErrResourceNotSupported
}
@@ -153,7 +153,7 @@ func (r *ResourceGetter) Get(resource, namespace, name string) (runtime.Object,
}
func (r *ResourceGetter) List(resource, namespace string, query *query.Query) (*api.ListResult, error) {
getter := r.tryResource(resource)
getter := r.TryResource(resource)
if getter == nil {
return nil, ErrResourceNotSupported
}

View File

@@ -30,7 +30,7 @@ import (
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1"
tenantv1alpha2 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizerfactory"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/rbac"
"kubesphere.io/kubesphere/pkg/apiserver/query"
fakeks "kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
"kubesphere.io/kubesphere/pkg/informers"
@@ -540,8 +540,8 @@ func prepare() Interface {
RoleBindings().Informer().GetIndexer().Add(roleBinding)
}
amOperator := am.NewOperator(fakeInformerFactory, ksClient, k8sClient)
authorizer := authorizerfactory.NewRBACAuthorizer(amOperator)
amOperator := am.NewOperator(ksClient, k8sClient, fakeInformerFactory)
authorizer := rbac.NewRBACAuthorizer(amOperator)
return New(fakeInformerFactory, k8sClient, ksClient, nil, nil, nil, amOperator, authorizer)
}