diff --git a/src/controller/robot/controller.go b/src/controller/robot/controller.go index c44b84109..c3a80a613 100644 --- a/src/controller/robot/controller.go +++ b/src/controller/robot/controller.go @@ -3,6 +3,7 @@ package robot import ( "context" "fmt" + "regexp" "strconv" "time" @@ -12,6 +13,7 @@ import ( "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/lib/retry" "github.com/goharbor/harbor/src/pkg" "github.com/goharbor/harbor/src/pkg/permission/types" "github.com/goharbor/harbor/src/pkg/project" @@ -99,9 +101,10 @@ func (d *controller) Create(ctx context.Context, r *Robot) (int64, string, error expiresAt = time.Now().AddDate(0, 0, duration).Unix() } - pwd := utils.GenerateRandomString() - salt := utils.GenerateRandomString() - secret := utils.Encrypt(pwd, salt, utils.SHA256) + secret, pwd, salt, err := CreateSec() + if err != nil { + return 0, "", err + } name := r.Name // for the project level robot, set the name pattern as projectname+robotname, and + is a illegal character. @@ -360,3 +363,44 @@ func (d *controller) toScope(ctx context.Context, p *Permission) (string, error) } return "", errors.New(nil).WithMessage("unknown robot kind").WithCode(errors.BadRequestCode) } + +func CreateSec(salt ...string) (string, string, string, error) { + var secret, pwd string + options := []retry.Option{ + retry.InitialInterval(time.Millisecond * 500), + retry.MaxInterval(time.Second * 10), + retry.Timeout(time.Minute), + retry.Callback(func(err error, sleep time.Duration) { + log.Debugf("failed to generate secret for robot, retry after %s : %v", sleep, err) + }), + } + + if err := retry.Retry(func() error { + pwd = utils.GenerateRandomString() + if !IsValidSec(pwd) { + return errors.New(nil).WithMessage("invalid secret format") + } + return nil + }, options...); err != nil { + return "", "", "", errors.Wrap(err, "failed to generate an valid random secret for robot in one minute, please try again") + } + + var saltTmp string + if len(salt) != 0 { + saltTmp = salt[0] + } else { + saltTmp = utils.GenerateRandomString() + } + secret = utils.Encrypt(pwd, saltTmp, utils.SHA256) + return secret, pwd, saltTmp, nil +} + +func IsValidSec(secret string) bool { + hasLower := regexp.MustCompile(`[a-z]`) + hasUpper := regexp.MustCompile(`[A-Z]`) + hasNumber := regexp.MustCompile(`\d`) + if len(secret) >= 8 && hasLower.MatchString(secret) && hasUpper.MatchString(secret) && hasNumber.MatchString(secret) { + return true + } + return false +} diff --git a/src/controller/robot/controller_test.go b/src/controller/robot/controller_test.go index b1ca63bc6..15bf82a4b 100644 --- a/src/controller/robot/controller_test.go +++ b/src/controller/robot/controller_test.go @@ -292,6 +292,20 @@ func (suite *ControllerTestSuite) TestToScope() { } +func (suite *ControllerTestSuite) TestIsValidSec() { + sec := "1234abcdABCD" + suite.True(IsValidSec(sec)) + sec = "1234abcd" + suite.False(IsValidSec(sec)) + sec = "123abc" + suite.False(IsValidSec(sec)) +} + +func (suite *ControllerTestSuite) TestCreateSec() { + _, pwd, _, err := CreateSec() + suite.Nil(err) + suite.True(IsValidSec(pwd)) +} func TestControllerTestSuite(t *testing.T) { suite.Run(t, &ControllerTestSuite{}) } diff --git a/src/server/v2.0/handler/robot.go b/src/server/v2.0/handler/robot.go index b68106c7d..e595cf5c2 100644 --- a/src/server/v2.0/handler/robot.go +++ b/src/server/v2.0/handler/robot.go @@ -239,14 +239,17 @@ func (rAPI *robotAPI) RefreshSec(ctx context.Context, params operation.RefreshSe var secret string robotSec := &models.RobotSec{} if params.RobotSec.Secret != "" { - if !isValidSec(params.RobotSec.Secret) { + if !robot.IsValidSec(params.RobotSec.Secret) { return rAPI.SendError(ctx, errors.New("the secret must longer than 8 chars with at least 1 uppercase letter, 1 lowercase letter and 1 number").WithCode(errors.BadRequestCode)) } secret = utils.Encrypt(params.RobotSec.Secret, r.Salt, utils.SHA256) robotSec.Secret = "" } else { - pwd := utils.GenerateRandomString() - secret = utils.Encrypt(pwd, r.Salt, utils.SHA256) + sec, pwd, _, err := robot.CreateSec(r.Salt) + if err != nil { + return rAPI.SendError(ctx, err) + } + secret = sec robotSec.Secret = pwd } @@ -348,16 +351,6 @@ func isValidDuration(d int64) bool { return d >= int64(-1) && d < math.MaxInt32 } -func isValidSec(sec string) bool { - hasLower := regexp.MustCompile(`[a-z]`) - hasUpper := regexp.MustCompile(`[A-Z]`) - hasNumber := regexp.MustCompile(`\d`) - if len(sec) >= 8 && hasLower.MatchString(sec) && hasUpper.MatchString(sec) && hasNumber.MatchString(sec) { - return true - } - return false -} - // validateName validates the robot name, especially '+' cannot be a valid character func validateName(name string) error { robotNameReg := `^[a-z0-9]+(?:[._-][a-z0-9]+)*$`