mirror of https://github.com/goharbor/harbor.git
363 lines
11 KiB
Go
363 lines
11 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/go-openapi/runtime/middleware"
|
|
"github.com/go-openapi/strfmt"
|
|
"github.com/goharbor/harbor/src/common/rbac"
|
|
"github.com/goharbor/harbor/src/common/utils"
|
|
"github.com/goharbor/harbor/src/controller/robot"
|
|
"github.com/goharbor/harbor/src/core/config"
|
|
"github.com/goharbor/harbor/src/lib"
|
|
"github.com/goharbor/harbor/src/lib/errors"
|
|
pkg "github.com/goharbor/harbor/src/pkg/robot/model"
|
|
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
|
|
"github.com/goharbor/harbor/src/server/v2.0/models"
|
|
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/robot"
|
|
"math"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func newRobotAPI() *robotAPI {
|
|
return &robotAPI{
|
|
robotCtl: robot.Ctl,
|
|
}
|
|
}
|
|
|
|
type robotAPI struct {
|
|
BaseAPI
|
|
robotCtl robot.Controller
|
|
}
|
|
|
|
func (rAPI *robotAPI) CreateRobot(ctx context.Context, params operation.CreateRobotParams) middleware.Responder {
|
|
if err := validateName(params.Robot.Name); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
if err := rAPI.validate(params.Robot.Duration, params.Robot.Level, params.Robot.Permissions); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
if err := rAPI.requireAccess(ctx, params.Robot.Level, params.Robot.Permissions[0].Namespace, rbac.ActionCreate); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
r := &robot.Robot{
|
|
Robot: pkg.Robot{
|
|
Name: params.Robot.Name,
|
|
Description: params.Robot.Description,
|
|
Duration: params.Robot.Duration,
|
|
Visible: true,
|
|
},
|
|
Level: params.Robot.Level,
|
|
}
|
|
|
|
lib.JSONCopy(&r.Permissions, params.Robot.Permissions)
|
|
|
|
rid, pwd, err := rAPI.robotCtl.Create(ctx, r)
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
created, err := rAPI.robotCtl.Get(ctx, rid, nil)
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
location := fmt.Sprintf("%s/%d", strings.TrimSuffix(params.HTTPRequest.URL.Path, "/"), created.ID)
|
|
return operation.NewCreateRobotCreated().WithLocation(location).WithPayload(&models.RobotCreated{
|
|
ID: created.ID,
|
|
Name: created.Name,
|
|
Secret: pwd,
|
|
CreationTime: strfmt.DateTime(created.CreationTime),
|
|
ExpiresAt: created.ExpiresAt,
|
|
})
|
|
}
|
|
|
|
func (rAPI *robotAPI) DeleteRobot(ctx context.Context, params operation.DeleteRobotParams) middleware.Responder {
|
|
if err := rAPI.RequireAuthenticated(ctx); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
r, err := rAPI.robotCtl.Get(ctx, params.RobotID, nil)
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
if err := rAPI.requireAccess(ctx, r.Level, r.ProjectID, rbac.ActionDelete); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
if err := rAPI.robotCtl.Delete(ctx, params.RobotID); err != nil {
|
|
// for the version 1 robot account, has to ignore the no permissions error.
|
|
if !r.Editable && errors.IsNotFoundErr(err) {
|
|
return operation.NewDeleteRobotOK()
|
|
}
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
return operation.NewDeleteRobotOK()
|
|
}
|
|
|
|
func (rAPI *robotAPI) ListRobot(ctx context.Context, params operation.ListRobotParams) middleware.Responder {
|
|
if err := rAPI.RequireAuthenticated(ctx); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
query, err := rAPI.BuildQuery(ctx, params.Q, params.Page, params.PageSize)
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
var projectID int64
|
|
var level string
|
|
// GET /api/v2.0/robots or GET /api/v2.0/robots?level=system to get all of system level robots.
|
|
// GET /api/v2.0/robots?level=project&project_id=1
|
|
if _, ok := query.Keywords["Level"]; ok {
|
|
if !isValidLevel(query.Keywords["Level"].(string)) {
|
|
return rAPI.SendError(ctx, errors.New(nil).WithMessage("bad request error level input").WithCode(errors.BadRequestCode))
|
|
}
|
|
level = query.Keywords["Level"].(string)
|
|
if level == robot.LEVELPROJECT {
|
|
if _, ok := query.Keywords["ProjectID"]; !ok {
|
|
return rAPI.SendError(ctx, errors.BadRequestError(nil).WithMessage("must with project ID when to query project robots"))
|
|
}
|
|
pid, err := strconv.ParseInt(query.Keywords["ProjectID"].(string), 10, 64)
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, errors.BadRequestError(nil).WithMessage("Project ID must be int type."))
|
|
}
|
|
projectID = pid
|
|
}
|
|
|
|
} else {
|
|
level = robot.LEVELSYSTEM
|
|
query.Keywords["ProjectID"] = 0
|
|
}
|
|
query.Keywords["Visible"] = true
|
|
|
|
if err := rAPI.requireAccess(ctx, level, projectID, rbac.ActionList); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
total, err := rAPI.robotCtl.Count(ctx, query)
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
robots, err := rAPI.robotCtl.List(ctx, query, &robot.Option{
|
|
WithPermission: true,
|
|
})
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
var results []*models.Robot
|
|
for _, r := range robots {
|
|
results = append(results, model.NewRobot(r).ToSwagger())
|
|
}
|
|
|
|
return operation.NewListRobotOK().
|
|
WithXTotalCount(total).
|
|
WithLink(rAPI.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()).
|
|
WithPayload(results)
|
|
}
|
|
|
|
func (rAPI *robotAPI) GetRobotByID(ctx context.Context, params operation.GetRobotByIDParams) middleware.Responder {
|
|
if err := rAPI.RequireAuthenticated(ctx); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
r, err := rAPI.robotCtl.Get(ctx, params.RobotID, &robot.Option{
|
|
WithPermission: true,
|
|
})
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
if err := rAPI.requireAccess(ctx, r.Level, r.ProjectID, rbac.ActionRead); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
return operation.NewGetRobotByIDOK().WithPayload(model.NewRobot(r).ToSwagger())
|
|
}
|
|
|
|
func (rAPI *robotAPI) UpdateRobot(ctx context.Context, params operation.UpdateRobotParams) middleware.Responder {
|
|
var err error
|
|
if err := rAPI.RequireAuthenticated(ctx); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
r, err := rAPI.robotCtl.Get(ctx, params.RobotID, &robot.Option{
|
|
WithPermission: true,
|
|
})
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
if !r.Editable {
|
|
err = rAPI.updateV1Robot(ctx, params, r)
|
|
} else {
|
|
err = rAPI.updateV2Robot(ctx, params, r)
|
|
}
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
return operation.NewUpdateRobotOK()
|
|
}
|
|
|
|
func (rAPI *robotAPI) RefreshSec(ctx context.Context, params operation.RefreshSecParams) middleware.Responder {
|
|
if err := rAPI.RequireAuthenticated(ctx); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
r, err := rAPI.robotCtl.Get(ctx, params.RobotID, nil)
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
if err := rAPI.requireAccess(ctx, r.Level, r.ProjectID, rbac.ActionUpdate); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
var secret string
|
|
robotSec := &models.RobotSec{}
|
|
if params.RobotSec.Secret != "" {
|
|
if !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)
|
|
robotSec.Secret = pwd
|
|
}
|
|
|
|
r.Secret = secret
|
|
if err := rAPI.robotCtl.Update(ctx, r, nil); err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
|
|
return operation.NewRefreshSecOK().WithPayload(robotSec)
|
|
}
|
|
|
|
func (rAPI *robotAPI) requireAccess(ctx context.Context, level string, projectIDOrName interface{}, action rbac.Action) error {
|
|
if level == robot.LEVELSYSTEM {
|
|
return rAPI.RequireSystemAccess(ctx, action, rbac.ResourceRobot)
|
|
} else if level == robot.LEVELPROJECT {
|
|
return rAPI.RequireProjectAccess(ctx, projectIDOrName, action, rbac.ResourceRobot)
|
|
}
|
|
return errors.ForbiddenError(nil)
|
|
}
|
|
|
|
// more validation
|
|
func (rAPI *robotAPI) validate(d int64, level string, permissions []*models.RobotPermission) error {
|
|
if !isValidDuration(d) {
|
|
return errors.New(nil).WithMessage("bad request error duration input: %d", d).WithCode(errors.BadRequestCode)
|
|
}
|
|
|
|
if !isValidLevel(level) {
|
|
return errors.New(nil).WithMessage("bad request error level input: %s", level).WithCode(errors.BadRequestCode)
|
|
}
|
|
|
|
if len(permissions) == 0 {
|
|
return errors.New(nil).WithMessage("bad request empty permission").WithCode(errors.BadRequestCode)
|
|
}
|
|
|
|
for _, perm := range permissions {
|
|
if len(perm.Access) == 0 {
|
|
return errors.New(nil).WithMessage("bad request empty access").WithCode(errors.BadRequestCode)
|
|
}
|
|
}
|
|
|
|
// to create a project robot, the permission must be only one project scope.
|
|
if level == robot.LEVELPROJECT && len(permissions) > 1 {
|
|
return errors.New(nil).WithMessage("bad request permission").WithCode(errors.BadRequestCode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// only disable can be updated for v1 robot
|
|
func (rAPI *robotAPI) updateV1Robot(ctx context.Context, params operation.UpdateRobotParams, r *robot.Robot) error {
|
|
if err := rAPI.requireAccess(ctx, params.Robot.Level, r.ProjectID, rbac.ActionUpdate); err != nil {
|
|
return err
|
|
}
|
|
r.Disabled = params.Robot.Disable
|
|
r.Description = params.Robot.Description
|
|
if err := rAPI.robotCtl.Update(ctx, r, &robot.Option{
|
|
WithPermission: false,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (rAPI *robotAPI) updateV2Robot(ctx context.Context, params operation.UpdateRobotParams, r *robot.Robot) error {
|
|
if err := rAPI.validate(params.Robot.Duration, params.Robot.Level, params.Robot.Permissions); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := rAPI.requireAccess(ctx, params.Robot.Level, params.Robot.Permissions[0].Namespace, rbac.ActionUpdate); err != nil {
|
|
return err
|
|
}
|
|
|
|
if params.Robot.Level != r.Level || params.Robot.Name != r.Name {
|
|
return errors.BadRequestError(nil).WithMessage("cannot update the level or name of robot")
|
|
}
|
|
|
|
if r.Duration != params.Robot.Duration {
|
|
r.Duration = params.Robot.Duration
|
|
if params.Robot.Duration == -1 {
|
|
r.ExpiresAt = -1
|
|
} else if params.Robot.Duration == 0 {
|
|
r.Duration = int64(config.RobotTokenDuration())
|
|
r.ExpiresAt = r.CreationTime.AddDate(0, 0, config.RobotTokenDuration()).Unix()
|
|
} else {
|
|
r.ExpiresAt = r.CreationTime.AddDate(0, 0, int(params.Robot.Duration)).Unix()
|
|
}
|
|
}
|
|
|
|
r.Description = params.Robot.Description
|
|
r.Disabled = params.Robot.Disable
|
|
if len(params.Robot.Permissions) != 0 {
|
|
lib.JSONCopy(&r.Permissions, params.Robot.Permissions)
|
|
}
|
|
|
|
if err := rAPI.robotCtl.Update(ctx, r, &robot.Option{
|
|
WithPermission: true,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isValidLevel(l string) bool {
|
|
return l == robot.LEVELSYSTEM || l == robot.LEVELPROJECT
|
|
}
|
|
|
|
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(`[0-9]`)
|
|
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]+)*$`
|
|
legal := regexp.MustCompile(robotNameReg).MatchString(name)
|
|
if !legal {
|
|
return errors.BadRequestError(nil).WithMessage("robot name is not in lower case or contains illegal characters")
|
|
}
|
|
return nil
|
|
}
|