提交 77a3722b 编写于 作者: H hongming

fix: password modify

Signed-off-by: Nhongming <talonwan@yunify.com>
上级 52abbeb3
......@@ -38,6 +38,11 @@ type TokenReview struct {
Status *Status `json:"status,omitempty" description:"token review status"`
}
type LoginRequest struct {
Username string `json:"username" description:"username"`
Password string `json:"password" description:"password"`
}
func (request *TokenReview) Validate() error {
if request.Spec == nil || request.Spec.Token == "" {
return fmt.Errorf("token must not be null")
......
......@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package im
package iam
import "kubesphere.io/kubesphere/pkg/simple/client/ldap"
func NewFakeOperator() IdentityManagementInterface {
return NewLDAPOperator(ldap.NewSimpleLdap())
type PasswordReset struct {
CurrentPassword string `json:"currentPassword"`
Password string `json:"password"`
}
......@@ -183,12 +183,11 @@ func (s *APIServer) installKubeSphereAPIs() {
s.Config.MultiClusterOptions.ProxyPublishService,
s.Config.MultiClusterOptions.ProxyPublishAddress,
s.Config.MultiClusterOptions.AgentImage))
urlruntime.Must(iamapi.AddToContainer(s.container,
im.NewOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory),
imOperator := im.NewOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory, s.Config.AuthenticationOptions)
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,
im.NewOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory),
urlruntime.Must(oauth.AddToContainer(s.container, imOperator,
token.NewJwtTokenIssuer(token.DefaultIssuerName, s.Config.AuthenticationOptions, s.CacheClient),
s.Config.AuthenticationOptions))
urlruntime.Must(servicemeshv1alpha2.AddToContainer(s.container))
......@@ -275,7 +274,7 @@ func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) {
// authenticators are unordered
authn := unionauth.New(anonymous.NewAuthenticator(),
basictoken.New(basic.NewBasicAuthenticator(im.NewOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory))),
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)
handler = filters.WithRequestInfo(handler, requestInfoResolver)
......
......@@ -28,16 +28,11 @@ type AuthenticationOptions struct {
// authenticate rate limit will
AuthenticateRateLimiterMaxTries int `json:"authenticateRateLimiterMaxTries" yaml:"authenticateRateLimiterMaxTries"`
AuthenticateRateLimiterDuration time.Duration `json:"authenticationRateLimiterDuration" yaml:"authenticationRateLimiterDuration"`
// maximum retries when authenticate failed
MaxAuthenticateRetries int `json:"maxAuthenticateRetries" yaml:"maxAuthenticateRetries"`
// allow multiple users login at the same time
MultipleLogin bool `json:"multipleLogin" yaml:"multipleLogin"`
// secret to signed jwt token
JwtSecret string `json:"-" yaml:"jwtSecret"`
// oauth options
OAuthOptions *oauth.Options `json:"oauthOptions" yaml:"oauthOptions"`
}
......@@ -45,7 +40,6 @@ func NewAuthenticateOptions() *AuthenticationOptions {
return &AuthenticationOptions{
AuthenticateRateLimiterMaxTries: 5,
AuthenticateRateLimiterDuration: time.Minute * 30,
MaxAuthenticateRetries: 0,
OAuthOptions: oauth.NewOptions(),
MultipleLogin: false,
JwtSecret: "",
......@@ -64,7 +58,6 @@ func (options *AuthenticationOptions) Validate() []error {
func (options *AuthenticationOptions) AddFlags(fs *pflag.FlagSet, s *AuthenticationOptions) {
fs.IntVar(&options.AuthenticateRateLimiterMaxTries, "authenticate-rate-limiter-max-retries", s.AuthenticateRateLimiterMaxTries, "")
fs.DurationVar(&options.AuthenticateRateLimiterDuration, "authenticate-rate-limiter-duration", s.AuthenticateRateLimiterDuration, "")
fs.IntVar(&options.MaxAuthenticateRetries, "authenticate-max-retries", s.MaxAuthenticateRetries, "")
fs.BoolVar(&options.MultipleLogin, "multiple-login", s.MultipleLogin, "Allow multiple login with the same account, disable means only one user can login at the same time.")
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.")
......
......@@ -54,16 +54,14 @@ func (s *jwtTokenIssuer) Verify(tokenString string) (User, error) {
}
clm := &Claims{}
_, err := jwt.ParseWithClaims(tokenString, clm, s.keyFunc)
if err != nil {
return nil, err
}
// 0 means no expiration.
// validate token cache
if s.options.OAuthOptions.AccessTokenMaxAge > 0 {
// 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 {
......
......@@ -121,7 +121,6 @@ func newTestConfig() (*Config, error) {
AuthenticationOptions: &authoptions.AuthenticationOptions{
AuthenticateRateLimiterMaxTries: 5,
AuthenticateRateLimiterDuration: 30 * time.Minute,
MaxAuthenticateRetries: 6,
JwtSecret: "xxxxxx",
MultipleLogin: false,
OAuthOptions: &oauth.Options{
......
......@@ -18,6 +18,7 @@ package filters
import (
"errors"
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
......@@ -54,7 +55,7 @@ func WithAuthentication(handler http.Handler, auth authenticator.Request) http.H
}
gv := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
responsewriters.ErrorNegotiated(apierrors.NewUnauthorized("Unauthorized"), s, gv, w, req)
responsewriters.ErrorNegotiated(apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized:%s", err)), s, gv, w, req)
return
}
......
......@@ -205,7 +205,8 @@ func (r *ReconcileNamespace) bindWorkspace(namespace *corev1.Namespace) error {
}
func removeWorkspaceOwnerReferences(ownerReferences []metav1.OwnerReference) []metav1.OwnerReference {
for i, owner := range ownerReferences {
for i := 0; i < len(ownerReferences); i++ {
owner := ownerReferences[i]
if owner.Kind == tenantv1alpha1.ResourceKindWorkspace {
ownerReferences = append(ownerReferences[:i], ownerReferences[i+1:]...)
i--
......
此差异已折叠。
......@@ -22,6 +22,7 @@ import (
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"kubesphere.io/kubesphere/pkg/api"
"kubesphere.io/kubesphere/pkg/api/iam"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
"kubesphere.io/kubesphere/pkg/apiserver/runtime"
......@@ -62,6 +63,13 @@ func AddToContainer(container *restful.Container, im im.IdentityManagementInterf
Param(ws.PathParameter("user", "username")).
Returns(http.StatusOK, api.StatusOK, iamv1alpha2.User{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AccessManagementTag}))
ws.Route(ws.PUT("/users/{user}/password").
To(handler.ModifyPassword).
Doc("Modify user's password.").
Reads(iam.PasswordReset{}).
Param(ws.PathParameter("user", "username")).
Returns(http.StatusOK, api.StatusOK, errors.None).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AccessManagementTag}))
ws.Route(ws.GET("/users/{user}").
To(handler.DescribeUser).
Doc("Retrieve user details.").
......
......@@ -90,7 +90,6 @@ func (h *oauthHandler) AuthorizeHandler(req *restful.Request, resp *restful.Resp
redirectURI := req.QueryParameter("redirect_uri")
conf, err := h.options.OAuthOptions.OAuthClient(clientId)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
resp.WriteError(http.StatusUnauthorized, err)
......@@ -109,14 +108,7 @@ func (h *oauthHandler) AuthorizeHandler(req *restful.Request, resp *restful.Resp
return
}
expiresIn := h.options.OAuthOptions.AccessTokenMaxAge
if conf.AccessTokenMaxAge != nil {
expiresIn = *conf.AccessTokenMaxAge
}
accessToken, err := h.issuer.IssueTo(user, expiresIn)
token, err := h.issueTo(user.GetName())
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
resp.WriteError(http.StatusUnauthorized, err)
......@@ -131,12 +123,11 @@ func (h *oauthHandler) AuthorizeHandler(req *restful.Request, resp *restful.Resp
return
}
redirectURL = fmt.Sprintf("%s#access_token=%s&token_type=Bearer", redirectURL, accessToken)
redirectURL = fmt.Sprintf("%s#access_token=%s&token_type=Bearer", redirectURL, token.AccessToken)
if expiresIn > 0 {
redirectURL = fmt.Sprintf("%s&expires_in=%v", redirectURL, expiresIn.Seconds())
if token.ExpiresIn > 0 {
redirectURL = fmt.Sprintf("%s&expires_in=%v", redirectURL, token.ExpiresIn)
}
resp.Header().Set("Content-Type", "text/plain")
http.Redirect(resp, req.Request, redirectURL, http.StatusFound)
}
......@@ -212,23 +203,57 @@ func (h *oauthHandler) OAuthCallBackHandler(req *restful.Request, resp *restful.
return
}
result, err := h.issueTo(user.GetName())
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
resp.WriteError(http.StatusUnauthorized, err)
return
}
resp.WriteEntity(result)
}
func (h *oauthHandler) 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
}
user, err := h.im.Authenticate(loginRequest.Username, loginRequest.Password)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
response.WriteError(http.StatusUnauthorized, err)
return
}
result, err := h.issueTo(user.Name)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
response.WriteError(http.StatusUnauthorized, 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: user.GetName(),
Name: username,
}, expiresIn)
if err != nil {
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
resp.WriteError(http.StatusUnauthorized, err)
return
klog.Error(err)
return nil, err
}
result := oauth.Token{
result := &oauth.Token{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: int(expiresIn.Seconds()),
}
resp.WriteEntity(result)
return result, nil
}
......@@ -90,5 +90,20 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, iss
c.Add(ws)
// legacy auth API
legacy := &restful.WebService{}
legacy.Path("/kapis/iam.kubesphere.io/v1alpha2/login").
Consumes(restful.MIME_JSON).
Produces(restful.MIME_JSON)
legacy.Route(legacy.POST("").
To(handler.Login).
Deprecate().
Doc("KubeSphere APIs support token-based authentication via the Authtoken request header. The POST Login API is used to retrieve the authentication token. After the authentication token is obtained, it must be inserted into the Authtoken header for all requests.").
Reads(auth.LoginRequest{}).
Returns(http.StatusOK, api.StatusOK, oauth.Token{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.IdentityManagementTag}))
c.Add(legacy)
return nil
}
......@@ -22,11 +22,13 @@ import (
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api"
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.io/kubesphere/pkg/informers"
resourcev1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource"
"net/mail"
"time"
)
type IdentityManagementInterface interface {
......@@ -36,6 +38,7 @@ type IdentityManagementInterface interface {
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
}
var (
......@@ -45,34 +48,66 @@ var (
UserNotExists = errors.New("user not exists")
)
func NewOperator(ksClient kubesphereclient.Interface, factory informers.InformerFactory) IdentityManagementInterface {
return &defaultIMOperator{
func NewOperator(ksClient kubesphereclient.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
}
return im
}
type defaultIMOperator struct {
ksClient kubesphereclient.Interface
resourceGetter *resourcev1alpha3.ResourceGetter
ksClient kubesphereclient.Interface
resourceGetter *resourcev1alpha3.ResourceGetter
authenticateRateLimiterMaxTries int
authenticateRateLimiterDuration time.Duration
}
func (im *defaultIMOperator) UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", user.Name)
if err != nil {
klog.Error(err)
return nil, err
}
old := obj.(*iamv1alpha2.User).DeepCopy()
if user.Annotations == nil {
user.Annotations = make(map[string]string, 0)
}
user.Annotations[iamv1alpha2.PasswordEncryptedAnnotation] = old.Annotations[iamv1alpha2.PasswordEncryptedAnnotation]
user.Spec.EncryptedPassword = old.Spec.EncryptedPassword
user.Status = old.Status
return im.ksClient.IamV1alpha2().Users().Update(user)
updated, err := im.ksClient.IamV1alpha2().Users().Update(user)
if err != nil {
klog.Error(err)
return nil, err
}
return ensurePasswordNotOutput(updated), nil
}
func (im *defaultIMOperator) ModifyPassword(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).DeepCopy()
delete(user.Annotations, iamv1alpha2.PasswordEncryptedAnnotation)
user.Spec.EncryptedPassword = password
_, err = im.ksClient.IamV1alpha2().Users().Update(user)
if err != nil {
klog.Error(err)
return err
}
return nil
}
func (im *defaultIMOperator) Authenticate(username, password string) (*iamv1alpha2.User, error) {
......@@ -80,26 +115,21 @@ func (im *defaultIMOperator) Authenticate(username, password string) (*iamv1alph
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)
......@@ -108,34 +138,36 @@ func (im *defaultIMOperator) Authenticate(username, password string) (*iamv1alph
}
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 {
klog.Error(err)
return nil, err
}
items := make([]interface{}, 0)
for _, item := range result.Items {
user := item.(*iamv1alpha2.User)
items = append(items, ensurePasswordNotOutput(user))
out := ensurePasswordNotOutput(user)
items = append(items, out)
}
result.Items = items
return result, nil
}
......@@ -146,14 +178,12 @@ func checkPasswordHash(password, hash string) bool {
func (im *defaultIMOperator) DescribeUser(username string) (*iamv1alpha2.User, error) {
obj, err := im.resourceGetter.Get(iamv1alpha2.ResourcesPluralUser, "", username)
if err != nil {
klog.Error(err)
return nil, err
}
user := obj.(*iamv1alpha2.User)
return ensurePasswordNotOutput(user), nil
}
......@@ -170,6 +200,29 @@ 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
}
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
}
func ensurePasswordNotOutput(user *iamv1alpha2.User) *iamv1alpha2.User {
out := user.DeepCopy()
// ensure encrypted password will not be output
......
/*
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 (
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/query"
"kubesphere.io/kubesphere/pkg/simple/client/ldap"
)
type ldapOperator struct {
ldapClient ldap.Interface
}
func NewLDAPOperator(ldapClient ldap.Interface) IdentityManagementInterface {
return &ldapOperator{
ldapClient: ldapClient,
}
}
func (im *ldapOperator) UpdateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
err := im.ldapClient.Update(user)
if err != nil {
return nil, err
}
return im.ldapClient.Get(user.Name)
}
func (im *ldapOperator) Authenticate(username, password string) (*iamv1alpha2.User, error) {
user, err := im.ldapClient.Get(username)
if err != nil {
return nil, err
}
err = im.ldapClient.Authenticate(user.Name, password)
if err != nil {
return nil, err
}
return user, nil
}
func (im *ldapOperator) DescribeUser(username string) (*iamv1alpha2.User, error) {
return im.ldapClient.Get(username)
}
func (im *ldapOperator) DeleteUser(username string) error {
return im.ldapClient.Delete(username)
}
func (im *ldapOperator) CreateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, error) {
err := im.ldapClient.Create(user)
if err != nil {
return nil, err
}
return user, nil
}
func (im *ldapOperator) ListUsers(query *query.Query) (*api.ListResult, error) {
result, err := im.ldapClient.List(query)
if err != nil {
klog.Error(err)
return nil, err
}
return result, nil
}
......@@ -43,10 +43,3 @@ type PodInfo struct {
Pod string `json:"pod" description:"pod name"`
Container string `json:"container" description:"container name"`
}
type AuthGrantResponse struct {
TokenType string `json:"token_type,omitempty"`
Token string `json:"access_token" description:"access token"`
ExpiresIn float64 `json:"expires_in,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册