mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-26 10:38:00 +01:00
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:
parent
b73480ed0c
commit
b9228096dc
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user