add robot account version 2 controller (#13472)

the controller is for the enhanced robot account

Signed-off-by: Wang Yan <wangyan@vmware.com>
This commit is contained in:
Wang Yan 2020-11-19 15:39:45 +08:00 committed by GitHub
parent def782b6f8
commit 04c4354df9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 715 additions and 0 deletions

View File

@ -0,0 +1,343 @@
package robot
import (
"context"
"fmt"
rbac_common "github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/core/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/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/robot2"
"github.com/goharbor/harbor/src/pkg/robot2/model"
"time"
)
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, error)
// Delete ...
Delete(ctx context.Context, id int64) error
// Update ...
Update(ctx context.Context, r *Robot) 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: project.Mgr,
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, error) {
if err := d.setProjectID(ctx, r); err != nil {
return 0, err
}
if r.ExpiresAt == 0 {
tokenDuration := time.Duration(config.RobotTokenDuration()) * time.Minute
r.ExpiresAt = time.Now().UTC().Add(tokenDuration).Unix()
}
key, err := config.SecretKey()
if err != nil {
return 0, err
}
str := utils.GenerateRandomString()
secret, err := utils.ReversibleEncrypt(str, key)
if err != nil {
return 0, err
}
robotID, err := d.robotMgr.Create(ctx, &model.Robot{
Name: r.Name,
Description: r.Description,
ProjectID: r.ProjectID,
ExpiresAt: r.ExpiresAt,
Secret: secret,
})
if err != nil {
return 0, err
}
if err := d.createPermission(ctx, r); err != nil {
return 0, err
}
return robotID, 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) error {
if r == nil {
return errors.New("cannot update a nil robot").WithCode(errors.BadRequestCode)
}
if err := d.robotMgr.Update(ctx, &r.Robot); err != nil {
return err
}
if err := d.setProjectID(ctx, r); err != nil {
return err
}
// update the permission
if err := d.rbacMgr.DeletePermissionsByRole(ctx, ROBOTTYPE, r.ID); err != nil {
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.Name = fmt.Sprintf("%s%s", config.RobotPrefix(), r.Name)
robot.setLevel()
if option == nil {
return robot, nil
}
if 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 {
p := &Permission{}
kind, namespace, err := d.convertScope(ctx, scope)
if err != nil {
log.Errorf("failed to decode scope of robot %d: %v", r.ID, err)
return err
}
p.Kind = kind
p.Namespace = namespace
p.Access = accesses
permissions = append(permissions, p)
}
r.Permissions = permissions
return nil
}
func (d *controller) setProjectID(ctx context.Context, r *Robot) error {
if r == nil {
return nil
}
var projectID int64
switch r.Level {
case LEVELSYSTEM:
projectID = 0
case LEVELPROJECT:
pro, err := d.proMgr.Get(ctx, r.Permissions[0].Namespace)
if err != nil {
return err
}
projectID = pro.ProjectID
default:
return errors.New(nil).WithMessage("unknown robot account level").WithCode(errors.BadRequestCode)
}
r.ProjectID = 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_common.ProjectNamespaceParse(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)
}

View File

@ -0,0 +1,288 @@
package robot
import (
"context"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/utils"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/test"
core_cfg "github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/permission/types"
rbac_model "github.com/goharbor/harbor/src/pkg/rbac/model"
"github.com/goharbor/harbor/src/pkg/robot2/model"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/goharbor/harbor/src/testing/pkg/project"
"github.com/goharbor/harbor/src/testing/pkg/rbac"
"github.com/goharbor/harbor/src/testing/pkg/robot2"
"github.com/stretchr/testify/suite"
"os"
"testing"
)
type ControllerTestSuite struct {
suite.Suite
}
func (suite *ControllerTestSuite) TestGet() {
projectMgr := &project.Manager{}
rbacMgr := &rbac.Manager{}
robotMgr := &robot2.Manager{}
c := controller{robotMgr: robotMgr, rbacMgr: rbacMgr, proMgr: projectMgr}
ctx := context.TODO()
projectMgr.On("Get", mock.Anything, mock.Anything).Return(&models.Project{ProjectID: 1, Name: "library"}, nil)
robotMgr.On("Get", mock.Anything, mock.Anything).Return(&model.Robot{
Name: "test",
Description: "test get method",
ProjectID: 1,
Secret: utils.RandStringBytes(10),
}, nil)
rbacMgr.On("GetPermissionsByRole", mock.Anything, mock.Anything, mock.Anything).Return([]*rbac_model.UniversalRolePermission{
{
RoleType: ROBOTTYPE,
RoleID: 1,
Scope: "/project/1",
Resource: "repository",
Action: "pull",
},
{
RoleType: ROBOTTYPE,
RoleID: 1,
Scope: "/project/1",
Resource: "repository",
Action: "push",
},
}, nil)
robot, err := c.Get(ctx, int64(1), &Option{
WithPermission: true,
})
suite.Nil(err)
suite.Equal("project", robot.Permissions[0].Kind)
suite.Equal("library", robot.Permissions[0].Namespace)
suite.Equal("pull", robot.Permissions[0].Access[0].Action.String())
suite.Equal("project", robot.Level)
}
func (suite *ControllerTestSuite) TestCount() {
projectMgr := &project.Manager{}
rbacMgr := &rbac.Manager{}
robotMgr := &robot2.Manager{}
c := controller{robotMgr: robotMgr, rbacMgr: rbacMgr, proMgr: projectMgr}
ctx := context.TODO()
robotMgr.On("Count", mock.Anything, mock.Anything).Return(int64(1), nil)
ct, err := c.Count(ctx, nil)
suite.Nil(err)
suite.Equal(int64(1), ct)
}
func (suite *ControllerTestSuite) TestCreate() {
secretKeyPath := "/tmp/secretkey"
_, err := test.GenerateKey(secretKeyPath)
suite.Nil(err)
defer os.Remove(secretKeyPath)
os.Setenv("KEY_PATH", secretKeyPath)
conf := map[string]interface{}{
common.RobotTokenDuration: "30",
}
core_cfg.InitWithSettings(conf)
projectMgr := &project.Manager{}
rbacMgr := &rbac.Manager{}
robotMgr := &robot2.Manager{}
c := controller{robotMgr: robotMgr, rbacMgr: rbacMgr, proMgr: projectMgr}
ctx := context.TODO()
projectMgr.On("Get", mock.Anything, mock.Anything).Return(&models.Project{ProjectID: 1, Name: "library"}, nil)
robotMgr.On("Create", mock.Anything, mock.Anything).Return(int64(1), nil)
rbacMgr.On("CreateRbacPolicy", mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
rbacMgr.On("CreatePermission", mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
id, err := c.Create(ctx, &Robot{
Robot: model.Robot{
Name: "testcreate",
Description: "testcreate",
ExpiresAt: 0,
},
ProjectName: "library",
Level: LEVELPROJECT,
Permissions: []*Permission{
{
Kind: "project",
Namespace: "library",
Access: []*types.Policy{
{
Resource: "repository",
Action: "push",
},
{
Resource: "repository",
Action: "pull",
},
},
},
},
})
suite.Nil(err)
suite.Equal(int64(1), id)
}
func (suite *ControllerTestSuite) TestDelete() {
projectMgr := &project.Manager{}
rbacMgr := &rbac.Manager{}
robotMgr := &robot2.Manager{}
c := controller{robotMgr: robotMgr, rbacMgr: rbacMgr, proMgr: projectMgr}
ctx := context.TODO()
robotMgr.On("Delete", mock.Anything, mock.Anything).Return(nil)
rbacMgr.On("DeletePermissionsByRole", mock.Anything, mock.Anything, mock.Anything).Return(nil)
err := c.Delete(ctx, int64(1))
suite.Nil(err)
}
func (suite *ControllerTestSuite) TestUpdate() {
projectMgr := &project.Manager{}
rbacMgr := &rbac.Manager{}
robotMgr := &robot2.Manager{}
c := controller{robotMgr: robotMgr, rbacMgr: rbacMgr, proMgr: projectMgr}
ctx := context.TODO()
robotMgr.On("Update", mock.Anything, mock.Anything).Return(nil)
projectMgr.On("Get", mock.Anything, mock.Anything).Return(&models.Project{ProjectID: 1, Name: "library"}, nil)
rbacMgr.On("DeletePermissionsByRole", mock.Anything, mock.Anything, mock.Anything).Return(nil)
rbacMgr.On("CreateRbacPolicy", mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
rbacMgr.On("CreatePermission", mock.Anything, mock.Anything, mock.Anything).Return(int64(1), nil)
err := c.Update(ctx, &Robot{
Robot: model.Robot{
Name: "testcreate",
Description: "testcreate",
ExpiresAt: 0,
},
ProjectName: "library",
Level: LEVELPROJECT,
Permissions: []*Permission{
{
Kind: "project",
Namespace: "library",
Access: []*types.Policy{
{
Resource: "repository",
Action: "push",
},
{
Resource: "repository",
Action: "pull",
},
},
},
},
})
suite.Nil(err)
}
func (suite *ControllerTestSuite) TestList() {
projectMgr := &project.Manager{}
rbacMgr := &rbac.Manager{}
robotMgr := &robot2.Manager{}
c := controller{robotMgr: robotMgr, rbacMgr: rbacMgr, proMgr: projectMgr}
ctx := context.TODO()
projectMgr.On("Get", mock.Anything, mock.Anything).Return(&models.Project{ProjectID: 1, Name: "library"}, nil)
robotMgr.On("List", mock.Anything, mock.Anything).Return([]*model.Robot{
{
Name: "test",
Description: "test list method",
ProjectID: 1,
Secret: utils.RandStringBytes(10),
},
}, nil)
rbacMgr.On("GetPermissionsByRole", mock.Anything, mock.Anything, mock.Anything).Return([]*rbac_model.UniversalRolePermission{
{
RoleType: ROBOTTYPE,
RoleID: 1,
Scope: "/project/1",
Resource: "repository",
Action: "pull",
},
{
RoleType: ROBOTTYPE,
RoleID: 1,
Scope: "/project/1",
Resource: "repository",
Action: "push",
},
}, nil)
projectMgr.On("Get", mock.Anything, mock.Anything).Return(&models.Project{ProjectID: 1, Name: "library"}, nil)
rs, err := c.List(ctx, &q.Query{
Keywords: map[string]interface{}{
"name": "test3",
},
}, &Option{
WithPermission: true,
})
suite.Nil(err)
suite.Equal("project", rs[0].Permissions[0].Kind)
suite.Equal("library", rs[0].Permissions[0].Namespace)
suite.Equal("pull", rs[0].Permissions[0].Access[0].Action.String())
suite.Equal("project", rs[0].Level)
}
func (suite *ControllerTestSuite) TestToScope() {
projectMgr := &project.Manager{}
rbacMgr := &rbac.Manager{}
robotMgr := &robot2.Manager{}
c := controller{robotMgr: robotMgr, rbacMgr: rbacMgr, proMgr: projectMgr}
ctx := context.TODO()
projectMgr.On("Get", mock.Anything, mock.Anything).Return(&models.Project{ProjectID: 1, Name: "library"}, nil)
p := &Permission{
Kind: "system",
Namespace: "/",
}
scope, err := c.toScope(ctx, p)
suite.Nil(err)
suite.Equal("/system", scope)
p = &Permission{
Kind: "system",
Namespace: "&",
}
_, err = c.toScope(ctx, p)
suite.NotNil(err)
p = &Permission{
Kind: "project",
Namespace: "library",
}
scope, err = c.toScope(ctx, p)
suite.Nil(err)
suite.Equal("/project/1", scope)
p = &Permission{
Kind: "project",
Namespace: "*",
}
scope, err = c.toScope(ctx, p)
suite.Nil(err)
suite.Equal("/project/*", scope)
}
func TestControllerTestSuite(t *testing.T) {
suite.Run(t, &ControllerTestSuite{})
}

View File

@ -0,0 +1,50 @@
package robot
import (
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot2/model"
)
const (
// LEVELSYSTEM ...
LEVELSYSTEM = "system"
// LEVELPROJECT ...
LEVELPROJECT = "project"
// SCOPESYSTEM ...
SCOPESYSTEM = "/system"
// SCOPEALLPROJECT ...
SCOPEALLPROJECT = "/project/*"
// ROBOTTYPE ...
ROBOTTYPE = "robotaccount"
)
// Robot ...
type Robot struct {
model.Robot
ProjectName string
Level string
Permissions []*Permission `json:"permissions"`
}
// setLevel, 0 is a system level robot, others are project level.
func (r *Robot) setLevel() {
if r.ProjectID == 0 {
r.Level = LEVELSYSTEM
} else {
r.Level = LEVELPROJECT
}
}
// Permission ...
type Permission struct {
Kind string `json:"kind"`
Namespace string `json:"namespace"`
Access []*types.Policy `json:"access"`
}
// Option ...
type Option struct {
WithPermission bool
}

View File

@ -0,0 +1,34 @@
package robot
import (
"github.com/goharbor/harbor/src/pkg/robot2/model"
"github.com/stretchr/testify/suite"
"testing"
)
type ModelTestSuite struct {
suite.Suite
}
func (suite *ModelTestSuite) TestSetLevel() {
r := Robot{
Robot: model.Robot{
ProjectID: 0,
},
}
r.setLevel()
suite.Equal(LEVELSYSTEM, r.Level)
r = Robot{
Robot: model.Robot{
ProjectID: 1,
},
}
r.setLevel()
suite.Equal(LEVELPROJECT, r.Level)
}
func TestModelTestSuite(t *testing.T) {
suite.Run(t, &ModelTestSuite{})
}