mirror of https://github.com/goharbor/harbor.git
398 lines
12 KiB
Go
398 lines
12 KiB
Go
// Copyright Project Harbor 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 handler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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/lib"
|
|
"github.com/goharbor/harbor/src/lib/errors"
|
|
"github.com/goharbor/harbor/src/lib/log"
|
|
"github.com/goharbor/harbor/src/pkg/permission/types"
|
|
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"
|
|
)
|
|
|
|
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,
|
|
}
|
|
|
|
if err := lib.JSONCopy(&r.Permissions, params.Robot.Permissions); err != nil {
|
|
log.Warningf("failed to call JSONCopy on robot permission when CreateRobot, error: %v", err)
|
|
}
|
|
|
|
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 permission 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.Sort, 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 = errors.DeniedError(nil).WithMessage("editing of legacy robot is not allowed")
|
|
} 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 !robot.IsValidSec(params.RobotSec.Secret) {
|
|
return rAPI.SendError(ctx, errors.New("the secret must be 8-128, inclusively, characters long 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 {
|
|
sec, pwd, _, err := robot.CreateSec(r.Salt)
|
|
if err != nil {
|
|
return rAPI.SendError(ctx, err)
|
|
}
|
|
secret = sec
|
|
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, duration must be either -1(Never) or a positive integer", 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)
|
|
}
|
|
|
|
// to validate the access scope
|
|
for _, perm := range permissions {
|
|
if perm.Kind == robot.LEVELSYSTEM {
|
|
polices := rbac.PoliciesMap["System"]
|
|
for _, acc := range perm.Access {
|
|
if !containsAccess(polices, acc) {
|
|
return errors.New(nil).WithMessage("bad request permission: %s:%s", acc.Resource, acc.Action).WithCode(errors.BadRequestCode)
|
|
}
|
|
}
|
|
} else if perm.Kind == robot.LEVELPROJECT {
|
|
polices := rbac.PoliciesMap["Project"]
|
|
for _, acc := range perm.Access {
|
|
if !containsAccess(polices, acc) {
|
|
return errors.New(nil).WithMessage("bad request permission: %s:%s", acc.Resource, acc.Action).WithCode(errors.BadRequestCode)
|
|
}
|
|
}
|
|
} else {
|
|
return errors.New(nil).WithMessage("bad request permission level: %s", perm.Kind).WithCode(errors.BadRequestCode)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (rAPI *robotAPI) updateV2Robot(ctx context.Context, params operation.UpdateRobotParams, r *robot.Robot) error {
|
|
if params.Robot.Duration == nil {
|
|
params.Robot.Duration = &r.Duration
|
|
}
|
|
if err := rAPI.validate(*params.Robot.Duration, params.Robot.Level, params.Robot.Permissions); err != nil {
|
|
return err
|
|
}
|
|
if r.Level != robot.LEVELSYSTEM {
|
|
projectID, err := getProjectID(ctx, params.Robot.Permissions[0].Namespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if r.ProjectID != projectID {
|
|
return errors.BadRequestError(nil).WithMessage("cannot update the project id of robot")
|
|
}
|
|
}
|
|
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 {
|
|
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 {
|
|
if err := lib.JSONCopy(&r.Permissions, params.Robot.Permissions); err != nil {
|
|
log.Warningf("failed to call JSONCopy on robot permission when updateV2Robot, error: %v", err)
|
|
}
|
|
}
|
|
|
|
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 == -1 || (d > 0 && d < math.MaxInt32)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func containsAccess(policies []*types.Policy, item *models.Access) bool {
|
|
for _, po := range policies {
|
|
if po.Resource.String() == item.Resource && po.Action.String() == item.Action {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|