mirror of https://github.com/goharbor/harbor.git
417 lines
11 KiB
Go
417 lines
11 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 robot
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
|
|
rbac_project "github.com/goharbor/harbor/src/common/rbac/project"
|
|
"github.com/goharbor/harbor/src/common/utils"
|
|
"github.com/goharbor/harbor/src/lib/config"
|
|
"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"
|
|
"github.com/goharbor/harbor/src/pkg/rbac"
|
|
rbac_model "github.com/goharbor/harbor/src/pkg/rbac/model"
|
|
robot "github.com/goharbor/harbor/src/pkg/robot"
|
|
"github.com/goharbor/harbor/src/pkg/robot/model"
|
|
)
|
|
|
|
var (
|
|
// Ctl is a global variable for the default robot account controller implementation
|
|
Ctl = NewController()
|
|
)
|
|
|
|
// Controller to handle the requests related with robot account
|
|
type Controller interface {
|
|
// Get ...
|
|
Get(ctx context.Context, id int64, option *Option) (*Robot, error)
|
|
|
|
// Count returns the total count of robots according to the query
|
|
Count(ctx context.Context, query *q.Query) (total int64, err error)
|
|
|
|
// Create ...
|
|
Create(ctx context.Context, r *Robot) (int64, string, error)
|
|
|
|
// Delete ...
|
|
Delete(ctx context.Context, id int64) error
|
|
|
|
// Update ...
|
|
Update(ctx context.Context, r *Robot, option *Option) error
|
|
|
|
// List ...
|
|
List(ctx context.Context, query *q.Query, option *Option) ([]*Robot, error)
|
|
}
|
|
|
|
// controller ...
|
|
type controller struct {
|
|
robotMgr robot.Manager
|
|
proMgr project.Manager
|
|
rbacMgr rbac.Manager
|
|
}
|
|
|
|
// NewController ...
|
|
func NewController() Controller {
|
|
return &controller{
|
|
robotMgr: robot.Mgr,
|
|
proMgr: pkg.ProjectMgr,
|
|
rbacMgr: rbac.Mgr,
|
|
}
|
|
}
|
|
|
|
// Get ...
|
|
func (d *controller) Get(ctx context.Context, id int64, option *Option) (*Robot, error) {
|
|
robot, err := d.robotMgr.Get(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return d.populate(ctx, robot, option)
|
|
}
|
|
|
|
// Count ...
|
|
func (d *controller) Count(ctx context.Context, query *q.Query) (int64, error) {
|
|
return d.robotMgr.Count(ctx, query)
|
|
}
|
|
|
|
// Create ...
|
|
func (d *controller) Create(ctx context.Context, r *Robot) (int64, string, error) {
|
|
if err := d.setProject(ctx, r); err != nil {
|
|
return 0, "", err
|
|
}
|
|
|
|
var expiresAt int64
|
|
if r.Duration == -1 {
|
|
expiresAt = -1
|
|
} else {
|
|
durationStr := strconv.FormatInt(r.Duration, 10)
|
|
duration, err := strconv.Atoi(durationStr)
|
|
if err != nil {
|
|
return 0, "", err
|
|
}
|
|
expiresAt = time.Now().AddDate(0, 0, duration).Unix()
|
|
}
|
|
|
|
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.
|
|
if r.Level == LEVELPROJECT {
|
|
name = fmt.Sprintf("%s+%s", r.ProjectName, r.Name)
|
|
}
|
|
robotID, err := d.robotMgr.Create(ctx, &model.Robot{
|
|
Name: name,
|
|
Description: r.Description,
|
|
ProjectID: r.ProjectID,
|
|
ExpiresAt: expiresAt,
|
|
Secret: secret,
|
|
Duration: r.Duration,
|
|
Salt: salt,
|
|
Visible: r.Visible,
|
|
})
|
|
if err != nil {
|
|
return 0, "", err
|
|
}
|
|
r.ID = robotID
|
|
if err := d.createPermission(ctx, r); err != nil {
|
|
return 0, "", err
|
|
}
|
|
return robotID, pwd, nil
|
|
}
|
|
|
|
// Delete ...
|
|
func (d *controller) Delete(ctx context.Context, id int64) error {
|
|
if err := d.robotMgr.Delete(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
if err := d.rbacMgr.DeletePermissionsByRole(ctx, ROBOTTYPE, id); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Update ...
|
|
func (d *controller) Update(ctx context.Context, r *Robot, option *Option) error {
|
|
if r == nil {
|
|
return errors.New("cannot update a nil robot").WithCode(errors.BadRequestCode)
|
|
}
|
|
if err := d.robotMgr.Update(ctx, &r.Robot, "secret", "description", "disabled", "duration", "expiresat"); err != nil {
|
|
return err
|
|
}
|
|
// update the permission
|
|
if option != nil && option.WithPermission {
|
|
if err := d.rbacMgr.DeletePermissionsByRole(ctx, ROBOTTYPE, r.ID); err != nil && !errors.IsNotFoundErr(err) {
|
|
return err
|
|
}
|
|
if err := d.createPermission(ctx, r); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// List ...
|
|
func (d *controller) List(ctx context.Context, query *q.Query, option *Option) ([]*Robot, error) {
|
|
robots, err := d.robotMgr.List(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var robotAccounts []*Robot
|
|
for _, r := range robots {
|
|
rb, err := d.populate(ctx, r, option)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
robotAccounts = append(robotAccounts, rb)
|
|
}
|
|
return robotAccounts, nil
|
|
}
|
|
|
|
func (d *controller) createPermission(ctx context.Context, r *Robot) error {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, per := range r.Permissions {
|
|
policy := &rbac_model.PermissionPolicy{}
|
|
scope, err := d.toScope(ctx, per)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
policy.Scope = scope
|
|
|
|
for _, access := range per.Access {
|
|
policy.Resource = access.Resource.String()
|
|
policy.Action = access.Action.String()
|
|
policy.Effect = access.Effect.String()
|
|
|
|
policyID, err := d.rbacMgr.CreateRbacPolicy(ctx, policy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = d.rbacMgr.CreatePermission(ctx, &rbac_model.RolePermission{
|
|
RoleType: ROBOTTYPE,
|
|
RoleID: r.ID,
|
|
PermissionPolicyID: policyID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *controller) populate(ctx context.Context, r *model.Robot, option *Option) (*Robot, error) {
|
|
if r == nil {
|
|
return nil, nil
|
|
}
|
|
robot := &Robot{
|
|
Robot: *r,
|
|
}
|
|
robot.setLevel()
|
|
robot.setEditable()
|
|
// for the v2 robots, add prefix to the robot name
|
|
// for the v1 legacy robots, keep the robot name
|
|
if robot.Editable {
|
|
robot.Name = fmt.Sprintf("%s%s", config.RobotPrefix(ctx), r.Name)
|
|
} else {
|
|
robot.Name = r.Name
|
|
}
|
|
if option != nil && option.WithPermission {
|
|
if err := d.populatePermissions(ctx, robot); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return robot, nil
|
|
}
|
|
|
|
func (d *controller) populatePermissions(ctx context.Context, r *Robot) error {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
rolePermissions, err := d.rbacMgr.GetPermissionsByRole(ctx, ROBOTTYPE, r.ID)
|
|
if err != nil {
|
|
log.Errorf("failed to get permissions of robot %d: %v", r.ID, err)
|
|
return err
|
|
}
|
|
if len(rolePermissions) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// scope: accesses
|
|
accessMap := make(map[string][]*types.Policy)
|
|
|
|
// group by scope
|
|
for _, rp := range rolePermissions {
|
|
_, exist := accessMap[rp.Scope]
|
|
if !exist {
|
|
accessMap[rp.Scope] = []*types.Policy{{
|
|
Resource: types.Resource(rp.Resource),
|
|
Action: types.Action(rp.Action),
|
|
Effect: types.Effect(rp.Effect),
|
|
}}
|
|
} else {
|
|
accesses := accessMap[rp.Scope]
|
|
accesses = append(accesses, &types.Policy{
|
|
Resource: types.Resource(rp.Resource),
|
|
Action: types.Action(rp.Action),
|
|
Effect: types.Effect(rp.Effect),
|
|
})
|
|
accessMap[rp.Scope] = accesses
|
|
}
|
|
}
|
|
|
|
var permissions []*Permission
|
|
for scope, accesses := range accessMap {
|
|
kind, namespace, err := d.convertScope(ctx, scope)
|
|
if err != nil {
|
|
// if the project is removed, do not list it in the robot account permission.
|
|
if errors.IsNotFoundErr(err) {
|
|
continue
|
|
}
|
|
log.Errorf("failed to decode scope of robot %d: %v", r.ID, err)
|
|
return err
|
|
}
|
|
p := &Permission{}
|
|
p.Scope = scope
|
|
p.Kind = kind
|
|
p.Namespace = namespace
|
|
p.Access = accesses
|
|
permissions = append(permissions, p)
|
|
}
|
|
r.Permissions = permissions
|
|
return nil
|
|
}
|
|
|
|
// set the project info if it's a project level robot
|
|
func (d *controller) setProject(ctx context.Context, r *Robot) error {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
if r.Level == LEVELPROJECT {
|
|
pro, err := d.proMgr.Get(ctx, r.Permissions[0].Namespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.ProjectName = pro.Name
|
|
r.ProjectID = pro.ProjectID
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// convertScope converts the db scope into robot model
|
|
// /system => Kind: system Namespace: /
|
|
// /project/* => Kind: project Namespace: *
|
|
// /project/1 => Kind: project Namespace: library
|
|
func (d *controller) convertScope(ctx context.Context, scope string) (kind, namespace string, err error) {
|
|
if scope == "" {
|
|
return
|
|
}
|
|
if scope == SCOPESYSTEM {
|
|
kind = LEVELSYSTEM
|
|
namespace = "/"
|
|
} else if scope == SCOPEALLPROJECT {
|
|
kind = LEVELPROJECT
|
|
namespace = "*"
|
|
} else {
|
|
kind = LEVELPROJECT
|
|
ns, ok := rbac_project.NamespaceParse(types.Resource(scope))
|
|
if !ok {
|
|
log.Debugf("got no namespace from the resource %s", scope)
|
|
return "", "", errors.Errorf("got no namespace from the resource %s", scope)
|
|
}
|
|
pro, err := d.proMgr.Get(ctx, ns.Identity())
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
namespace = pro.Name
|
|
}
|
|
return
|
|
}
|
|
|
|
// toScope ...
|
|
func (d *controller) toScope(ctx context.Context, p *Permission) (string, error) {
|
|
switch p.Kind {
|
|
case LEVELSYSTEM:
|
|
if p.Namespace != "/" {
|
|
return "", errors.New(nil).WithMessage("unknown namespace").WithCode(errors.BadRequestCode)
|
|
}
|
|
return SCOPESYSTEM, nil
|
|
case LEVELPROJECT:
|
|
if p.Namespace == "*" {
|
|
return SCOPEALLPROJECT, nil
|
|
}
|
|
pro, err := d.proMgr.Get(ctx, p.Namespace)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("/project/%d", pro.ProjectID), nil
|
|
}
|
|
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
|
|
}
|
|
|
|
var (
|
|
hasLower = regexp.MustCompile(`[a-z]`)
|
|
hasUpper = regexp.MustCompile(`[A-Z]`)
|
|
hasNumber = regexp.MustCompile(`\d`)
|
|
)
|
|
|
|
func IsValidSec(secret string) bool {
|
|
return len(secret) >= 8 && len(secret) <= 128 && hasLower.MatchString(secret) && hasUpper.MatchString(secret) && hasNumber.MatchString(secret)
|
|
}
|