login record CRD (#2565)

* Signed-off-by: hongming <talonwan@yunify.com>

support ldap identity provider

Signed-off-by: hongming <talonwan@yunify.com>

* add login record

Signed-off-by: Jeff <zw0948@gmail.com>

Co-authored-by: hongming <talonwan@yunify.com>
This commit is contained in:
zryfish
2020-07-23 22:10:39 +08:00
committed by GitHub
parent 50a6c7b2b5
commit 3d74bb0589
51 changed files with 2163 additions and 548 deletions

View File

@@ -22,6 +22,7 @@ import (
"k8s.io/klog"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
tenantv1alpha2 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha2"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/controller/application"
"kubesphere.io/kubesphere/pkg/controller/certificatesigningrequest"
"kubesphere.io/kubesphere/pkg/controller/cluster"
@@ -61,6 +62,7 @@ func addControllers(
devopsClient devops.Interface,
s3Client s3.Interface,
ldapClient ldapclient.Interface,
authenticationOptions *authoptions.AuthenticationOptions,
openpitrixClient openpitrix.Client,
multiClusterEnabled bool,
networkPolicyEnabled bool,
@@ -207,10 +209,18 @@ func addControllers(
go fedWorkspaceRoleBindingCacheController.Run(stopCh)
}
userController := user.NewController(client.Kubernetes(), client.KubeSphere(), client.Config(),
userController := user.NewUserController(client.Kubernetes(), client.KubeSphere(), client.Config(),
kubesphereInformer.Iam().V1alpha2().Users(),
fedUserCache, fedUserCacheController,
kubernetesInformer.Core().V1().ConfigMaps(), ldapClient, multiClusterEnabled)
kubesphereInformer.Iam().V1alpha2().LoginRecords(),
kubernetesInformer.Core().V1().ConfigMaps(),
ldapClient, authenticationOptions, multiClusterEnabled)
loginRecordController := user.NewLoginRecordController(
client.Kubernetes(),
client.KubeSphere(),
kubesphereInformer.Iam().V1alpha2().LoginRecords(),
authenticationOptions)
csrController := certificatesigningrequest.NewController(client.Kubernetes(),
kubernetesInformer.Certificates().V1beta1().CertificateSigningRequests(),
@@ -282,6 +292,7 @@ func addControllers(
"storagecapability-controller": storageCapabilityController,
"volumeexpansion-controller": volumeExpansionController,
"user-controller": userController,
"loginrecord-controller": loginRecordController,
"cluster-controller": clusterController,
"nsnp-controller": nsnpController,
"csr-controller": csrController,

View File

@@ -183,10 +183,18 @@ func Run(s *options.KubeSphereControllerManagerOptions, stopCh <-chan struct{})
// TODO(jeff): refactor config with CRD
servicemeshEnabled := s.ServiceMeshOptions != nil && len(s.ServiceMeshOptions.IstioPilotHost) != 0
if err = addControllers(mgr, kubernetesClient, informerFactory,
devopsClient, s3Client, ldapClient, openpitrixClient,
s.MultiClusterOptions.Enable, s.NetworkOptions.EnableNetworkPolicy,
servicemeshEnabled, s.AuthenticationOptions.KubectlImage, stopCh); err != nil {
if err = addControllers(mgr,
kubernetesClient,
informerFactory,
devopsClient,
s3Client,
ldapClient,
s.AuthenticationOptions,
openpitrixClient,
s.MultiClusterOptions.Enable,
s.NetworkOptions.EnableNetworkPolicy,
servicemeshEnabled,
s.AuthenticationOptions.KubectlImage, stopCh); err != nil {
klog.Fatalf("unable to register controllers to the manager: %v", err)
}

View File

@@ -0,0 +1,72 @@
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: (devel)
creationTimestamp: null
name: loginrecords.iam.kubesphere.io
spec:
additionalPrinterColumns:
- JSONPath: .spec.type
name: Type
type: string
- JSONPath: .status.reason
name: Reason
type: string
- JSONPath: .metadata.creationTimestamp
name: Age
type: date
group: iam.kubesphere.io
names:
categories:
- iam
kind: LoginRecord
listKind: LoginRecordList
plural: loginrecords
singular: loginrecord
scope: Cluster
subresources: {}
validation:
openAPIV3Schema:
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
properties:
reason:
type: string
sourceIP:
type: string
type:
type: string
required:
- reason
- sourceIP
- type
type: object
required:
- spec
type: object
version: v1alpha2
versions:
- name: v1alpha2
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@@ -69,29 +69,11 @@ spec:
status:
description: UserStatus defines the observed state of User
properties:
conditions:
description: Represents the latest available observations of a user's
current state.
items:
properties:
lastTransitionTime:
format: date-time
type: string
message:
type: string
reason:
type: string
status:
description: Status of the condition, one of True, False, Unknown.
type: string
type:
description: Type of user controller condition.
type: string
required:
- status
- type
type: object
type: array
lastTransitionTime:
format: date-time
type: string
reason:
type: string
state:
description: The user status
type: string

View File

@@ -51,6 +51,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&User{},
&UserList{},
&LoginRecord{},
&LoginRecordList{},
&GlobalRole{},
&GlobalRoleList{},
&GlobalRoleBinding{},

View File

@@ -26,6 +26,9 @@ const (
ResourceKindUser = "User"
ResourcesSingularUser = "user"
ResourcesPluralUser = "users"
ResourceKindLoginRecord = "LoginRecord"
ResourcesSingularLoginRecord = "loginrecord"
ResourcesPluralLoginRecord = "loginrecords"
ResourceKindGlobalRoleBinding = "GlobalRoleBinding"
ResourcesSingularGlobalRoleBinding = "globalrolebinding"
ResourcesPluralGlobalRoleBinding = "globalrolebindings"
@@ -119,6 +122,13 @@ const (
UserActive UserState = "Active"
// UserDisabled means the user is disabled.
UserDisabled UserState = "Disabled"
// UserDisabled means the user is disabled.
UserAuthLimitExceeded UserState = "AuthLimitExceeded"
LoginFailure LoginRecordType = "LoginFailure"
LoginSuccess LoginRecordType = "LoginSuccess"
AuthenticatedSuccessfully = "authenticated successfully"
)
// UserStatus defines the observed state of User
@@ -126,43 +136,12 @@ type UserStatus struct {
// The user status
// +optional
State UserState `json:"state,omitempty"`
// Represents the latest available observations of a user's current state.
// +optional
Conditions []UserCondition `json:"conditions,omitempty"`
}
type UserCondition struct {
// Type of user controller condition.
Type UserConditionType `json:"type"`
// Status of the condition, one of True, False, Unknown.
Status ConditionStatus `json:"status"`
// +optional
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
// +optional
Reason string `json:"reason,omitempty"`
// +optional
Message string `json:"message,omitempty"`
LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty"`
}
type UserConditionType string
// These are valid conditions of a user.
const (
// UserLoginFailure contains information about user login.
LoginFailure UserConditionType = "LoginFailure"
)
type ConditionStatus string
// These are valid condition statuses. "ConditionTrue" means a resource is in the condition.
// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes
// can't decide if a resource is in the condition or not. In the future, we could add other
// intermediate conditions, e.g. ConditionDegraded.
const (
ConditionTrue ConditionStatus = "True"
ConditionFalse ConditionStatus = "False"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// UserList contains a list of User
@@ -306,3 +285,33 @@ type RoleBaseList struct {
metav1.ListMeta `json:"metadata,omitempty"`
Items []RoleBase `json:"items"`
}
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type"
// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.reason"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:resource:categories="iam",scope="Cluster"
type LoginRecord struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec LoginRecordSpec `json:"spec"`
}
type LoginRecordSpec struct {
SourceIP string `json:"sourceIP"`
Type LoginRecordType `json:"type"`
Reason string `json:"reason"`
}
type LoginRecordType string
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// LoginRecordList contains a list of LoginRecord
type LoginRecordList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []LoginRecord `json:"items"`
}

View File

@@ -294,6 +294,79 @@ func (in *GlobalRoleList) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LoginRecord) DeepCopyInto(out *LoginRecord) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoginRecord.
func (in *LoginRecord) DeepCopy() *LoginRecord {
if in == nil {
return nil
}
out := new(LoginRecord)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *LoginRecord) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LoginRecordList) DeepCopyInto(out *LoginRecordList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]LoginRecord, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoginRecordList.
func (in *LoginRecordList) DeepCopy() *LoginRecordList {
if in == nil {
return nil
}
out := new(LoginRecordList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *LoginRecordList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LoginRecordSpec) DeepCopyInto(out *LoginRecordSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoginRecordSpec.
func (in *LoginRecordSpec) DeepCopy() *LoginRecordSpec {
if in == nil {
return nil
}
out := new(LoginRecordSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Placement) DeepCopyInto(out *Placement) {
*out = *in
@@ -445,22 +518,6 @@ func (in *User) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UserCondition) DeepCopyInto(out *UserCondition) {
*out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserCondition.
func (in *UserCondition) DeepCopy() *UserCondition {
if in == nil {
return nil
}
out := new(UserCondition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UserList) DeepCopyInto(out *UserList) {
*out = *in
@@ -516,12 +573,9 @@ func (in *UserSpec) DeepCopy() *UserSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UserStatus) DeepCopyInto(out *UserStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]UserCondition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
if in.LastTransitionTime != nil {
in, out := &in.LastTransitionTime, &out.LastTransitionTime
*out = (*in).DeepCopy()
}
}

View File

@@ -37,7 +37,6 @@ import (
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/anonymous"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/basictoken"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/bearertoken"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizerfactory"
authorizationoptions "kubesphere.io/kubesphere/pkg/apiserver/authorization/options"
@@ -78,10 +77,9 @@ import (
"kubesphere.io/kubesphere/pkg/simple/client/openpitrix"
"kubesphere.io/kubesphere/pkg/simple/client/s3"
"kubesphere.io/kubesphere/pkg/simple/client/sonarqube"
"net"
utilnet "kubesphere.io/kubesphere/pkg/utils/net"
"net/http"
rt "runtime"
"strings"
"time"
)
@@ -187,8 +185,16 @@ func (s *APIServer) installKubeSphereAPIs() {
urlruntime.Must(iamapi.AddToContainer(s.container, imOperator,
am.NewOperator(s.InformerFactory, s.KubernetesClient.KubeSphere(), s.KubernetesClient.Kubernetes()),
s.Config.AuthenticationOptions))
urlruntime.Must(oauth.AddToContainer(s.container, imOperator,
token.NewJwtTokenIssuer(token.DefaultIssuerName, s.Config.AuthenticationOptions, s.CacheClient),
im.NewTokenOperator(
s.CacheClient,
s.Config.AuthenticationOptions),
im.NewPasswordAuthenticator(
s.KubernetesClient.KubeSphere(),
s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister(),
s.Config.AuthenticationOptions),
im.NewLoginRecorder(s.KubernetesClient.KubeSphere()),
s.Config.AuthenticationOptions))
urlruntime.Must(servicemeshv1alpha2.AddToContainer(s.container))
urlruntime.Must(devopsv1alpha2.AddToContainer(s.container,
@@ -283,11 +289,12 @@ func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) {
handler = filters.WithAuthorization(handler, authorizers)
loginRecorder := im.NewLoginRecorder(s.KubernetesClient.KubeSphere())
// authenticators are unordered
authn := unionauth.New(anonymous.NewAuthenticator(),
basictoken.New(basic.NewBasicAuthenticator(im.NewOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory, s.Config.AuthenticationOptions))),
bearertoken.New(jwttoken.NewTokenAuthenticator(token.NewJwtTokenIssuer(token.DefaultIssuerName, s.Config.AuthenticationOptions, s.CacheClient))))
handler = filters.WithAuthentication(handler, authn)
basictoken.New(basic.NewBasicAuthenticator(im.NewPasswordAuthenticator(s.KubernetesClient.KubeSphere(), s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister(), s.Config.AuthenticationOptions))),
bearertoken.New(jwttoken.NewTokenAuthenticator(im.NewTokenOperator(s.CacheClient, s.Config.AuthenticationOptions))))
handler = filters.WithAuthentication(handler, authn, loginRecorder)
handler = filters.WithRequestInfo(handler, requestInfoResolver)
s.Server.Handler = handler
}
@@ -373,6 +380,7 @@ func (s *APIServer) waitForResourceSync(stopCh <-chan struct{}) error {
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "globalrolebindings"},
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "workspaceroles"},
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "workspacerolebindings"},
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "loginrecords"},
{Group: "cluster.kubesphere.io", Version: "v1alpha1", Resource: "clusters"},
{Group: "devops.kubesphere.io", Version: "v1alpha3", Resource: "devopsprojects"},
}
@@ -514,7 +522,7 @@ func logRequestAndResponse(req *restful.Request, resp *restful.Response, chain *
}
logWithVerbose.Infof("%s - \"%s %s %s\" %d %d %dms",
getRequestIP(req),
utilnet.GetRequestIP(req.Request),
req.Request.Method,
req.Request.URL,
req.Request.Proto,
@@ -524,25 +532,6 @@ func logRequestAndResponse(req *restful.Request, resp *restful.Response, chain *
)
}
func getRequestIP(req *restful.Request) string {
address := strings.Trim(req.Request.Header.Get("X-Real-Ip"), " ")
if address != "" {
return address
}
address = strings.Trim(req.Request.Header.Get("X-Forwarded-For"), " ")
if address != "" {
return address
}
address, _, err := net.SplitHostPort(req.Request.RemoteAddr)
if err != nil {
return req.Request.RemoteAddr
}
return address
}
type errorResponder struct{}
func (e *errorResponder) Error(w http.ResponseWriter, req *http.Request, err error) {

View File

@@ -29,18 +29,18 @@ import (
// and group from user.AllUnauthenticated. This helps requests be passed along the handler chain,
// because some resources are public accessible.
type basicAuthenticator struct {
im im.IdentityManagementInterface
authenticator im.PasswordAuthenticator
}
func NewBasicAuthenticator(im im.IdentityManagementInterface) authenticator.Password {
func NewBasicAuthenticator(authenticator im.PasswordAuthenticator) authenticator.Password {
return &basicAuthenticator{
im: im,
authenticator: authenticator,
}
}
func (t *basicAuthenticator) AuthenticatePassword(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
providedUser, err := t.im.Authenticate(username, password)
providedUser, err := t.authenticator.Authenticate(username, password)
if err != nil {
return nil, false, err
@@ -49,7 +49,7 @@ func (t *basicAuthenticator) AuthenticatePassword(ctx context.Context, username,
return &authenticator.Response{
User: &user.DefaultInfo{
Name: providedUser.GetName(),
UID: string(providedUser.GetUID()),
UID: providedUser.GetUID(),
Groups: []string{user.AllAuthenticated},
},
}, true, nil

View File

@@ -20,7 +20,8 @@ import (
"context"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
token2 "kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/models/iam/im"
)
// TokenAuthenticator implements kubernetes token authenticate interface with our custom logic.
@@ -29,18 +30,19 @@ import (
// and group from user.AllUnauthenticated. This helps requests be passed along the handler chain,
// because some resources are public accessible.
type tokenAuthenticator struct {
jwtTokenIssuer token2.Issuer
tokenOperator im.TokenManagementInterface
}
func NewTokenAuthenticator(issuer token2.Issuer) authenticator.Token {
func NewTokenAuthenticator(tokenOperator im.TokenManagementInterface) authenticator.Token {
return &tokenAuthenticator{
jwtTokenIssuer: issuer,
tokenOperator: tokenOperator,
}
}
func (t *tokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
providedUser, err := t.jwtTokenIssuer.Verify(token)
providedUser, err := t.tokenOperator.Verify(token)
if err != nil {
klog.Error(err)
return nil, false, err
}

View File

@@ -99,7 +99,7 @@ type GithubIdentity struct {
}
func init() {
identityprovider.RegisterOAuthProviderCodec(&Github{})
identityprovider.RegisterOAuthProvider(&Github{})
}
func (g *Github) Type() string {

View File

@@ -16,33 +16,7 @@ limitations under the License.
package identityprovider
import (
"errors"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
)
var (
ErrorIdentityProviderNotFound = errors.New("the identity provider was not found")
oauthProviders = make(map[string]OAuthProvider, 0)
)
type OAuthProvider interface {
Type() string
Setup(options *oauth.DynamicOptions) (OAuthProvider, error)
IdentityExchange(code string) (Identity, error)
}
type Identity interface {
GetName() string
GetEmail() string
}
func GetOAuthProvider(providerType string, options *oauth.DynamicOptions) (OAuthProvider, error) {
if provider, ok := oauthProviders[providerType]; ok {
return provider.Setup(options)
}
return nil, ErrorIdentityProviderNotFound
}
func RegisterOAuthProviderCodec(provider OAuthProvider) {
oauthProviders[provider.Type()] = provider
}

View File

@@ -0,0 +1,117 @@
/*
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 (
"fmt"
"github.com/go-ldap/ldap"
"gopkg.in/yaml.v3"
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"
)
const LdapIdentityProvider = "LDAPIdentityProvider"
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
LoginAttribute string `json:"loginAttribute" yaml:"loginAttribute"`
MailAttribute string `json:"mailAttribute" yaml:"mailAttribute"`
DisplayNameAttribute string `json:"displayNameAttribute" yaml:"displayNameAttribute"`
}
type ldapProvider struct {
options ldapOptions
}
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 {
return nil, err
}
return &ldapProvider{options: ldapOptions}, nil
}
func (l ldapProvider) Authenticate(username string, password string) (*iamv1alpha2.User, error) {
conn, err := ldap.Dial("tcp", l.options.Host)
if err != nil {
klog.Error(err)
return nil, err
}
defer conn.Close()
err = conn.Bind(l.options.ManagerDN, l.options.ManagerPassword)
if err != nil {
klog.Error(err)
return nil, err
}
filter := fmt.Sprintf("(&(%s=%s))", l.options.LoginAttribute, username)
result, err := conn.Search(&ldap.SearchRequest{
BaseDN: l.options.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},
})
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
}
return &iamv1alpha2.User{
ObjectMeta: metav1.ObjectMeta{
Name: username,
},
Spec: iamv1alpha2.UserSpec{
Email: entry.GetAttributeValue(l.options.MailAttribute),
DisplayName: entry.GetAttributeValue(l.options.DisplayNameAttribute),
},
}, nil
}
return nil, ldap.NewError(ldap.LDAPResultNoSuchObject, fmt.Errorf(" could not find user %s in LDAP directory", username))
}

View File

@@ -0,0 +1,46 @@
/*
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 (
"errors"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
)
var (
oauthProviders = make(map[string]OAuthProvider, 0)
ErrorIdentityProviderNotFound = errors.New("the identity provider was not found")
)
type OAuthProvider interface {
Type() string
Setup(options *oauth.DynamicOptions) (OAuthProvider, error)
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
}
func RegisterOAuthProvider(provider OAuthProvider) {
oauthProviders[provider.Type()] = provider
}

View File

@@ -46,6 +46,7 @@ const (
var (
ErrorClientNotFound = errors.New("the OAuth client was not found")
ErrorProviderNotFound = errors.New("the identity provider was not found")
ErrorRedirectURLNotAllowed = errors.New("redirect URL is not allowed")
)
@@ -92,7 +93,7 @@ type IdentityProviderOptions struct {
Type string `json:"type" yaml:"type"`
// The options of identify provider
Provider *DynamicOptions `json:"provider,omitempty" yaml:"provider"`
Provider *DynamicOptions `json:"-" yaml:"provider"`
}
type Token struct {
@@ -155,6 +156,7 @@ var (
DefaultAccessTokenInactivityTimeout = time.Duration(0)
DefaultClients = []Client{{
Name: "default",
Secret: "kubesphere",
RespondWithChallenges: true,
RedirectURIs: []string{AllowAllRedirectURI},
GrantMethod: GrantHandlerAuto,
@@ -177,13 +179,13 @@ func (o *Options) OAuthClient(name string) (Client, error) {
}
return Client{}, ErrorClientNotFound
}
func (o *Options) IdentityProviderOptions(name string) (IdentityProviderOptions, error) {
func (o *Options) IdentityProviderOptions(name string) (*IdentityProviderOptions, error) {
for _, found := range o.IdentityProviders {
if found.Name == name {
return found, nil
return &found, nil
}
}
return IdentityProviderOptions{}, ErrorClientNotFound
return nil, ErrorProviderNotFound
}
func (c Client) anyRedirectAbleURI() []string {
@@ -224,7 +226,7 @@ func NewOptions() *Options {
return &Options{
IdentityProviders: make([]IdentityProviderOptions, 0),
Clients: make([]Client, 0),
AccessTokenMaxAge: time.Hour * 24,
AccessTokenInactivityTimeout: 0,
AccessTokenMaxAge: time.Hour * 2,
AccessTokenInactivityTimeout: time.Hour * 2,
}
}

View File

@@ -28,6 +28,7 @@ func TestDefaultAuthOptions(t *testing.T) {
expect := Client{
Name: "default",
RespondWithChallenges: true,
Secret: "kubesphere",
RedirectURIs: []string{AllowAllRedirectURI},
GrantMethod: GrantHandlerAuto,
ScopeRestrictions: []string{"full"},

View File

@@ -25,9 +25,13 @@ import (
)
type AuthenticationOptions struct {
// authenticate rate limit will
// authenticate rate limit
AuthenticateRateLimiterMaxTries int `json:"authenticateRateLimiterMaxTries" yaml:"authenticateRateLimiterMaxTries"`
AuthenticateRateLimiterDuration time.Duration `json:"authenticationRateLimiterDuration" yaml:"authenticationRateLimiterDuration"`
AuthenticateRateLimiterDuration time.Duration `json:"authenticateRateLimiterDuration" yaml:"authenticateRateLimiterDuration"`
// Token verification maximum time difference
MaximumClockSkew time.Duration `json:"maximumClockSkew" yaml:"maximumClockSkew"`
// retention login records
RecordRetentionPeriod time.Duration `json:"recordRetentionPeriod" yaml:"recordRetentionPeriod"`
// allow multiple users login at the same time
MultipleLogin bool `json:"multipleLogin" yaml:"multipleLogin"`
// secret to signed jwt token
@@ -41,6 +45,8 @@ func NewAuthenticateOptions() *AuthenticationOptions {
return &AuthenticationOptions{
AuthenticateRateLimiterMaxTries: 5,
AuthenticateRateLimiterDuration: time.Minute * 30,
MaximumClockSkew: 10 * time.Second,
RecordRetentionPeriod: time.Hour * 24 * 7,
OAuthOptions: oauth.NewOptions(),
MultipleLogin: false,
JwtSecret: "",
@@ -64,4 +70,5 @@ func (options *AuthenticationOptions) AddFlags(fs *pflag.FlagSet, s *Authenticat
fs.StringVar(&options.JwtSecret, "jwt-secret", s.JwtSecret, "Secret to sign jwt token, must not be empty.")
fs.DurationVar(&options.OAuthOptions.AccessTokenMaxAge, "access-token-max-age", s.OAuthOptions.AccessTokenMaxAge, "AccessTokenMaxAgeSeconds control the lifetime of access tokens, 0 means no expiration.")
fs.StringVar(&s.KubectlImage, "kubectl-image", s.KubectlImage, "Setup the image used by kubectl terminal pod")
fs.DurationVar(&options.MaximumClockSkew, "maximum-clock-skew", s.MaximumClockSkew, "The maximum time difference between the system clocks of the ks-apiserver that issued a JWT and the ks-apiserver that verified the JWT.")
}

View File

@@ -16,16 +16,24 @@ limitations under the License.
package token
import "time"
import (
"k8s.io/apiserver/pkg/authentication/user"
"time"
)
const (
AccessToken TokenType = "access_token"
RefreshToken TokenType = "refresh_token"
StaticToken TokenType = "static_token"
)
type TokenType string
// Issuer issues token to user, tokens are required to perform mutating requests to resources
type Issuer interface {
// IssueTo issues a token a User, return error if issuing process failed
IssueTo(user User, expiresIn time.Duration) (string, error)
IssueTo(user user.Info, tokenType TokenType, expiresIn time.Duration) (string, error)
// Verify verifies a token, and return a User if it's a valid token, otherwise return error
Verify(string) (User, error)
// Revoke a token,
Revoke(token string) error
// Verify verifies a token, and return a user info if it's a valid token, otherwise return error
Verify(string) (user.Info, TokenType, error)
}

View File

@@ -21,68 +21,50 @@ import (
"github.com/dgrijalva/jwt-go"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/klog"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/server/errors"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"time"
)
const DefaultIssuerName = "kubesphere"
var (
errInvalidToken = errors.New("invalid token")
errTokenExpired = errors.New("expired token")
const (
DefaultIssuerName = "kubesphere"
)
type Claims struct {
Username string `json:"username"`
UID string `json:"uid"`
Username string `json:"username"`
UID string `json:"uid"`
TokenType TokenType `json:"token_type"`
// Currently, we are not using any field in jwt.StandardClaims
jwt.StandardClaims
}
type jwtTokenIssuer struct {
name string
options *authoptions.AuthenticationOptions
cache cache.Interface
keyFunc jwt.Keyfunc
name string
secret []byte
// Maximum time difference
maximumClockSkew time.Duration
}
func (s *jwtTokenIssuer) Verify(tokenString string) (User, error) {
if len(tokenString) == 0 {
return nil, errInvalidToken
}
func (s *jwtTokenIssuer) Verify(tokenString string) (user.Info, TokenType, error) {
clm := &Claims{}
// verify token signature and expiration time
_, err := jwt.ParseWithClaims(tokenString, clm, s.keyFunc)
if err != nil {
return nil, err
klog.Error(err)
return nil, "", err
}
// accessTokenMaxAge = 0 or token without expiration time means that the token will not expire
// do not validate token cache
if s.options.OAuthOptions.AccessTokenMaxAge > 0 && clm.ExpiresAt > 0 {
_, err = s.cache.Get(tokenCacheKey(tokenString))
if err != nil {
if err == cache.ErrNoSuchKey {
return nil, errTokenExpired
}
return nil, err
}
}
return &user.DefaultInfo{Name: clm.Username, UID: clm.UID}, nil
return &user.DefaultInfo{Name: clm.Username, UID: clm.UID}, clm.TokenType, nil
}
func (s *jwtTokenIssuer) IssueTo(user User, expiresIn time.Duration) (string, error) {
func (s *jwtTokenIssuer) IssueTo(user user.Info, tokenType TokenType, expiresIn time.Duration) (string, error) {
issueAt := time.Now().Unix() - int64(s.maximumClockSkew.Seconds())
notBefore := issueAt
clm := &Claims{
Username: user.GetName(),
UID: user.GetUID(),
Username: user.GetName(),
UID: user.GetUID(),
TokenType: tokenType,
StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().Unix(),
IssuedAt: issueAt,
Issuer: s.name,
NotBefore: time.Now().Unix(),
NotBefore: notBefore,
},
}
@@ -92,48 +74,28 @@ func (s *jwtTokenIssuer) IssueTo(user User, expiresIn time.Duration) (string, er
token := jwt.NewWithClaims(jwt.SigningMethodHS256, clm)
tokenString, err := token.SignedString([]byte(s.options.JwtSecret))
tokenString, err := token.SignedString(s.secret)
if err != nil {
klog.Error(err)
return "", err
}
// 0 means no expiration.
// validate token cache
if s.options.OAuthOptions.AccessTokenMaxAge > 0 {
err = s.cache.Set(tokenCacheKey(tokenString), tokenString, s.options.OAuthOptions.AccessTokenMaxAge)
if err != nil {
klog.Error(err)
return "", err
}
}
return tokenString, nil
}
func (s *jwtTokenIssuer) Revoke(token string) error {
if s.options.OAuthOptions.AccessTokenMaxAge > 0 {
return s.cache.Del(tokenCacheKey(token))
func (s *jwtTokenIssuer) keyFunc(token *jwt.Token) (i interface{}, err error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
return s.secret, nil
} else {
return nil, fmt.Errorf("expect token signed with HMAC but got %v", token.Header["alg"])
}
return nil
}
func NewJwtTokenIssuer(issuerName string, options *authoptions.AuthenticationOptions, cache cache.Interface) Issuer {
func NewTokenIssuer(secret string, maximumClockSkew time.Duration) Issuer {
return &jwtTokenIssuer{
name: issuerName,
options: options,
cache: cache,
keyFunc: func(token *jwt.Token) (i interface{}, err error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
return []byte(options.JwtSecret), nil
} else {
return nil, fmt.Errorf("expect token signed with HMAC but got %v", token.Header["alg"])
}
},
name: DefaultIssuerName,
secret: []byte(secret),
maximumClockSkew: maximumClockSkew,
}
}
func tokenCacheKey(token string) string {
return fmt.Sprintf("kubesphere:tokens:%s", token)
}

View File

@@ -19,89 +19,30 @@ package token
import (
"github.com/google/go-cmp/cmp"
"k8s.io/apiserver/pkg/authentication/user"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"testing"
)
func TestJwtTokenIssuer(t *testing.T) {
options := authoptions.NewAuthenticateOptions()
options.JwtSecret = "kubesphere"
issuer := NewJwtTokenIssuer(DefaultIssuerName, options, cache.NewSimpleCache())
testCases := []struct {
description string
name string
uid string
email string
}{
{
name: "admin",
uid: "b8be6edd-2c92-4535-9b2a-df6326474458",
},
{
name: "bar",
uid: "b8be6edd-2c92-4535-9b2a-df6326474452",
},
}
for _, testCase := range testCases {
user := &user.DefaultInfo{
Name: testCase.name,
UID: testCase.uid,
}
t.Run(testCase.description, func(t *testing.T) {
token, err := issuer.IssueTo(user, 0)
if err != nil {
t.Fatal(err)
}
got, err := issuer.Verify(token)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(user, got); len(diff) != 0 {
t.Errorf("%T differ (-got, +expected), %s", user, diff)
}
})
}
}
func TestTokenVerifyWithoutCacheValidate(t *testing.T) {
options := authoptions.NewAuthenticateOptions()
// do not set token cache and disable token cache validate,
options.OAuthOptions = &oauth.Options{AccessTokenMaxAge: 0}
options.JwtSecret = "kubesphere"
issuer := NewJwtTokenIssuer(DefaultIssuerName, options, nil)
issuer := NewTokenIssuer("kubesphere", 0)
client, err := options.OAuthOptions.OAuthClient("default")
if err != nil {
t.Fatal(err)
}
user := &user.DefaultInfo{
admin := &user.DefaultInfo{
Name: "admin",
UID: "admin",
}
tokenString, err := issuer.IssueTo(user, *client.AccessTokenMaxAge)
tokenString, err := issuer.IssueTo(admin, AccessToken, 0)
if err != nil {
t.Fatal(err)
}
got, err := issuer.Verify(tokenString)
got, _, err := issuer.Verify(tokenString)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(got, user); diff != "" {
if diff := cmp.Diff(got, admin); diff != "" {
t.Error("token validate failed")
}
}

View File

@@ -1,25 +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 token
type User interface {
// Name
GetName() string
// UID
GetUID() string
}

View File

@@ -27,24 +27,31 @@ import (
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/apiserver/request"
"kubesphere.io/kubesphere/pkg/models/iam/im"
"net/http"
)
// WithAuthentication installs authentication handler to handler chain.
func WithAuthentication(handler http.Handler, auth authenticator.Request) http.Handler {
func WithAuthentication(handler http.Handler, auth authenticator.Request, loginRecorder im.LoginRecorder) http.Handler {
if auth == nil {
klog.Warningf("Authentication is disabled")
return handler
}
s := serializer.NewCodecFactory(runtime.NewScheme()).WithoutConversion()
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
resp, ok, err := auth.AuthenticateRequest(req)
if err != nil || !ok {
if err != nil {
klog.Errorf("Unable to authenticate the request due to error: %v", err)
if err == im.AuthFailedIncorrectPassword { // log failed login attempts
go func() {
if loginRecorder != nil && resp != nil {
err = loginRecorder.RecordLogin(resp.User.GetName(), err, req)
klog.Errorf("Failed to record unsuccessful login attempt for user %s", resp.User.GetName())
}
}()
}
}
ctx := req.Context()
@@ -55,7 +62,7 @@ func WithAuthentication(handler http.Handler, auth authenticator.Request) http.H
}
gv := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
responsewriters.ErrorNegotiated(apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized:%s", err)), s, gv, w, req)
responsewriters.ErrorNegotiated(apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)), s, gv, w, req)
return
}

View File

@@ -36,6 +36,10 @@ func (c *FakeIamV1alpha2) GlobalRoleBindings() v1alpha2.GlobalRoleBindingInterfa
return &FakeGlobalRoleBindings{c}
}
func (c *FakeIamV1alpha2) LoginRecords() v1alpha2.LoginRecordInterface {
return &FakeLoginRecords{c}
}
func (c *FakeIamV1alpha2) RoleBases() v1alpha2.RoleBaseInterface {
return &FakeRoleBases{c}
}

View File

@@ -0,0 +1,120 @@
/*
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.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
schema "k8s.io/apimachinery/pkg/runtime/schema"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
testing "k8s.io/client-go/testing"
v1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
)
// FakeLoginRecords implements LoginRecordInterface
type FakeLoginRecords struct {
Fake *FakeIamV1alpha2
}
var loginrecordsResource = schema.GroupVersionResource{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "loginrecords"}
var loginrecordsKind = schema.GroupVersionKind{Group: "iam.kubesphere.io", Version: "v1alpha2", Kind: "LoginRecord"}
// Get takes name of the loginRecord, and returns the corresponding loginRecord object, and an error if there is any.
func (c *FakeLoginRecords) Get(name string, options v1.GetOptions) (result *v1alpha2.LoginRecord, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootGetAction(loginrecordsResource, name), &v1alpha2.LoginRecord{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha2.LoginRecord), err
}
// List takes label and field selectors, and returns the list of LoginRecords that match those selectors.
func (c *FakeLoginRecords) List(opts v1.ListOptions) (result *v1alpha2.LoginRecordList, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootListAction(loginrecordsResource, loginrecordsKind, opts), &v1alpha2.LoginRecordList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &v1alpha2.LoginRecordList{ListMeta: obj.(*v1alpha2.LoginRecordList).ListMeta}
for _, item := range obj.(*v1alpha2.LoginRecordList).Items {
if label.Matches(labels.Set(item.Labels)) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Watch returns a watch.Interface that watches the requested loginRecords.
func (c *FakeLoginRecords) Watch(opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewRootWatchAction(loginrecordsResource, opts))
}
// Create takes the representation of a loginRecord and creates it. Returns the server's representation of the loginRecord, and an error, if there is any.
func (c *FakeLoginRecords) Create(loginRecord *v1alpha2.LoginRecord) (result *v1alpha2.LoginRecord, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootCreateAction(loginrecordsResource, loginRecord), &v1alpha2.LoginRecord{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha2.LoginRecord), err
}
// Update takes the representation of a loginRecord and updates it. Returns the server's representation of the loginRecord, and an error, if there is any.
func (c *FakeLoginRecords) Update(loginRecord *v1alpha2.LoginRecord) (result *v1alpha2.LoginRecord, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootUpdateAction(loginrecordsResource, loginRecord), &v1alpha2.LoginRecord{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha2.LoginRecord), err
}
// Delete takes name of the loginRecord and deletes it. Returns an error if one occurs.
func (c *FakeLoginRecords) Delete(name string, options *v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewRootDeleteAction(loginrecordsResource, name), &v1alpha2.LoginRecord{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakeLoginRecords) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
action := testing.NewRootDeleteCollectionAction(loginrecordsResource, listOptions)
_, err := c.Fake.Invokes(action, &v1alpha2.LoginRecordList{})
return err
}
// Patch applies the patch and returns the patched loginRecord.
func (c *FakeLoginRecords) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.LoginRecord, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootPatchSubresourceAction(loginrecordsResource, name, pt, data, subresources...), &v1alpha2.LoginRecord{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha2.LoginRecord), err
}

View File

@@ -22,6 +22,8 @@ type GlobalRoleExpansion interface{}
type GlobalRoleBindingExpansion interface{}
type LoginRecordExpansion interface{}
type RoleBaseExpansion interface{}
type UserExpansion interface{}

View File

@@ -28,6 +28,7 @@ type IamV1alpha2Interface interface {
RESTClient() rest.Interface
GlobalRolesGetter
GlobalRoleBindingsGetter
LoginRecordsGetter
RoleBasesGetter
UsersGetter
WorkspaceRolesGetter
@@ -47,6 +48,10 @@ func (c *IamV1alpha2Client) GlobalRoleBindings() GlobalRoleBindingInterface {
return newGlobalRoleBindings(c)
}
func (c *IamV1alpha2Client) LoginRecords() LoginRecordInterface {
return newLoginRecords(c)
}
func (c *IamV1alpha2Client) RoleBases() RoleBaseInterface {
return newRoleBases(c)
}

View File

@@ -0,0 +1,164 @@
/*
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.
*/
// Code generated by client-gen. DO NOT EDIT.
package v1alpha2
import (
"time"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
rest "k8s.io/client-go/rest"
v1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
scheme "kubesphere.io/kubesphere/pkg/client/clientset/versioned/scheme"
)
// LoginRecordsGetter has a method to return a LoginRecordInterface.
// A group's client should implement this interface.
type LoginRecordsGetter interface {
LoginRecords() LoginRecordInterface
}
// LoginRecordInterface has methods to work with LoginRecord resources.
type LoginRecordInterface interface {
Create(*v1alpha2.LoginRecord) (*v1alpha2.LoginRecord, error)
Update(*v1alpha2.LoginRecord) (*v1alpha2.LoginRecord, error)
Delete(name string, options *v1.DeleteOptions) error
DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
Get(name string, options v1.GetOptions) (*v1alpha2.LoginRecord, error)
List(opts v1.ListOptions) (*v1alpha2.LoginRecordList, error)
Watch(opts v1.ListOptions) (watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.LoginRecord, err error)
LoginRecordExpansion
}
// loginRecords implements LoginRecordInterface
type loginRecords struct {
client rest.Interface
}
// newLoginRecords returns a LoginRecords
func newLoginRecords(c *IamV1alpha2Client) *loginRecords {
return &loginRecords{
client: c.RESTClient(),
}
}
// Get takes name of the loginRecord, and returns the corresponding loginRecord object, and an error if there is any.
func (c *loginRecords) Get(name string, options v1.GetOptions) (result *v1alpha2.LoginRecord, err error) {
result = &v1alpha2.LoginRecord{}
err = c.client.Get().
Resource("loginrecords").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do().
Into(result)
return
}
// List takes label and field selectors, and returns the list of LoginRecords that match those selectors.
func (c *loginRecords) List(opts v1.ListOptions) (result *v1alpha2.LoginRecordList, err error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
result = &v1alpha2.LoginRecordList{}
err = c.client.Get().
Resource("loginrecords").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Do().
Into(result)
return
}
// Watch returns a watch.Interface that watches the requested loginRecords.
func (c *loginRecords) Watch(opts v1.ListOptions) (watch.Interface, error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
opts.Watch = true
return c.client.Get().
Resource("loginrecords").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Watch()
}
// Create takes the representation of a loginRecord and creates it. Returns the server's representation of the loginRecord, and an error, if there is any.
func (c *loginRecords) Create(loginRecord *v1alpha2.LoginRecord) (result *v1alpha2.LoginRecord, err error) {
result = &v1alpha2.LoginRecord{}
err = c.client.Post().
Resource("loginrecords").
Body(loginRecord).
Do().
Into(result)
return
}
// Update takes the representation of a loginRecord and updates it. Returns the server's representation of the loginRecord, and an error, if there is any.
func (c *loginRecords) Update(loginRecord *v1alpha2.LoginRecord) (result *v1alpha2.LoginRecord, err error) {
result = &v1alpha2.LoginRecord{}
err = c.client.Put().
Resource("loginrecords").
Name(loginRecord.Name).
Body(loginRecord).
Do().
Into(result)
return
}
// Delete takes name of the loginRecord and deletes it. Returns an error if one occurs.
func (c *loginRecords) Delete(name string, options *v1.DeleteOptions) error {
return c.client.Delete().
Resource("loginrecords").
Name(name).
Body(options).
Do().
Error()
}
// DeleteCollection deletes a collection of objects.
func (c *loginRecords) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
var timeout time.Duration
if listOptions.TimeoutSeconds != nil {
timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second
}
return c.client.Delete().
Resource("loginrecords").
VersionedParams(&listOptions, scheme.ParameterCodec).
Timeout(timeout).
Body(options).
Do().
Error()
}
// Patch applies the patch and returns the patched loginRecord.
func (c *loginRecords) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha2.LoginRecord, err error) {
result = &v1alpha2.LoginRecord{}
err = c.client.Patch(pt).
Resource("loginrecords").
SubResource(subresources...).
Name(name).
Body(data).
Do().
Into(result)
return
}

View File

@@ -93,6 +93,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
return &genericInformer{resource: resource.GroupResource(), informer: f.Iam().V1alpha2().GlobalRoles().Informer()}, nil
case v1alpha2.SchemeGroupVersion.WithResource("globalrolebindings"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Iam().V1alpha2().GlobalRoleBindings().Informer()}, nil
case v1alpha2.SchemeGroupVersion.WithResource("loginrecords"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Iam().V1alpha2().LoginRecords().Informer()}, nil
case v1alpha2.SchemeGroupVersion.WithResource("rolebases"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Iam().V1alpha2().RoleBases().Informer()}, nil
case v1alpha2.SchemeGroupVersion.WithResource("users"):

View File

@@ -28,6 +28,8 @@ type Interface interface {
GlobalRoles() GlobalRoleInformer
// GlobalRoleBindings returns a GlobalRoleBindingInformer.
GlobalRoleBindings() GlobalRoleBindingInformer
// LoginRecords returns a LoginRecordInformer.
LoginRecords() LoginRecordInformer
// RoleBases returns a RoleBaseInformer.
RoleBases() RoleBaseInformer
// Users returns a UserInformer.
@@ -59,6 +61,11 @@ func (v *version) GlobalRoleBindings() GlobalRoleBindingInformer {
return &globalRoleBindingInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
}
// LoginRecords returns a LoginRecordInformer.
func (v *version) LoginRecords() LoginRecordInformer {
return &loginRecordInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
}
// RoleBases returns a RoleBaseInformer.
func (v *version) RoleBases() RoleBaseInformer {
return &roleBaseInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}

View File

@@ -0,0 +1,88 @@
/*
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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package v1alpha2
import (
time "time"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
versioned "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
internalinterfaces "kubesphere.io/kubesphere/pkg/client/informers/externalversions/internalinterfaces"
v1alpha2 "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
)
// LoginRecordInformer provides access to a shared informer and lister for
// LoginRecords.
type LoginRecordInformer interface {
Informer() cache.SharedIndexInformer
Lister() v1alpha2.LoginRecordLister
}
type loginRecordInformer struct {
factory internalinterfaces.SharedInformerFactory
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// NewLoginRecordInformer constructs a new informer for LoginRecord type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewLoginRecordInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredLoginRecordInformer(client, resyncPeriod, indexers, nil)
}
// NewFilteredLoginRecordInformer constructs a new informer for LoginRecord type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredLoginRecordInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.IamV1alpha2().LoginRecords().List(options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.IamV1alpha2().LoginRecords().Watch(options)
},
},
&iamv1alpha2.LoginRecord{},
resyncPeriod,
indexers,
)
}
func (f *loginRecordInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredLoginRecordInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}
func (f *loginRecordInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&iamv1alpha2.LoginRecord{}, f.defaultInformer)
}
func (f *loginRecordInformer) Lister() v1alpha2.LoginRecordLister {
return v1alpha2.NewLoginRecordLister(f.Informer().GetIndexer())
}

View File

@@ -26,6 +26,10 @@ type GlobalRoleListerExpansion interface{}
// GlobalRoleBindingLister.
type GlobalRoleBindingListerExpansion interface{}
// LoginRecordListerExpansion allows custom methods to be added to
// LoginRecordLister.
type LoginRecordListerExpansion interface{}
// RoleBaseListerExpansion allows custom methods to be added to
// RoleBaseLister.
type RoleBaseListerExpansion interface{}

View File

@@ -0,0 +1,65 @@
/*
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.
*/
// Code generated by lister-gen. DO NOT EDIT.
package v1alpha2
import (
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
v1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
)
// LoginRecordLister helps list LoginRecords.
type LoginRecordLister interface {
// List lists all LoginRecords in the indexer.
List(selector labels.Selector) (ret []*v1alpha2.LoginRecord, err error)
// Get retrieves the LoginRecord from the index for a given name.
Get(name string) (*v1alpha2.LoginRecord, error)
LoginRecordListerExpansion
}
// loginRecordLister implements the LoginRecordLister interface.
type loginRecordLister struct {
indexer cache.Indexer
}
// NewLoginRecordLister returns a new LoginRecordLister.
func NewLoginRecordLister(indexer cache.Indexer) LoginRecordLister {
return &loginRecordLister{indexer: indexer}
}
// List lists all LoginRecords in the indexer.
func (s *loginRecordLister) List(selector labels.Selector) (ret []*v1alpha2.LoginRecord, err error) {
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
ret = append(ret, m.(*v1alpha2.LoginRecord))
})
return ret, err
}
// Get retrieves the LoginRecord from the index for a given name.
func (s *loginRecordLister) Get(name string) (*v1alpha2.LoginRecord, error) {
obj, exists, err := s.indexer.GetByKey(name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(v1alpha2.Resource("loginrecord"), name)
}
return obj.(*v1alpha2.LoginRecord), nil
}

View File

@@ -0,0 +1,230 @@
/*
*
* 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 user
import (
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
iamv1alpha2informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions/iam/v1alpha2"
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
"time"
)
type LoginRecordController struct {
k8sClient kubernetes.Interface
ksClient kubesphere.Interface
loginRecordInformer iamv1alpha2informers.LoginRecordInformer
loginRecordLister iamv1alpha2listers.LoginRecordLister
loginRecordSynced cache.InformerSynced
// workqueue is a rate limited work queue. This is used to queue work to be
// processed instead of performing it as soon as a change happens. This
// means we can ensure we only process a fixed amount of resources at a
// time, and makes it easy to ensure we are never processing the same item
// simultaneously in two different workers.
workqueue workqueue.RateLimitingInterface
// recorder is an event recorder for recording Event resources to the
// Kubernetes API.
recorder record.EventRecorder
authenticationOptions *authoptions.AuthenticationOptions
}
func NewLoginRecordController(k8sClient kubernetes.Interface, ksClient kubesphere.Interface,
loginRecordInformer iamv1alpha2informers.LoginRecordInformer,
authenticationOptions *authoptions.AuthenticationOptions) *LoginRecordController {
klog.V(4).Info("Creating event broadcaster")
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(klog.Infof)
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: k8sClient.CoreV1().Events("")})
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "loginrecord-controller"})
ctl := &LoginRecordController{
k8sClient: k8sClient,
ksClient: ksClient,
loginRecordInformer: loginRecordInformer,
loginRecordLister: loginRecordInformer.Lister(),
loginRecordSynced: loginRecordInformer.Informer().HasSynced,
authenticationOptions: authenticationOptions,
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "WorkspaceTemplate"),
recorder: recorder,
}
klog.Error(authenticationOptions.RecordRetentionPeriod)
klog.Info("Setting up event handlers")
return ctl
}
func (c *LoginRecordController) Run(threadiness int, stopCh <-chan struct{}) error {
defer utilruntime.HandleCrash()
defer c.workqueue.ShutDown()
// Start the informer factories to begin populating the informer caches
klog.Info("Starting LoginRecord controller")
// Wait for the caches to be synced before starting workers
klog.Info("Waiting for informer caches to sync")
if ok := cache.WaitForCacheSync(stopCh, c.loginRecordSynced); !ok {
return fmt.Errorf("failed to wait for caches to sync")
}
klog.Info("Starting workers")
// Launch two workers to process Foo resources
for i := 0; i < threadiness; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}
go wait.Until(func() {
if err := c.sync(); err != nil {
klog.Errorf("Error periodically sync user status, %v", err)
}
}, time.Hour, stopCh)
klog.Info("Started workers")
<-stopCh
klog.Info("Shutting down workers")
return nil
}
func (c *LoginRecordController) enqueueLoginRecord(obj interface{}) {
var key string
var err error
if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
utilruntime.HandleError(err)
return
}
c.workqueue.Add(key)
}
func (c *LoginRecordController) runWorker() {
for c.processNextWorkItem() {
}
}
func (c *LoginRecordController) processNextWorkItem() bool {
obj, shutdown := c.workqueue.Get()
if shutdown {
return false
}
// We wrap this block in a func so we can defer c.workqueue.Done.
err := func(obj interface{}) error {
// We call Done here so the workqueue knows we have finished
// processing this item. We also must remember to call Forget if we
// do not want this work item being re-queued. For example, we do
// not call Forget if a transient error occurs, instead the item is
// put back on the workqueue and attempted again after a back-off
// period.
defer c.workqueue.Done(obj)
var key string
var ok bool
// We expect strings to come off the workqueue. These are of the
// form namespace/name. We do this as the delayed nature of the
// workqueue means the items in the informer cache may actually be
// more up to date that when the item was initially put onto the
// workqueue.
if key, ok = obj.(string); !ok {
// As the item in the workqueue is actually invalid, we call
// Forget here else we'd go into a loop of attempting to
// process a work item that is invalid.
c.workqueue.Forget(obj)
utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
return nil
}
// Run the reconcile, passing it the namespace/name string of the
// Foo resource to be synced.
if err := c.reconcile(key); err != nil {
// Put the item back on the workqueue to handle any transient errors.
c.workqueue.AddRateLimited(key)
return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
}
// Finally, if no error occurs we Forget this item so it does not
// get queued again until another change happens.
c.workqueue.Forget(obj)
klog.Infof("Successfully synced %s:%s", "key", key)
return nil
}(obj)
if err != nil {
utilruntime.HandleError(err)
return true
}
return true
}
// syncHandler compares the actual state with the desired, and attempts to
// converge the two. It then updates the Status block of the Foo resource
// with the current status of the resource.
func (c *LoginRecordController) reconcile(key string) error {
loginRecord, err := c.loginRecordLister.Get(key)
if err != nil {
// The user may no longer exist, in which case we stop
// processing.
if errors.IsNotFound(err) {
utilruntime.HandleError(fmt.Errorf("login record '%s' in work queue no longer exists", key))
return nil
}
klog.Error(err)
return err
}
now := time.Now()
if now.Sub(loginRecord.CreationTimestamp.Time) > c.authenticationOptions.RecordRetentionPeriod {
if err = c.ksClient.IamV1alpha2().LoginRecords().Delete(loginRecord.Name, metav1.NewDeleteOptions(0)); err != nil {
klog.Error(err)
return err
}
}
c.recorder.Event(loginRecord, corev1.EventTypeNormal, successSynced, messageResourceSynced)
return nil
}
func (c *LoginRecordController) Start(stopCh <-chan struct{}) error {
return c.Run(4, stopCh)
}
func (c *LoginRecordController) sync() error {
records, err := c.loginRecordLister.List(labels.Everything())
if err != nil {
return err
}
for _, record := range records {
key, err := cache.MetaNamespaceKeyFunc(record)
if err != nil {
return err
}
c.workqueue.AddRateLimited(key)
}
return nil
}

View File

@@ -38,6 +38,7 @@ import (
"k8s.io/client-go/util/workqueue"
"k8s.io/klog"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
kubespherescheme "kubesphere.io/kubesphere/pkg/client/clientset/versioned/scheme"
iamv1alpha2informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions/iam/v1alpha2"
@@ -58,19 +59,25 @@ const (
// is synced successfully
messageResourceSynced = "User synced successfully"
controllerName = "user-controller"
// user finalizer
finalizer = "finalizers.kubesphere.io/users"
)
type Controller struct {
k8sClient kubernetes.Interface
ksClient kubesphere.Interface
kubeconfig kubeconfig.Interface
userInformer iamv1alpha2informers.UserInformer
userLister iamv1alpha2listers.UserLister
userSynced cache.InformerSynced
cmSynced cache.InformerSynced
fedUserCache cache.Store
fedUserController cache.Controller
ldapClient ldapclient.Interface
k8sClient kubernetes.Interface
ksClient kubesphere.Interface
kubeconfig kubeconfig.Interface
userInformer iamv1alpha2informers.UserInformer
userLister iamv1alpha2listers.UserLister
userSynced cache.InformerSynced
loginRecordInformer iamv1alpha2informers.LoginRecordInformer
loginRecordLister iamv1alpha2listers.LoginRecordLister
loginRecordSynced cache.InformerSynced
cmSynced cache.InformerSynced
fedUserCache cache.Store
fedUserController cache.Controller
ldapClient ldapclient.Interface
// workqueue is a rate limited work queue. This is used to queue work to be
// processed instead of performing it as soon as a change happens. This
// means we can ensure we only process a fixed amount of resources at a
@@ -79,15 +86,19 @@ type Controller struct {
workqueue workqueue.RateLimitingInterface
// recorder is an event recorder for recording Event resources to the
// Kubernetes API.
recorder record.EventRecorder
multiClusterEnabled bool
recorder record.EventRecorder
authenticationOptions *authoptions.AuthenticationOptions
multiClusterEnabled bool
}
func NewController(k8sClient kubernetes.Interface, ksClient kubesphere.Interface,
func NewUserController(k8sClient kubernetes.Interface, ksClient kubesphere.Interface,
config *rest.Config, userInformer iamv1alpha2informers.UserInformer,
fedUserCache cache.Store, fedUserController cache.Controller,
loginRecordInformer iamv1alpha2informers.LoginRecordInformer,
configMapInformer corev1informers.ConfigMapInformer,
ldapClient ldapclient.Interface, multiClusterEnabled bool) *Controller {
ldapClient ldapclient.Interface,
authenticationOptions *authoptions.AuthenticationOptions,
multiClusterEnabled bool) *Controller {
// Create event broadcaster
// Add sample-controller types to the default Kubernetes Scheme so Events can be
// logged for sample-controller types.
@@ -103,19 +114,23 @@ func NewController(k8sClient kubernetes.Interface, ksClient kubesphere.Interface
kubeconfigOperator = kubeconfig.NewOperator(k8sClient, configMapInformer, config)
}
ctl := &Controller{
k8sClient: k8sClient,
ksClient: ksClient,
kubeconfig: kubeconfigOperator,
userInformer: userInformer,
userLister: userInformer.Lister(),
userSynced: userInformer.Informer().HasSynced,
cmSynced: configMapInformer.Informer().HasSynced,
fedUserCache: fedUserCache,
fedUserController: fedUserController,
ldapClient: ldapClient,
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Users"),
recorder: recorder,
multiClusterEnabled: multiClusterEnabled,
k8sClient: k8sClient,
ksClient: ksClient,
kubeconfig: kubeconfigOperator,
userInformer: userInformer,
userLister: userInformer.Lister(),
userSynced: userInformer.Informer().HasSynced,
loginRecordInformer: loginRecordInformer,
loginRecordLister: loginRecordInformer.Lister(),
loginRecordSynced: loginRecordInformer.Informer().HasSynced,
cmSynced: configMapInformer.Informer().HasSynced,
fedUserCache: fedUserCache,
fedUserController: fedUserController,
ldapClient: ldapClient,
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Users"),
recorder: recorder,
multiClusterEnabled: multiClusterEnabled,
authenticationOptions: authenticationOptions,
}
klog.Info("Setting up event handlers")
userInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
@@ -125,6 +140,18 @@ func NewController(k8sClient kubernetes.Interface, ksClient kubesphere.Interface
},
DeleteFunc: ctl.enqueueUser,
})
loginRecordInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(new interface{}) {
if username := new.(*iamv1alpha2.LoginRecord).Labels[iamv1alpha2.UserReferenceLabel]; username != "" {
ctl.workqueue.Add(username)
}
},
DeleteFunc: func(obj interface{}) {
if username := obj.(*iamv1alpha2.LoginRecord).Labels[iamv1alpha2.UserReferenceLabel]; username != "" {
ctl.workqueue.Add(username)
}
},
})
return ctl
}
@@ -139,7 +166,7 @@ func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {
klog.Info("Waiting for informer caches to sync")
synced := make([]cache.InformerSynced, 0)
synced = append(synced, c.userSynced, c.cmSynced)
synced = append(synced, c.userSynced, c.loginRecordSynced, c.cmSynced)
if c.multiClusterEnabled {
synced = append(synced, c.fedUserController.HasSynced)
}
@@ -182,39 +209,19 @@ func (c *Controller) processNextWorkItem() bool {
return false
}
// We wrap this block in a func so we can defer c.workqueue.Done.
err := func(obj interface{}) error {
// We call Done here so the workqueue knows we have finished
// processing this item. We also must remember to call Forget if we
// do not want this work item being re-queued. For example, we do
// not call Forget if a transient error occurs, instead the item is
// put back on the workqueue and attempted again after a back-off
// period.
defer c.workqueue.Done(obj)
var key string
var ok bool
// We expect strings to come off the workqueue. These are of the
// form namespace/name. We do this as the delayed nature of the
// workqueue means the items in the informer cache may actually be
// more up to date that when the item was initially put onto the
// workqueue.
if key, ok = obj.(string); !ok {
// As the item in the workqueue is actually invalid, we call
// Forget here else we'd go into a loop of attempting to
// process a work item that is invalid.
c.workqueue.Forget(obj)
utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
return nil
}
// Run the reconcile, passing it the namespace/name string of the
// Foo resource to be synced.
if err := c.reconcile(key); err != nil {
// Put the item back on the workqueue to handle any transient errors.
c.workqueue.AddRateLimited(key)
return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
}
// Finally, if no error occurs we Forget this item so it does not
// get queued again until another change happens.
c.workqueue.Forget(obj)
klog.Infof("Successfully synced %s:%s", "key", key)
return nil
@@ -246,9 +253,6 @@ func (c *Controller) reconcile(key string) error {
return err
}
// name of your custom finalizer
finalizer := "finalizers.kubesphere.io/users"
if user.ObjectMeta.DeletionTimestamp.IsZero() {
// The object is not being deleted, so if it does not have our finalizer,
// then lets add the finalizer and update the object.
@@ -274,12 +278,17 @@ func (c *Controller) reconcile(key string) error {
return err
}
if err = c.deleteLoginRecords(user); err != nil {
klog.Error(err)
return err
}
// remove our finalizer from the list and update it.
user.Finalizers = sliceutil.RemoveString(user.ObjectMeta.Finalizers, func(item string) bool {
return item == finalizer
})
if _, err := c.ksClient.IamV1alpha2().Users().Update(user); err != nil {
if user, err = c.ksClient.IamV1alpha2().Users().Update(user); err != nil {
return err
}
}
@@ -298,6 +307,11 @@ func (c *Controller) reconcile(key string) error {
return err
}
if user, err = c.syncUserStatus(user); err != nil {
klog.Error(err)
return err
}
if c.kubeconfig != nil {
// ensure user kubeconfig configmap is created
if err = c.kubeconfig.CreateKubeConfig(user); err != nil {
@@ -319,11 +333,11 @@ func (c *Controller) reconcile(key string) error {
}
func (c *Controller) Start(stopCh <-chan struct{}) error {
return c.Run(4, stopCh)
return c.Run(5, stopCh)
}
func (c *Controller) ensurePasswordIsEncrypted(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
encrypted, _ := strconv.ParseBool(user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation])
encrypted := user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation] == "true"
// password is not encrypted
if !encrypted {
password, err := encrypt(user.Spec.EncryptedPassword)
@@ -337,7 +351,10 @@ func (c *Controller) ensurePasswordIsEncrypted(user *iamv1alpha2.User) (*iamv1al
user.Annotations = make(map[string]string, 0)
}
user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation] = "true"
user.Status.State = iamv1alpha2.UserActive
user.Status = iamv1alpha2.UserStatus{
State: iamv1alpha2.UserActive,
LastTransitionTime: &metav1.Time{Time: time.Now()},
}
return c.ksClient.IamV1alpha2().Users().Update(user)
}
@@ -382,15 +399,10 @@ func (c *Controller) multiClusterSync(user *iamv1alpha2.User) error {
}
if !reflect.DeepEqual(federatedUser.Spec.Template.Spec, user.Spec) ||
!reflect.DeepEqual(federatedUser.Spec.Template.Status, user.Status) ||
!reflect.DeepEqual(federatedUser.Labels, user.Labels) ||
!reflect.DeepEqual(federatedUser.Annotations, user.Annotations) {
!reflect.DeepEqual(federatedUser.Spec.Template.Status, user.Status) {
federatedUser.Labels = user.Labels
federatedUser.Spec.Template.Spec = user.Spec
federatedUser.Spec.Template.Status = user.Status
federatedUser.Spec.Template.Labels = user.Labels
federatedUser.Spec.Template.Annotations = user.Annotations
return c.updateFederatedUser(&federatedUser)
}
@@ -408,10 +420,6 @@ func (c *Controller) createFederatedUser(user *iamv1alpha2.User) error {
},
Spec: iamv1alpha2.FederatedUserSpec{
Template: iamv1alpha2.UserTemplate{
ObjectMeta: metav1.ObjectMeta{
Labels: user.Labels,
Annotations: user.Annotations,
},
Spec: user.Spec,
Status: user.Status,
},
@@ -531,6 +539,81 @@ func (c *Controller) deleteRoleBindings(user *iamv1alpha2.User) error {
return nil
}
func (c *Controller) deleteLoginRecords(user *iamv1alpha2.User) error {
listOptions := metav1.ListOptions{
LabelSelector: labels.SelectorFromSet(labels.Set{iamv1alpha2.UserReferenceLabel: user.Name}).String(),
}
deleteOptions := metav1.NewDeleteOptions(0)
if err := c.ksClient.IamV1alpha2().LoginRecords().
DeleteCollection(deleteOptions, listOptions); err != nil {
klog.Error(err)
return err
}
return nil
}
// syncUserStatus will reconcile user state based on user login records
func (c *Controller) syncUserStatus(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
// disabled user, nothing to do
if user == nil || (user.Status.State == iamv1alpha2.UserDisabled) {
return user, nil
}
// blocked user, check if need to unblock user
if user.Status.State == iamv1alpha2.UserAuthLimitExceeded {
if user.Status.LastTransitionTime != nil &&
user.Status.LastTransitionTime.Add(c.authenticationOptions.AuthenticateRateLimiterDuration).After(time.Now()) {
expected := user.DeepCopy()
// unblock user
if user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation] == "true" {
expected.Status = iamv1alpha2.UserStatus{
State: iamv1alpha2.UserActive,
LastTransitionTime: &metav1.Time{Time: time.Now()},
}
}
if !reflect.DeepEqual(expected.Status, user.Status) {
return c.ksClient.IamV1alpha2().Users().Update(expected)
}
}
}
// normal user, check user's login records see if we need to block
records, err := c.loginRecordLister.List(labels.SelectorFromSet(labels.Set{iamv1alpha2.UserReferenceLabel: user.Name}))
if err != nil {
klog.Error(err)
return nil, err
}
// count failed login attempts during last AuthenticateRateLimiterDuration
now := time.Now()
failedLoginAttempts := 0
for _, loginRecord := range records {
if loginRecord.Spec.Type == iamv1alpha2.LoginFailure &&
loginRecord.CreationTimestamp.Add(c.authenticationOptions.AuthenticateRateLimiterDuration).After(now) {
failedLoginAttempts++
}
}
// block user if failed login attempts exceeds maximum tries setting
if failedLoginAttempts >= c.authenticationOptions.AuthenticateRateLimiterMaxTries {
expect := user.DeepCopy()
expect.Status = iamv1alpha2.UserStatus{
State: iamv1alpha2.UserAuthLimitExceeded,
Reason: fmt.Sprintf("Failed login attempts exceed %d in last %s", failedLoginAttempts, c.authenticationOptions.AuthenticateRateLimiterDuration),
LastTransitionTime: &metav1.Time{Time: time.Now()},
}
// block user for AuthenticateRateLimiterDuration duration, after that put it back to the queue to unblock
c.workqueue.AddAfter(user.Name, c.authenticationOptions.AuthenticateRateLimiterDuration)
return c.ksClient.IamV1alpha2().Users().Update(expect)
}
return user, nil
}
func encrypt(password string) (string, error) {
// when user is already mapped to another identity, password is empty by default
// unable to log in directly until password reset

View File

@@ -27,6 +27,7 @@ import (
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
ksinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions"
ldapclient "kubesphere.io/kubesphere/pkg/simple/client/ldap"
@@ -94,11 +95,12 @@ func (f *fixture) newController() (*Controller, ksinformers.SharedInformerFactor
}
}
c := NewController(f.k8sclient, f.ksclient, nil,
c := NewUserController(f.k8sclient, f.ksclient, nil,
ksinformers.Iam().V1alpha2().Users(),
nil, nil,
ksinformers.Iam().V1alpha2().LoginRecords(),
k8sinformers.Core().V1().ConfigMaps(),
ldapClient, false)
ldapClient, options.NewAuthenticateOptions(), false)
c.userSynced = alwaysReady
c.recorder = &record.FakeRecorder{}
@@ -187,8 +189,11 @@ func checkAction(expected, actual core.Action, t *testing.T) {
e, _ := expected.(core.UpdateActionImpl)
expObject := e.GetObject()
object := a.GetObject()
if !reflect.DeepEqual(expObject, object) {
expUser := expObject.(*iamv1alpha2.User)
user := object.(*iamv1alpha2.User)
expUser.Status.LastTransitionTime = nil
user.Status.LastTransitionTime = nil
if !reflect.DeepEqual(expUser, user) {
t.Errorf("Action %s %s has wrong object\nDiff:\n %s",
a.GetVerb(), a.GetResource().Resource, diff.ObjectGoPrintSideBySide(expObject, object))
}

View File

@@ -45,7 +45,8 @@ func (a *EmailValidator) Handle(ctx context.Context, req admission.Request) admi
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}
if _, err := mail.ParseAddress(user.Spec.Email); err != nil {
if _, err := mail.ParseAddress(user.Spec.Email); user.Spec.Email != "" && err != nil {
return admission.Errored(http.StatusBadRequest, fmt.Errorf("invalid email address:%s", user.Spec.Email))
}

View File

@@ -119,7 +119,7 @@ func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {
defer c.workqueue.ShutDown()
// Start the informer factories to begin populating the informer caches
klog.Info("Starting GlobalRole controller")
klog.Info("Starting WorkspaceRole controller")
// Wait for the caches to be synced before starting workers
klog.Info("Waiting for informer caches to sync")

View File

@@ -138,7 +138,7 @@ func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {
defer c.workqueue.ShutDown()
// Start the informer factories to begin populating the informer caches
klog.Info("Starting GlobalRole controller")
klog.Info("Starting WorkspaceTemplate controller")
// Wait for the caches to be synced before starting workers
klog.Info("Waiting for informer caches to sync")

View File

@@ -5,6 +5,8 @@ import (
"github.com/emicklei/go-restful"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api"
@@ -530,8 +532,7 @@ func (h *iamHandler) ModifyPassword(request *restful.Request, response *restful.
operator, ok := apirequest.UserFrom(request.Request.Context())
// change password by self
if ok && operator.GetName() == username {
_, err := h.im.Authenticate(username, passwordReset.CurrentPassword)
if err != nil {
if err = h.im.PasswordVerify(username, passwordReset.CurrentPassword); err != nil {
if err == im.AuthFailedIncorrectPassword {
err = errors.NewBadRequest("incorrect old password")
klog.Warning(err)
@@ -1209,6 +1210,30 @@ func (h *iamHandler) updateGlobalRoleBinding(operator user.Info, user *iamv1alph
return nil
}
func (h *iamHandler) ListUserLoginRecords(request *restful.Request, response *restful.Response) {
username := request.PathParameter("user")
queryParam := query.ParseQueryParameter(request)
selector, _ := labels.Parse(queryParam.LabelSelector)
if selector == nil {
selector = labels.NewSelector()
}
requirement, err := labels.NewRequirement(iamv1alpha2.UserReferenceLabel, selection.Equals, []string{username})
if err != nil {
klog.Error(err)
handleError(request, response, err)
return
}
selector.Add(*requirement)
queryParam.LabelSelector = selector.String()
result, err := h.im.ListLoginRecords(queryParam)
if err != nil {
klog.Error(err)
handleError(request, response, err)
return
}
response.WriteEntity(result)
}
func handleError(request *restful.Request, response *restful.Response, err error) {
if errors.IsBadRequest(err) {
api.HandleBadRequest(response, request, err)

View File

@@ -81,6 +81,11 @@ func AddToContainer(container *restful.Container, im im.IdentityManagementInterf
Doc("List all users in global scope.").
Returns(http.StatusOK, api.StatusOK, api.ListResult{Items: []interface{}{iamv1alpha2.User{}}}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AccessManagementTag}))
ws.Route(ws.GET("/users/{user}/loginrecords").
To(handler.ListUserLoginRecords).
Doc("List user's login records.").
Returns(http.StatusOK, api.StatusOK, api.ListResult{Items: []interface{}{iamv1alpha2.LoginRecord{}}}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AccessManagementTag}))
// clustermembers
ws.Route(ws.POST("/clustermembers").

View File

@@ -21,7 +21,7 @@ import (
"github.com/emicklei/go-restful"
apierrors "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
authuser "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api"
"kubesphere.io/kubesphere/pkg/api/auth"
@@ -29,25 +29,26 @@ import (
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
"kubesphere.io/kubesphere/pkg/apiserver/request"
"kubesphere.io/kubesphere/pkg/models/iam/im"
"net/http"
)
type oauthHandler struct {
issuer token.Issuer
im im.IdentityManagementInterface
options *authoptions.AuthenticationOptions
type handler struct {
im im.IdentityManagementInterface
options *authoptions.AuthenticationOptions
tokenOperator im.TokenManagementInterface
authenticator im.PasswordAuthenticator
loginRecorder im.LoginRecorder
}
func newOAUTHHandler(im im.IdentityManagementInterface, issuer token.Issuer, options *authoptions.AuthenticationOptions) *oauthHandler {
return &oauthHandler{im: im, issuer: issuer, options: options}
func newHandler(im im.IdentityManagementInterface, tokenOperator im.TokenManagementInterface, authenticator im.PasswordAuthenticator, loginRecorder im.LoginRecorder, options *authoptions.AuthenticationOptions) *handler {
return &handler{im: im, tokenOperator: tokenOperator, authenticator: authenticator, loginRecorder: loginRecorder, options: options}
}
// Implement webhook authentication interface
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
func (h *oauthHandler) TokenReviewHandler(req *restful.Request, resp *restful.Response) {
func (h *handler) TokenReview(req *restful.Request, resp *restful.Response) {
var tokenReview auth.TokenReview
err := req.ReadEntity(&tokenReview)
@@ -64,7 +65,7 @@ func (h *oauthHandler) TokenReviewHandler(req *restful.Request, resp *restful.Re
return
}
user, err := h.issuer.Verify(tokenReview.Spec.Token)
authenticated, err := h.tokenOperator.Verify(tokenReview.Spec.Token)
if err != nil {
klog.Errorln(err)
@@ -76,15 +77,15 @@ func (h *oauthHandler) TokenReviewHandler(req *restful.Request, resp *restful.Re
Kind: auth.KindTokenReview,
Status: &auth.Status{
Authenticated: true,
User: map[string]interface{}{"username": user.GetName(), "uid": user.GetUID()},
User: map[string]interface{}{"username": authenticated.GetName(), "uid": authenticated.GetUID()},
},
}
resp.WriteEntity(success)
}
func (h *oauthHandler) AuthorizeHandler(req *restful.Request, resp *restful.Response) {
user, ok := request.UserFrom(req.Request.Context())
func (h *handler) Authorize(req *restful.Request, resp *restful.Response) {
authenticated, ok := request.UserFrom(req.Request.Context())
clientId := req.QueryParameter("client_id")
responseType := req.QueryParameter("response_type")
redirectURI := req.QueryParameter("redirect_uri")
@@ -97,7 +98,7 @@ func (h *oauthHandler) AuthorizeHandler(req *restful.Request, resp *restful.Resp
}
if responseType != "token" {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: response type %s is not supported", responseType))
err := apierrors.NewBadRequest(fmt.Sprintf("Response type %s is not supported", responseType))
resp.WriteError(http.StatusUnauthorized, err)
return
}
@@ -108,7 +109,7 @@ func (h *oauthHandler) AuthorizeHandler(req *restful.Request, resp *restful.Resp
return
}
token, err := h.issueTo(user.GetName())
token, err := h.tokenOperator.IssueTo(authenticated)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
resp.WriteError(http.StatusUnauthorized, err)
@@ -132,7 +133,7 @@ func (h *oauthHandler) AuthorizeHandler(req *restful.Request, resp *restful.Resp
http.Redirect(resp, req.Request, redirectURL, http.StatusFound)
}
func (h *oauthHandler) OAuthCallBackHandler(req *restful.Request, resp *restful.Response) {
func (h *handler) OAuthCallBack(req *restful.Request, resp *restful.Response) {
code := req.QueryParameter("code")
name := req.PathParameter("callback")
@@ -157,7 +158,7 @@ func (h *oauthHandler) OAuthCallBackHandler(req *restful.Request, resp *restful.
return
}
user, err := oauthIdentityProvider.IdentityExchange(code)
identity, err := oauthIdentityProvider.IdentityExchange(code)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
@@ -165,16 +166,18 @@ func (h *oauthHandler) OAuthCallBackHandler(req *restful.Request, resp *restful.
return
}
existed, err := h.im.DescribeUser(user.GetName())
authenticated, err := h.im.DescribeUser(identity.GetName())
if err != nil {
// create user if not exist
if apierrors.IsNotFound(err) && oauth.MappingMethodAuto == providerOptions.MappingMethod {
if (oauth.MappingMethodAuto == providerOptions.MappingMethod ||
oauth.MappingMethodMixed == providerOptions.MappingMethod) &&
apierrors.IsNotFound(err) {
create := &iamv1alpha2.User{
ObjectMeta: v1.ObjectMeta{Name: user.GetName(),
ObjectMeta: v1.ObjectMeta{Name: identity.GetName(),
Annotations: map[string]string{iamv1alpha2.IdentifyProviderLabel: providerOptions.Name}},
Spec: iamv1alpha2.UserSpec{Email: user.GetEmail()},
Spec: iamv1alpha2.UserSpec{Email: identity.GetEmail()},
}
if existed, err = h.im.CreateUser(create); err != nil {
if authenticated, err = h.im.CreateUser(create); err != nil {
klog.Error(err)
api.HandleInternalError(resp, req, err)
return
@@ -186,9 +189,9 @@ func (h *oauthHandler) OAuthCallBackHandler(req *restful.Request, resp *restful.
}
}
// oauth.MappingMethodLookup
if existed == nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("user %s cannot bound to this identify provider", user.GetName()))
if oauth.MappingMethodLookup == providerOptions.MappingMethod &&
authenticated == nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("user %s cannot bound to this identify provider", identity.GetName()))
klog.Error(err)
resp.WriteError(http.StatusUnauthorized, err)
return
@@ -196,64 +199,140 @@ func (h *oauthHandler) OAuthCallBackHandler(req *restful.Request, resp *restful.
// oauth.MappingMethodAuto
// Fails if a user with that user name is already mapped to another identity.
if providerOptions.MappingMethod == oauth.MappingMethodMixed || existed.Annotations[iamv1alpha2.IdentifyProviderLabel] != providerOptions.Name {
err := apierrors.NewUnauthorized(fmt.Sprintf("user %s is already bound to other identify provider", user.GetName()))
if providerOptions.MappingMethod == oauth.MappingMethodAuto && authenticated.Annotations[iamv1alpha2.IdentifyProviderLabel] != providerOptions.Name {
err := apierrors.NewUnauthorized(fmt.Sprintf("user %s is already bound to other identify provider", identity.GetName()))
klog.Error(err)
resp.WriteError(http.StatusUnauthorized, err)
return
}
result, err := h.issueTo(user.GetName())
result, err := h.tokenOperator.IssueTo(&user.DefaultInfo{
Name: authenticated.Name,
UID: string(authenticated.UID),
})
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
resp.WriteError(http.StatusUnauthorized, err)
return
}
if err = h.loginRecorder.RecordLogin(authenticated.Name, nil, req.Request); err != nil {
klog.Error(err)
err := apierrors.NewInternalError(err)
resp.WriteError(http.StatusInternalServerError, err)
return
}
resp.WriteEntity(result)
}
func (h *oauthHandler) Login(request *restful.Request, response *restful.Response) {
func (h *handler) Login(request *restful.Request, response *restful.Response) {
var loginRequest auth.LoginRequest
err := request.ReadEntity(&loginRequest)
if err != nil || loginRequest.Username == "" || loginRequest.Password == "" {
response.WriteHeaderAndEntity(http.StatusUnauthorized, fmt.Errorf("empty username or password"))
return
}
h.passwordGrant(loginRequest.Username, loginRequest.Password, request, response)
}
user, err := h.im.Authenticate(loginRequest.Username, loginRequest.Password)
func (h *handler) Token(req *restful.Request, response *restful.Response) {
grantType, err := req.BodyParameter("grant_type")
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
response.WriteError(http.StatusUnauthorized, err)
klog.Error(err)
api.HandleBadRequest(response, req, err)
return
}
switch grantType {
case "password":
username, err := req.BodyParameter("username")
if err != nil {
klog.Error(err)
api.HandleBadRequest(response, req, err)
return
}
password, err := req.BodyParameter("password")
if err != nil {
klog.Error(err)
api.HandleBadRequest(response, req, err)
return
}
h.passwordGrant(username, password, req, response)
break
case "refresh_token":
h.refreshTokenGrant(req, response)
break
default:
err := apierrors.NewBadRequest(fmt.Sprintf("Grant type %s is not supported", grantType))
response.WriteError(http.StatusBadRequest, err)
}
}
func (h *handler) passwordGrant(username string, password string, req *restful.Request, response *restful.Response) {
authenticated, err := h.authenticator.Authenticate(username, password)
if err != nil {
if err == im.AuthFailedIncorrectPassword {
if err := h.loginRecorder.RecordLogin(username, err, req.Request); err != nil {
klog.Error(err)
err := apierrors.NewInternalError(err)
response.WriteError(http.StatusInternalServerError, err)
return
}
}
if err == im.AuthFailedIncorrectPassword ||
err == im.AuthFailedIdentityMappingNotMatch ||
err == im.AuthRateLimitExceeded {
klog.V(4).Info(err)
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
response.WriteError(http.StatusUnauthorized, err)
return
}
klog.Error(err)
err := apierrors.NewInternalError(err)
response.WriteError(http.StatusInternalServerError, err)
return
}
result, err := h.issueTo(user.Name)
result, err := h.tokenOperator.IssueTo(authenticated)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
response.WriteError(http.StatusUnauthorized, err)
klog.Error(err)
err := apierrors.NewInternalError(err)
response.WriteError(http.StatusInternalServerError, err)
return
}
if err = h.loginRecorder.RecordLogin(authenticated.GetName(), nil, req.Request); err != nil {
klog.Error(err)
err := apierrors.NewInternalError(err)
response.WriteError(http.StatusInternalServerError, err)
return
}
response.WriteEntity(result)
}
func (h *oauthHandler) issueTo(username string) (*oauth.Token, error) {
expiresIn := h.options.OAuthOptions.AccessTokenMaxAge
accessToken, err := h.issuer.IssueTo(&authuser.DefaultInfo{
Name: username,
}, expiresIn)
func (h *handler) refreshTokenGrant(req *restful.Request, response *restful.Response) {
refreshToken, err := req.BodyParameter("refresh_token")
if err != nil {
klog.Error(err)
return nil, err
api.HandleBadRequest(response, req, err)
return
}
result := &oauth.Token{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: int(expiresIn.Seconds()),
authenticated, err := h.tokenOperator.Verify(refreshToken)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
response.WriteError(http.StatusUnauthorized, err)
return
}
return result, nil
result, err := h.tokenOperator.IssueTo(authenticated)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
response.WriteError(http.StatusUnauthorized, err)
return
}
response.WriteEntity(result)
}

View File

@@ -23,7 +23,6 @@ import (
"kubesphere.io/kubesphere/pkg/api/auth"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
"kubesphere.io/kubesphere/pkg/constants"
"kubesphere.io/kubesphere/pkg/models/iam/im"
"net/http"
@@ -35,13 +34,13 @@ import (
// Most authentication integrations place an authenticating proxy in front of this endpoint, or configure ks-apiserver
// to validate credentials against a backing identity provider.
// Requests to <ks-apiserver>/oauth/authorize can come from user-agents that cannot display interactive login pages, such as the CLI.
func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, issuer token.Issuer, options *authoptions.AuthenticationOptions) error {
func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, tokenOperator im.TokenManagementInterface, authenticator im.PasswordAuthenticator, loginRecorder im.LoginRecorder, options *authoptions.AuthenticationOptions) error {
ws := &restful.WebService{}
ws.Path("/oauth").
Consumes(restful.MIME_JSON).
Produces(restful.MIME_JSON)
handler := newOAUTHHandler(im, issuer, options)
handler := newHandler(im, tokenOperator, authenticator, loginRecorder, options)
// Implement webhook authentication interface
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
@@ -49,7 +48,7 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, iss
Doc("TokenReview attempts to authenticate a token to a known user. Note: TokenReview requests may be "+
"cached by the webhook token authenticator plugin in the kube-apiserver.").
Reads(auth.TokenReview{}).
To(handler.TokenReviewHandler).
To(handler.TokenReview).
Returns(http.StatusOK, api.StatusOK, auth.TokenReview{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.IdentityManagementTag}))
@@ -65,8 +64,15 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, iss
Param(ws.QueryParameter("redirect_uri", "After completing its interaction with the resource owner, "+
"the authorization server directs the resource owner's user-agent back to the client.The redirection endpoint "+
"URI MUST be an absolute URI as defined by [RFC3986] Section 4.3.").Required(false)).
To(handler.AuthorizeHandler))
//ws.Route(ws.POST("/token"))
To(handler.Authorize))
// Resource Owner Password Credentials Grant
// https://tools.ietf.org/html/rfc6749#section-4.3
ws.Route(ws.POST("/token").
Consumes("application/x-www-form-urlencoded").
Doc("The resource owner password credentials grant type is suitable in\n" +
"cases where the resource owner has a trust relationship with the\n" +
"client, such as the device operating system or a highly privileged application.").
To(handler.Token))
// Authorization callback URL, where the end of the URL contains the identity provider name.
// The provider name is also used to build the callback URL.
@@ -85,7 +91,7 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, iss
"otherwise, REQUIRED. The scope of the access token as described by [RFC6479] Section 3.3.").Required(false)).
Param(ws.QueryParameter("state", "if the \"state\" parameter was present in the client authorization request."+
"The exact value received from the client.").Required(true)).
To(handler.OAuthCallBackHandler).
To(handler.OAuthCallBack).
Returns(http.StatusOK, api.StatusOK, oauth.Token{}))
c.Add(ws)

View File

@@ -0,0 +1,173 @@
/*
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"
"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"
"net/mail"
)
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 &authuser.DefaultInfo{Name: user.Name}, 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),
}, 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,6 @@ limitations under the License.
package im
import (
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog"
@@ -24,11 +23,9 @@ import (
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/apiserver/query"
kubesphereclient "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
"kubesphere.io/kubesphere/pkg/informers"
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
"net/mail"
"time"
)
type IdentityManagementInterface interface {
@@ -37,34 +34,24 @@ type IdentityManagementInterface interface {
DeleteUser(username string) error
UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error)
DescribeUser(username string) (*iamv1alpha2.User, error)
Authenticate(username, password string) (*iamv1alpha2.User, error)
ModifyPassword(username string, password string) error
ListLoginRecords(query *query.Query) (*api.ListResult, error)
PasswordVerify(username string, password string) error
}
var (
AuthRateLimitExceeded = errors.New("user auth rate limit exceeded")
AuthFailedIncorrectPassword = errors.New("incorrect password")
UserAlreadyExists = errors.New("user already exists")
UserNotExists = errors.New("user not exists")
)
func NewOperator(ksClient kubesphereclient.Interface, factory informers.InformerFactory, options *authoptions.AuthenticationOptions) IdentityManagementInterface {
func NewOperator(ksClient kubesphere.Interface, factory informers.InformerFactory, options *authoptions.AuthenticationOptions) IdentityManagementInterface {
im := &defaultIMOperator{
ksClient: ksClient,
resourceGetter: resourcev1alpha3.NewResourceGetter(factory),
}
if options != nil {
im.authenticateRateLimiterDuration = options.AuthenticateRateLimiterDuration
im.authenticateRateLimiterMaxTries = options.AuthenticateRateLimiterMaxTries
options: options,
}
return im
}
type defaultIMOperator struct {
ksClient kubesphereclient.Interface
resourceGetter *resourcev1alpha3.ResourceGetter
authenticateRateLimiterMaxTries int
authenticateRateLimiterDuration time.Duration
ksClient kubesphere.Interface
resourceGetter *resourcev1alpha3.ResourceGetter
options *authoptions.AuthenticationOptions
}
func (im *defaultIMOperator) UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
@@ -110,50 +97,6 @@ func (im *defaultIMOperator) ModifyPassword(username string, password string) er
return nil
}
func (im *defaultIMOperator) Authenticate(username, password string) (*iamv1alpha2.User, error) {
var user *iamv1alpha2.User
if _, err := mail.ParseAddress(username); err != nil {
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", username)
if err != nil {
klog.Error(err)
return nil, err
}
user = obj.(*iamv1alpha2.User)
} else {
objs, err := im.resourceGetter.List(iamv1alpha2.ResourcesPluralUser, "", &query.Query{
Pagination: query.NoPagination,
Filters: map[query.Field]query.Value{iamv1alpha2.FieldEmail: query.Value(username)},
})
if err != nil {
klog.Error(err)
return nil, err
}
if len(objs.Items) != 1 {
if len(objs.Items) == 0 {
klog.Warningf("username or email: %s not exist", username)
} else {
klog.Errorf("duplicate user entries: %+v", objs)
}
return nil, AuthFailedIncorrectPassword
}
user = objs.Items[0].(*iamv1alpha2.User)
}
if im.authRateLimitExceeded(user) {
im.authFailRecord(user, AuthRateLimitExceeded)
return nil, AuthRateLimitExceeded
}
if checkPasswordHash(password, user.Spec.EncryptedPassword) {
return user, nil
}
im.authFailRecord(user, AuthFailedIncorrectPassword)
return nil, AuthFailedIncorrectPassword
}
func (im *defaultIMOperator) ListUsers(query *query.Query) (result *api.ListResult, err error) {
result, err = im.resourceGetter.List(iamv1alpha2.ResourcesPluralUser, "", query)
if err != nil {
@@ -171,6 +114,19 @@ 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)
if err != nil {
klog.Error(err)
return err
}
user := obj.(*iamv1alpha2.User)
if checkPasswordHash(password, user.Spec.EncryptedPassword) {
return nil
}
return AuthFailedIncorrectPassword
}
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
@@ -200,27 +156,13 @@ func (im *defaultIMOperator) CreateUser(user *iamv1alpha2.User) (*iamv1alpha2.Us
return user, nil
}
func (im *defaultIMOperator) authRateLimitEnabled() bool {
if im.authenticateRateLimiterMaxTries <= 0 || im.authenticateRateLimiterDuration == 0 {
return false
func (im *defaultIMOperator) ListLoginRecords(query *query.Query) (*api.ListResult, error) {
result, err := im.resourceGetter.List(iamv1alpha2.ResourcesPluralLoginRecord, "", query)
if err != nil {
klog.Error(err)
return nil, err
}
return true
}
func (im *defaultIMOperator) authRateLimitExceeded(user *iamv1alpha2.User) bool {
if !im.authRateLimitEnabled() {
return false
}
// TODO record login history using CRD
return false
}
func (im *defaultIMOperator) authFailRecord(user *iamv1alpha2.User, err error) {
if !im.authRateLimitEnabled() {
return
}
// TODO record login history using CRD
return result, nil
}
func ensurePasswordNotOutput(user *iamv1alpha2.User) *iamv1alpha2.User {

View File

@@ -0,0 +1,72 @@
/*
*
* 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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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"
)
type LoginRecorder interface {
RecordLogin(username string, authErr error, req *http.Request) error
}
type loginRecorder struct {
ksClient kubesphere.Interface
}
func NewLoginRecorder(ksClient kubesphere.Interface) LoginRecorder {
return &loginRecorder{
ksClient: ksClient,
}
}
func (l *loginRecorder) RecordLogin(username string, authErr error, req *http.Request) error {
loginEntry := &iamv1alpha2.LoginRecord{
ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("%s-", username),
Labels: map[string]string{
iamv1alpha2.UserReferenceLabel: username,
},
},
Spec: iamv1alpha2.LoginRecordSpec{
SourceIP: net.GetRequestIP(req),
Type: iamv1alpha2.LoginSuccess,
Reason: iamv1alpha2.AuthenticatedSuccessfully,
},
}
if authErr != nil {
loginEntry.Spec.Type = iamv1alpha2.LoginFailure
loginEntry.Spec.Reason = authErr.Error()
}
_, err := l.ksClient.IamV1alpha2().LoginRecords().Create(loginEntry)
if err != nil {
klog.Error(err)
return err
}
return nil
}

146
pkg/models/iam/im/token.go Normal file
View File

@@ -0,0 +1,146 @@
/*
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"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"time"
)
type TokenManagementInterface interface {
// Verify verifies a token, and return a User if it's a valid token, otherwise return error
Verify(token string) (user.Info, error)
// IssueTo issues a token a User, return error if issuing process failed
IssueTo(user user.Info) (*oauth.Token, error)
}
type tokenOperator struct {
issuer token.Issuer
options *authoptions.AuthenticationOptions
cache cache.Interface
}
func NewTokenOperator(cache cache.Interface, options *authoptions.AuthenticationOptions) TokenManagementInterface {
operator := &tokenOperator{
issuer: token.NewTokenIssuer(options.JwtSecret, options.MaximumClockSkew),
options: options,
cache: cache,
}
return operator
}
func (t tokenOperator) Verify(tokenStr string) (user.Info, error) {
authenticated, tokenType, err := t.issuer.Verify(tokenStr)
if err != nil {
klog.Error(err)
return nil, err
}
if t.options.OAuthOptions.AccessTokenMaxAge == 0 ||
tokenType == token.StaticToken {
return authenticated, nil
}
if err := t.tokenCacheValidate(authenticated.GetName(), tokenStr); err != nil {
klog.Error(err)
return nil, err
}
return authenticated, nil
}
func (t tokenOperator) IssueTo(user user.Info) (*oauth.Token, error) {
accessTokenExpiresIn := t.options.OAuthOptions.AccessTokenMaxAge
refreshTokenExpiresIn := accessTokenExpiresIn + t.options.OAuthOptions.AccessTokenInactivityTimeout
accessToken, err := t.issuer.IssueTo(user, token.AccessToken, accessTokenExpiresIn)
if err != nil {
klog.Error(err)
return nil, err
}
refreshToken, err := t.issuer.IssueTo(user, token.RefreshToken, refreshTokenExpiresIn)
if err != nil {
klog.Error(err)
return nil, err
}
result := &oauth.Token{
AccessToken: accessToken,
TokenType: "Bearer",
RefreshToken: refreshToken,
ExpiresIn: int(accessTokenExpiresIn.Seconds()),
}
if !t.options.MultipleLogin {
if err = t.revokeAllUserTokens(user.GetName()); err != nil {
klog.Error(err)
return nil, err
}
}
if accessTokenExpiresIn > 0 {
if err = t.cacheToken(user.GetName(), accessToken, accessTokenExpiresIn); err != nil {
klog.Error(err)
return nil, err
}
if err = t.cacheToken(user.GetName(), refreshToken, refreshTokenExpiresIn); err != nil {
klog.Error(err)
return nil, err
}
}
return result, nil
}
func (t tokenOperator) revokeAllUserTokens(username string) error {
pattern := fmt.Sprintf("kubesphere:user:%s:token:*", username)
if keys, err := t.cache.Keys(pattern); err != nil {
klog.Error(err)
return err
} else if len(keys) > 0 {
if err := t.cache.Del(keys...); err != nil {
klog.Error(err)
return err
}
}
return nil
}
func (t tokenOperator) tokenCacheValidate(username, token string) error {
key := fmt.Sprintf("kubesphere:user:%s:token:%s", username, token)
if exist, err := t.cache.Exists(key); err != nil {
return err
} else if !exist {
return fmt.Errorf("token not found in cache")
}
return nil
}
func (t tokenOperator) cacheToken(username, token string, duration time.Duration) error {
key := fmt.Sprintf("kubesphere:user:%s:token:%s", username, token)
if err := t.cache.Set(key, token, duration); err != nil {
klog.Error(err)
return err
}
return nil
}

View File

@@ -0,0 +1,87 @@
/*
Copyright 2019 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 loginrecord
import (
"k8s.io/apimachinery/pkg/runtime"
"kubesphere.io/kubesphere/pkg/api"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/query"
ksinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3"
)
const recordType = "type"
type loginrecordsGetter struct {
ksInformer ksinformers.SharedInformerFactory
}
func New(ksinformer ksinformers.SharedInformerFactory) v1alpha3.Interface {
return &loginrecordsGetter{ksInformer: ksinformer}
}
func (d *loginrecordsGetter) Get(_, name string) (runtime.Object, error) {
return d.ksInformer.Iam().V1alpha2().Users().Lister().Get(name)
}
func (d *loginrecordsGetter) List(_ string, query *query.Query) (*api.ListResult, error) {
records, err := d.ksInformer.Iam().V1alpha2().LoginRecords().Lister().List(query.Selector())
if err != nil {
return nil, err
}
var result []runtime.Object
for _, user := range records {
result = append(result, user)
}
return v1alpha3.DefaultList(result, query, d.compare, d.filter), nil
}
func (d *loginrecordsGetter) compare(left runtime.Object, right runtime.Object, field query.Field) bool {
leftUser, ok := left.(*iamv1alpha2.LoginRecord)
if !ok {
return false
}
rightUser, ok := right.(*iamv1alpha2.LoginRecord)
if !ok {
return false
}
return v1alpha3.DefaultObjectMetaCompare(leftUser.ObjectMeta, rightUser.ObjectMeta, field)
}
func (d *loginrecordsGetter) filter(object runtime.Object, filter query.Filter) bool {
record, ok := object.(*iamv1alpha2.LoginRecord)
if !ok {
return false
}
switch filter.Field {
case recordType:
return string(record.Spec.Type) == string(filter.Value)
default:
return v1alpha3.DefaultObjectMetaFilter(record.ObjectMeta, filter)
}
}

View File

@@ -0,0 +1,110 @@
/*
Copyright 2019 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 loginrecord
import (
"github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"kubesphere.io/kubesphere/pkg/api"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/query"
"kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3"
"testing"
)
func TestListLoginRecords(t *testing.T) {
tests := []struct {
description string
namespace string
query *query.Query
expected *api.ListResult
expectedErr error
}{
{
"test name filter",
"bar",
&query.Query{
Pagination: &query.Pagination{
Limit: 1,
Offset: 0,
},
SortBy: query.FieldName,
Ascending: false,
Filters: map[query.Field]query.Value{query.FieldName: query.Value("foo2")},
},
&api.ListResult{
Items: []interface{}{
foo2,
},
TotalItems: 1,
},
nil,
},
}
getter := prepare()
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
got, err := getter.List(test.namespace, test.query)
if test.expectedErr != nil && err != test.expectedErr {
t.Errorf("expected error, got nothing")
} else if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(got, test.expected); diff != "" {
t.Errorf("%T differ (-got, +want): %s", test.expected, diff)
}
})
}
}
var (
foo1 = &iamv1alpha2.LoginRecord{
ObjectMeta: metav1.ObjectMeta{
Name: "foo1",
},
}
foo2 = &iamv1alpha2.LoginRecord{
ObjectMeta: metav1.ObjectMeta{
Name: "foo2",
},
}
bar1 = &iamv1alpha2.LoginRecord{
ObjectMeta: metav1.ObjectMeta{
Name: "bar1",
},
}
records = []interface{}{foo1, foo2, bar1}
)
func prepare() v1alpha3.Interface {
client := fake.NewSimpleClientset()
informer := informers.NewSharedInformerFactory(client, 0)
for _, record := range records {
informer.Iam().V1alpha2().LoginRecords().Informer().GetIndexer().Add(record)
}
return New(informer)
}

View File

@@ -44,6 +44,7 @@ import (
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/globalrole"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/globalrolebinding"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/ingress"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/loginrecord"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/namespace"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/networkpolicy"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/node"
@@ -89,6 +90,7 @@ func NewResourceGetter(factory informers.InformerFactory) *ResourceGetter {
getters[iamv1alpha2.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralUser)] = user.New(factory.KubeSphereSharedInformerFactory(), factory.KubernetesSharedInformerFactory())
getters[iamv1alpha2.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralGlobalRoleBinding)] = globalrolebinding.New(factory.KubeSphereSharedInformerFactory())
getters[iamv1alpha2.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralWorkspaceRoleBinding)] = workspacerolebinding.New(factory.KubeSphereSharedInformerFactory())
getters[iamv1alpha2.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralLoginRecord)] = loginrecord.New(factory.KubeSphereSharedInformerFactory())
getters[rbacv1.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralRole)] = role.New(factory.KubernetesSharedInformerFactory())
getters[rbacv1.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralClusterRole)] = clusterrole.New(factory.KubernetesSharedInformerFactory())
getters[rbacv1.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralRoleBinding)] = rolebinding.New(factory.KubernetesSharedInformerFactory())
@@ -97,7 +99,6 @@ func NewResourceGetter(factory informers.InformerFactory) *ResourceGetter {
getters[snapshotv1beta1.SchemeGroupVersion.WithResource("volumesnapshots")] = volumesnapshot.New(factory.SnapshotSharedInformerFactory())
getters[schema.GroupVersionResource{Group: "cluster.kubesphere.io", Version: "v1alpha1", Resource: "clusters"}] = cluster.New(factory.KubeSphereSharedInformerFactory())
getters[schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}] = customresourcedefinition.New(factory.ApiExtensionSharedInformerFactory())
getters[typesv1beta1.SchemeGroupVersion.WithResource(typesv1beta1.ResourcesPluralFedNamespace)] = federatednamespace.New(factory.KubeSphereSharedInformerFactory())
return &ResourceGetter{

View File

@@ -76,13 +76,8 @@ func (s *simpleCache) Set(key string, value string, duration time.Duration) erro
func (s *simpleCache) Del(keys ...string) error {
for _, key := range keys {
if _, ok := s.store[key]; ok {
delete(s.store, key)
} else {
return ErrNoSuchKey
}
delete(s.store, key)
}
return nil
}
@@ -99,7 +94,7 @@ func (s *simpleCache) Get(key string) (string, error) {
func (s *simpleCache) Exists(keys ...string) (bool, error) {
for _, key := range keys {
if _, ok := s.store[key]; !ok {
return false, ErrNoSuchKey
return false, nil
}
}

View File

@@ -16,7 +16,32 @@ limitations under the License.
package net
import (
"net"
"net/http"
"strings"
)
// 0 is considered as a non valid port
func IsValidPort(port int) bool {
return port > 0 && port < 65535
}
func GetRequestIP(req *http.Request) string {
address := strings.Trim(req.Header.Get("X-Real-Ip"), " ")
if address != "" {
return address
}
address = strings.Trim(req.Header.Get("X-Forwarded-For"), " ")
if address != "" {
return address
}
address, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return req.RemoteAddr
}
return address
}