enable robot to support create project (#15461)

1, for admin only, the system level robot should contains the project creation access.
2, for not admin only, the system level robot can create project.
3, for the project that created by system level robot, use the admin ID as the ownerID.

No path for project level robot to create project.

Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
Wang Yan 2021-08-24 09:34:02 +08:00 committed by GitHub
parent b73480ed0c
commit b9228096dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 148 additions and 41 deletions

View File

@ -31,6 +31,11 @@ type Robot struct {
Permissions []*Permission `json:"permissions"`
}
// IsSysLevel, true is a system level robot, others are project level.
func (r *Robot) IsSysLevel() bool {
return r.Level == LEVELSYSTEM
}
// setLevel, 0 is a system level robot, others are project level.
func (r *Robot) setLevel() {
if r.ProjectID == 0 {
@ -56,6 +61,11 @@ type Permission struct {
Scope string `json:"-"`
}
// IsCoverAll ...
func (p *Permission) IsCoverAll() bool {
return p.Scope == SCOPEALLPROJECT
}
// Option ...
type Option struct {
WithPermission bool

View File

@ -31,6 +31,24 @@ func (suite *ModelTestSuite) TestSetLevel() {
suite.Equal(LEVELPROJECT, r.Level)
}
func (suite *ModelTestSuite) TestIsSysLevel() {
r := Robot{
Robot: model.Robot{
ProjectID: 0,
},
}
r.setLevel()
suite.True(r.IsSysLevel())
r = Robot{
Robot: model.Robot{
ProjectID: 1,
},
}
r.setLevel()
suite.False(r.IsSysLevel())
}
func (suite *ModelTestSuite) TestSetEditable() {
r := Robot{
Robot: model.Robot{
@ -38,7 +56,7 @@ func (suite *ModelTestSuite) TestSetEditable() {
},
}
r.setEditable()
suite.Equal(false, r.Editable)
suite.False(r.Editable)
r = Robot{
Robot: model.Robot{
@ -66,7 +84,29 @@ func (suite *ModelTestSuite) TestSetEditable() {
},
}
r.setEditable()
suite.Equal(true, r.Editable)
suite.True(r.Editable)
}
func (suite *ModelTestSuite) TestIsCoverAll() {
p := &Permission{
Kind: "project",
Namespace: "library",
Access: []*types.Policy{
{
Resource: "repository",
Action: "push",
},
{
Resource: "repository",
Action: "pull",
},
},
Scope: "/project/*",
}
suite.True(p.IsCoverAll())
p.Scope = "/system"
suite.False(p.IsCoverAll())
}
func TestModelTestSuite(t *testing.T) {

View File

@ -18,14 +18,13 @@ import (
"context"
"fmt"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/pkg/member"
commonmodels "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/security/local"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/member"
"github.com/goharbor/harbor/src/pkg/oidc"
"github.com/goharbor/harbor/src/pkg/user"
"github.com/goharbor/harbor/src/pkg/user/models"
@ -45,7 +44,7 @@ type Controller interface {
// UpdatePassword ...
UpdatePassword(ctx context.Context, id int, password string) error
// List ...
List(ctx context.Context, query *q.Query) ([]*models.User, error)
List(ctx context.Context, query *q.Query, options ...models.Option) (models.Users, error)
// Create ...
Create(ctx context.Context, u *models.User) (int, error)
// Count ...
@ -185,8 +184,8 @@ func (c *controller) Delete(ctx context.Context, id int) error {
return c.mgr.Delete(ctx, id)
}
func (c *controller) List(ctx context.Context, query *q.Query) ([]*models.User, error) {
return c.mgr.List(ctx, query)
func (c *controller) List(ctx context.Context, query *q.Query, options ...models.Option) (models.Users, error) {
return c.mgr.List(ctx, query, options...)
}
func (c *controller) UpdatePassword(ctx context.Context, id int, password string) error {

View File

@ -17,14 +17,13 @@ package user
import (
"context"
"fmt"
"strings"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/user/dao"
"github.com/goharbor/harbor/src/pkg/user/models"
"strings"
)
var (
@ -39,7 +38,7 @@ type Manager interface {
// GetByName get user by username, it will return an error if the user does not exist
GetByName(ctx context.Context, username string) (*models.User, error)
// List users according to the query
List(ctx context.Context, query *q.Query) (models.Users, error)
List(ctx context.Context, query *q.Query, options ...models.Option) (models.Users, error)
// Count counts the number of users according to the query
Count(ctx context.Context, query *q.Query) (int64, error)
// Create creates the user, the password of input should be plaintext
@ -177,21 +176,20 @@ func (m *manager) GetByName(ctx context.Context, username string) (*models.User,
}
// List users according to the query
func (m *manager) List(ctx context.Context, query *q.Query) (models.Users, error) {
func (m *manager) List(ctx context.Context, query *q.Query, options ...models.Option) (models.Users, error) {
query = q.MustClone(query)
excludeAdmin := true
for key := range query.Keywords {
str := strings.ToLower(key)
if str == "user_id__in" {
excludeAdmin = false
options = append(options, models.WithDefaultAdmin())
break
} else if str == "user_id" {
excludeAdmin = false
options = append(options, models.WithDefaultAdmin())
break
}
}
if excludeAdmin {
// Exclude admin account when not filter by UserIDs, see https://github.com/goharbor/harbor/issues/2527
opts := models.NewOptions(options...)
if !opts.IncludeDefaultAdmin {
query.Keywords["user_id__gt"] = 1
}
return m.dao.List(ctx, query)

View File

@ -35,3 +35,25 @@ func (users Users) MapByUserID() map[int]*User {
return m
}
type Option func(*Options)
type Options struct {
IncludeDefaultAdmin bool
}
// WithDefaultAdmin set the IncludeAdmin = true
func WithDefaultAdmin() Option {
return func(o *Options) {
o.IncludeDefaultAdmin = true
}
}
// NewOptions ...
func NewOptions(options ...Option) *Options {
opts := &Options{}
for _, f := range options {
f(opts)
}
return opts
}

View File

@ -34,8 +34,8 @@ import (
"github.com/goharbor/harbor/src/controller/registry"
"github.com/goharbor/harbor/src/controller/repository"
"github.com/goharbor/harbor/src/controller/retention"
robotCtr "github.com/goharbor/harbor/src/controller/robot"
"github.com/goharbor/harbor/src/controller/scanner"
"github.com/goharbor/harbor/src/controller/user"
"github.com/goharbor/harbor/src/core/api"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/config"
@ -50,7 +50,7 @@ import (
"github.com/goharbor/harbor/src/pkg/quota/types"
"github.com/goharbor/harbor/src/pkg/retention/policy"
"github.com/goharbor/harbor/src/pkg/robot"
"github.com/goharbor/harbor/src/pkg/user"
userModels "github.com/goharbor/harbor/src/pkg/user/models"
"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/project"
@ -63,7 +63,7 @@ func newProjectAPI() *projectAPI {
return &projectAPI{
auditMgr: audit.Mgr,
metadataMgr: metadata.Mgr,
userMgr: user.Mgr,
userCtl: user.Ctl,
repositoryCtl: repository.Ctl,
projectCtl: project.Ctl,
memberMgr: member.Mgr,
@ -79,7 +79,7 @@ type projectAPI struct {
BaseAPI
auditMgr audit.Manager
metadataMgr metadata.Manager
userMgr user.Manager
userCtl user.Controller
repositoryCtl repository.Controller
projectCtl project.Controller
memberMgr member.Manager
@ -101,6 +101,10 @@ func (a *projectAPI) CreateProject(ctx context.Context, params operation.CreateP
}
secCtx, _ := security.FromContext(ctx)
if r, ok := secCtx.(*robotSec.SecurityContext); ok && !r.User().IsSysLevel() {
log.Errorf("Only system level robot can create project")
return a.SendError(ctx, errors.ForbiddenError(nil).WithMessage("Only system level robot can create project"))
}
if onlyAdmin && !(a.isSysAdmin(ctx, rbac.ActionCreate) || secCtx.IsSolutionUser()) {
log.Errorf("Only sys admin can create project")
return a.SendError(ctx, errors.ForbiddenError(nil).WithMessage("Only system admin can create project"))
@ -155,14 +159,32 @@ func (a *projectAPI) CreateProject(ctx context.Context, params operation.CreateP
}
var ownerID int
// TODO: revise the ownerID in project model.
// set the owner as the system admin when the API being called by replication
// it's a solution to workaround the restriction of project creation API:
// only normal users can create projects
if secCtx.IsSolutionUser() {
ownerID = 1
// Remove the assumption of user id 1 is the system admin. And use the minimum system admin id as the owner ID,
// in most case, it's 1
if _, ok := secCtx.(*robotSec.SecurityContext); ok || secCtx.IsSolutionUser() {
q := &q.Query{
Keywords: map[string]interface{}{
"sysadmin_flag": true,
},
Sorts: []*q.Sort{
q.NewSort("user_id", false),
},
}
admins, err := a.userCtl.List(ctx, q, userModels.WithDefaultAdmin())
if err != nil {
return a.SendError(ctx, err)
}
if len(admins) == 0 {
return a.SendError(ctx, errors.New(nil).WithMessage("cannot create project as no system admin found"))
}
ownerID = admins[0].UserID
} else {
ownerName := secCtx.GetUsername()
user, err := a.userMgr.GetByName(ctx, ownerName)
user, err := a.userCtl.GetByName(ctx, ownerName)
if err != nil {
return a.SendError(ctx, err)
}
@ -422,7 +444,7 @@ func (a *projectAPI) ListProjects(ctx context.Context, params operation.ListProj
var coverAll bool
var names []string
for _, p := range r.User().Permissions {
if p.Scope == robotCtr.SCOPEALLPROJECT {
if p.IsCoverAll() {
coverAll = true
break
}

View File

@ -11,6 +11,8 @@ import (
q "github.com/goharbor/harbor/src/lib/q"
user "github.com/goharbor/harbor/src/controller/user"
usermodels "github.com/goharbor/harbor/src/pkg/user/models"
)
// Controller is an autogenerated mock type for the Controller type
@ -143,22 +145,29 @@ func (_m *Controller) GetBySubIss(ctx context.Context, sub string, iss string) (
return r0, r1
}
// List provides a mock function with given fields: ctx, query
func (_m *Controller) List(ctx context.Context, query *q.Query) ([]*models.User, error) {
ret := _m.Called(ctx, query)
// List provides a mock function with given fields: ctx, query, options
func (_m *Controller) List(ctx context.Context, query *q.Query, options ...usermodels.Option) (usermodels.Users, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, query)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 []*models.User
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*models.User); ok {
r0 = rf(ctx, query)
var r0 usermodels.Users
if rf, ok := ret.Get(0).(func(context.Context, *q.Query, ...usermodels.Option) usermodels.Users); ok {
r0 = rf(ctx, query, options...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.User)
r0 = ret.Get(0).(usermodels.Users)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
if rf, ok := ret.Get(1).(func(context.Context, *q.Query, ...usermodels.Option) error); ok {
r1 = rf(ctx, query, options...)
} else {
r1 = ret.Error(1)
}

View File

@ -120,13 +120,20 @@ func (_m *Manager) GetByName(ctx context.Context, username string) (*models.User
return r0, r1
}
// List provides a mock function with given fields: ctx, query
func (_m *Manager) List(ctx context.Context, query *q.Query) (usermodels.Users, error) {
ret := _m.Called(ctx, query)
// List provides a mock function with given fields: ctx, query, options
func (_m *Manager) List(ctx context.Context, query *q.Query, options ...usermodels.Option) (usermodels.Users, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, query)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 usermodels.Users
if rf, ok := ret.Get(0).(func(context.Context, *q.Query) usermodels.Users); ok {
r0 = rf(ctx, query)
if rf, ok := ret.Get(0).(func(context.Context, *q.Query, ...usermodels.Option) usermodels.Users); ok {
r0 = rf(ctx, query, options...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(usermodels.Users)
@ -134,8 +141,8 @@ func (_m *Manager) List(ctx context.Context, query *q.Query) (usermodels.Users,
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok {
r1 = rf(ctx, query)
if rf, ok := ret.Get(1).(func(context.Context, *q.Query, ...usermodels.Option) error); ok {
r1 = rf(ctx, query, options...)
} else {
r1 = ret.Error(1)
}