harbor/src/server/v2.0/handler/robot.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 >= int64(-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
}