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:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
72
config/crds/iam.kubesphere.io_loginrecords.yaml
generated
Normal file
72
config/crds/iam.kubesphere.io_loginrecords.yaml
generated
Normal 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: []
|
||||
28
config/crds/iam.kubesphere.io_users.yaml
generated
28
config/crds/iam.kubesphere.io_users.yaml
generated
@@ -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
|
||||
|
||||
@@ -51,6 +51,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&User{},
|
||||
&UserList{},
|
||||
&LoginRecord{},
|
||||
&LoginRecordList{},
|
||||
&GlobalRole{},
|
||||
&GlobalRoleList{},
|
||||
&GlobalRoleBinding{},
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
98
pkg/apis/iam/v1alpha2/zz_generated.deepcopy.go
generated
98
pkg/apis/iam/v1alpha2/zz_generated.deepcopy.go
generated
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ type GithubIdentity struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
identityprovider.RegisterOAuthProviderCodec(&Github{})
|
||||
identityprovider.RegisterOAuthProvider(&Github{})
|
||||
}
|
||||
|
||||
func (g *Github) Type() string {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
117
pkg/apiserver/authentication/identityprovider/ldap_provider.go
Normal file
117
pkg/apiserver/authentication/identityprovider/ldap_provider.go
Normal 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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -22,6 +22,8 @@ type GlobalRoleExpansion interface{}
|
||||
|
||||
type GlobalRoleBindingExpansion interface{}
|
||||
|
||||
type LoginRecordExpansion interface{}
|
||||
|
||||
type RoleBaseExpansion interface{}
|
||||
|
||||
type UserExpansion interface{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
164
pkg/client/clientset/versioned/typed/iam/v1alpha2/loginrecord.go
Normal file
164
pkg/client/clientset/versioned/typed/iam/v1alpha2/loginrecord.go
Normal 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
|
||||
}
|
||||
@@ -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"):
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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{}
|
||||
|
||||
65
pkg/client/listers/iam/v1alpha2/loginrecord.go
Normal file
65
pkg/client/listers/iam/v1alpha2/loginrecord.go
Normal 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
|
||||
}
|
||||
230
pkg/controller/user/loginrecord_controller.go
Normal file
230
pkg/controller/user/loginrecord_controller.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
173
pkg/models/iam/im/authenticator.go
Normal file
173
pkg/models/iam/im/authenticator.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
72
pkg/models/iam/im/login_recoder.go
Normal file
72
pkg/models/iam/im/login_recoder.go
Normal 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
146
pkg/models/iam/im/token.go
Normal 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
|
||||
}
|
||||
87
pkg/models/resources/v1alpha3/loginrecord/loginrecords.go
Normal file
87
pkg/models/resources/v1alpha3/loginrecord/loginrecords.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
110
pkg/models/resources/v1alpha3/loginrecord/loginrecords_test.go
Normal file
110
pkg/models/resources/v1alpha3/loginrecord/loginrecords_test.go
Normal 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)
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
9
pkg/simple/client/cache/simple_cache.go
vendored
9
pkg/simple/client/cache/simple_cache.go
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user