API for user to set the CLI secret

This commit replace the API to generate CLI secret with a new API to
update the secret

Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
Daniel Jiang 2019-10-10 19:46:30 +08:00
parent 49f12d0b16
commit 53a13e165d
4 changed files with 58 additions and 25 deletions

View File

@ -959,13 +959,13 @@ paths:
description: User ID does not exist. description: User ID does not exist.
'500': '500':
description: Unexpected internal errors. description: Unexpected internal errors.
'/users/{user_id}/gen_cli_secret': '/users/{user_id}/cli_secret':
post: put:
summary: Generate new CLI secret for a user. summary: Set CLI secret for a user.
description: | description: |
This endpoint let user generate a new CLI secret for himself. This API only works when auth mode is set to 'OIDC'. This endpoint let user generate a new CLI secret for himself. This API only works when auth mode is set to 'OIDC'.
Once this API returns with successful status, the old secret will be invalid, as there will be only one CLI secret Once this API returns with successful status, the old secret will be invalid, as there will be only one CLI secret
for a user. The new secret will be returned in the response. for a user.
parameters: parameters:
- name: user_id - name: user_id
in: path in: path
@ -973,19 +973,23 @@ paths:
format: int format: int
required: true required: true
description: User ID description: User ID
tags: - name: input_secret
- Products in: body
responses: description: JSON object that includes the new secret
'200': required: true
description: The secret is successfully generated.
schema: schema:
type: object type: object
properties: properties:
secret: secret:
type: string type: string
description: The new secret description: The new secret
tags:
- Products
responses:
'200':
description: The secret is successfully updated
'400': '400':
description: Invalid user ID. Or user is not onboarded via OIDC authentication. description: Invalid user ID. Or user is not onboarded via OIDC authentication. Or the secret does not meet the standard.
'401': '401':
description: User need to log in first. description: User need to log in first.
'403': '403':

View File

@ -52,7 +52,7 @@ type userSearch struct {
Username string `json:"username"` Username string `json:"username"`
} }
type secretResp struct { type secretReq struct {
Secret string `json:"secret"` Secret string `json:"secret"`
} }
@ -405,8 +405,8 @@ func (ua *UserAPI) ChangePassword() {
return return
} }
if len(req.NewPassword) == 0 { if err := validateSecret(req.NewPassword); err != nil {
ua.SendBadRequestError(errors.New("empty new_password")) ua.SendBadRequestError(err)
return return
} }
@ -512,8 +512,8 @@ func (ua *UserAPI) ListUserPermissions() {
return return
} }
// GenCLISecret generates a new CLI secret and replace the old one // SetCLISecret handles request PUT /api/users/:id/cli_secret to update the CLI secret of the user
func (ua *UserAPI) GenCLISecret() { func (ua *UserAPI) SetCLISecret() {
if ua.AuthMode != common.OIDCAuth { if ua.AuthMode != common.OIDCAuth {
ua.SendPreconditionFailedError(errors.New("the auth mode has to be oidc auth")) ua.SendPreconditionFailedError(errors.New("the auth mode has to be oidc auth"))
return return
@ -534,8 +534,17 @@ func (ua *UserAPI) GenCLISecret() {
return return
} }
sec := utils.GenerateRandomString() s := &secretReq{}
encSec, err := utils.ReversibleEncrypt(sec, ua.secretKey) if err := ua.DecodeJSONReq(s); err != nil {
ua.SendBadRequestError(err)
return
}
if err := validateSecret(s.Secret); err != nil {
ua.SendBadRequestError(err)
return
}
encSec, err := utils.ReversibleEncrypt(s.Secret, ua.secretKey)
if err != nil { if err != nil {
log.Errorf("Failed to encrypt secret, error: %v", err) log.Errorf("Failed to encrypt secret, error: %v", err)
ua.SendInternalServerError(errors.New("failed to encrypt secret")) ua.SendInternalServerError(errors.New("failed to encrypt secret"))
@ -548,8 +557,6 @@ func (ua *UserAPI) GenCLISecret() {
ua.SendInternalServerError(errors.New("failed to update secret in DB")) ua.SendInternalServerError(errors.New("failed to update secret in DB"))
return return
} }
ua.Data["json"] = secretResp{sec}
ua.ServeJSON()
} }
func (ua *UserAPI) getOIDCUserInfo() (*models.OIDCUser, error) { func (ua *UserAPI) getOIDCUserInfo() (*models.OIDCUser, error) {
@ -588,12 +595,24 @@ func validate(user models.User) error {
if utils.IsContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) { if utils.IsContainIllegalChar(user.Username, []string{",", "~", "#", "$", "%"}) {
return fmt.Errorf("username contains illegal characters") return fmt.Errorf("username contains illegal characters")
} }
if utils.IsIllegalLength(user.Password, 8, 20) {
return fmt.Errorf("password with illegal length") if err := validateSecret(user.Password); err != nil {
return err
} }
return commonValidate(user) return commonValidate(user)
} }
func validateSecret(in string) error {
hasLower := regexp.MustCompile(`[a-z]`)
hasUpper := regexp.MustCompile(`[A-Z]`)
hasNumber := regexp.MustCompile(`[0-9]`)
if len(in) >= 8 && hasLower.MatchString(in) && hasUpper.MatchString(in) && hasNumber.MatchString(in) {
return nil
}
return errors.New("the password or secret must longer than 8 chars with at least 1 uppercase letter, 1 lowercase letter and 1 number")
}
// commonValidate validates email, realname, comment information when user register or change their profile // commonValidate validates email, realname, comment information when user register or change their profile
func commonValidate(user models.User) error { func commonValidate(user models.User) error {

View File

@ -380,8 +380,8 @@ func buildChangeUserPasswordURL(id int) string {
func TestUsersUpdatePassword(t *testing.T) { func TestUsersUpdatePassword(t *testing.T) {
fmt.Println("Testing Update User Password") fmt.Println("Testing Update User Password")
oldPassword := "old_password" oldPassword := "old_Passw0rd"
newPassword := "new_password" newPassword := "new_Passw0rd"
user01 := models.User{ user01 := models.User{
Username: "user01_for_testing_change_password", Username: "user01_for_testing_change_password",
@ -515,7 +515,7 @@ func TestUsersUpdatePassword(t *testing.T) {
method: http.MethodPut, method: http.MethodPut,
url: buildChangeUserPasswordURL(user01.UserID), url: buildChangeUserPasswordURL(user01.UserID),
bodyJSON: &passwordReq{ bodyJSON: &passwordReq{
NewPassword: "another_new_password", NewPassword: "another_new_Passw0rd",
}, },
credential: admin, credential: admin,
}, },
@ -642,3 +642,13 @@ func TestUsersCurrentPermissions(t *testing.T) {
assert.Nil(err) assert.Nil(err)
assert.Equal(int(403), httpStatusCode, "httpStatusCode should be 403") assert.Equal(int(403), httpStatusCode, "httpStatusCode should be 403")
} }
func TestValidateSecret(t *testing.T) {
assert.NotNil(t, validateSecret(""))
assert.NotNil(t, validateSecret("12345678"))
assert.NotNil(t, validateSecret("passw0rd"))
assert.NotNil(t, validateSecret("PASSW0RD"))
assert.NotNil(t, validateSecret("Sh0rt"))
assert.Nil(t, validateSecret("Passw0rd"))
assert.Nil(t, validateSecret("Thisis1Valid_password"))
}

View File

@ -52,7 +52,7 @@ func initRouters() {
beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword") beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
beego.Router("/api/users/:id/permissions", &api.UserAPI{}, "get:ListUserPermissions") beego.Router("/api/users/:id/permissions", &api.UserAPI{}, "get:ListUserPermissions")
beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole") beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/users/:id/gen_cli_secret", &api.UserAPI{}, "post:GenCLISecret") beego.Router("/api/users/:id/cli_secret", &api.UserAPI{}, "put:SetCLISecret")
beego.Router("/api/usergroups/?:ugid([0-9]+)", &api.UserGroupAPI{}) beego.Router("/api/usergroups/?:ugid([0-9]+)", &api.UserGroupAPI{})
beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping") beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping")
beego.Router("/api/ldap/users/search", &api.LdapAPI{}, "get:Search") beego.Router("/api/ldap/users/search", &api.LdapAPI{}, "get:Search")