mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-13 19:21:56 +01:00
support robot to list project (#15431)
1, add permission check for API of List Projects 2, add permission check for API of List Repositories 3, use the self defined query to handle both names and public query Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
parent
eabff82366
commit
14f7274989
@ -16,6 +16,7 @@ package robot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
rbac_project "github.com/goharbor/harbor/src/common/rbac/project"
|
||||
"github.com/goharbor/harbor/src/common/rbac/system"
|
||||
"github.com/goharbor/harbor/src/controller/robot"
|
||||
@ -27,26 +28,21 @@ import (
|
||||
"github.com/goharbor/harbor/src/pkg/permission/evaluator"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
"github.com/goharbor/harbor/src/pkg/project/models"
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
)
|
||||
|
||||
// SecurityContext implements security.Context interface based on database
|
||||
type SecurityContext struct {
|
||||
robot *model.Robot
|
||||
isSystemLevel bool
|
||||
ctl project.Controller
|
||||
policies []*types.Policy
|
||||
evaluator evaluator.Evaluator
|
||||
once sync.Once
|
||||
robot *robot.Robot
|
||||
ctl project.Controller
|
||||
evaluator evaluator.Evaluator
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// NewSecurityContext ...
|
||||
func NewSecurityContext(robot *model.Robot, isSystemLevel bool, policy []*types.Policy) *SecurityContext {
|
||||
func NewSecurityContext(r *robot.Robot) *SecurityContext {
|
||||
return &SecurityContext{
|
||||
ctl: project.Ctl,
|
||||
robot: robot,
|
||||
policies: policy,
|
||||
isSystemLevel: isSystemLevel,
|
||||
ctl: project.Ctl,
|
||||
robot: r,
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,6 +65,11 @@ func (s *SecurityContext) GetUsername() string {
|
||||
return s.robot.Name
|
||||
}
|
||||
|
||||
// User get the current user
|
||||
func (s *SecurityContext) User() *robot.Robot {
|
||||
return s.robot
|
||||
}
|
||||
|
||||
// IsSysAdmin robot cannot be a system admin
|
||||
func (s *SecurityContext) IsSysAdmin() bool {
|
||||
return false
|
||||
@ -81,12 +82,27 @@ func (s *SecurityContext) IsSolutionUser() bool {
|
||||
|
||||
// Can returns whether the robot can do action on resource
|
||||
func (s *SecurityContext) Can(ctx context.Context, action types.Action, resource types.Resource) bool {
|
||||
if s.robot == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
s.once.Do(func() {
|
||||
if s.isSystemLevel {
|
||||
var accesses []*types.Policy
|
||||
for _, p := range s.robot.Permissions {
|
||||
for _, a := range p.Access {
|
||||
accesses = append(accesses, &types.Policy{
|
||||
Action: a.Action,
|
||||
Effect: a.Effect,
|
||||
Resource: types.Resource(fmt.Sprintf("%s/%s", p.Scope, a.Resource)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if s.robot.Level == robot.LEVELSYSTEM {
|
||||
var proPolicies []*types.Policy
|
||||
var sysPolicies []*types.Policy
|
||||
var evaluators evaluator.Evaluators
|
||||
for _, p := range s.policies {
|
||||
for _, p := range accesses {
|
||||
if strings.HasPrefix(p.Resource.String(), robot.SCOPESYSTEM) {
|
||||
sysPolicies = append(sysPolicies, p)
|
||||
} else if strings.HasPrefix(p.Resource.String(), robot.SCOPEPROJECT) {
|
||||
@ -101,7 +117,7 @@ func (s *SecurityContext) Can(ctx context.Context, action types.Action, resource
|
||||
s.evaluator = evaluators
|
||||
|
||||
} else {
|
||||
s.evaluator = rbac_project.NewEvaluator(s.ctl, rbac_project.NewBuilderForPolicies(s.GetUsername(), s.policies, filterRobotPolicies))
|
||||
s.evaluator = rbac_project.NewEvaluator(s.ctl, rbac_project.NewBuilderForPolicies(s.GetUsername(), accesses, filterRobotPolicies))
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/rbac/project"
|
||||
"github.com/goharbor/harbor/src/controller/robot"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@ -32,117 +33,165 @@ import (
|
||||
|
||||
var (
|
||||
private = &proModels.Project{
|
||||
Name: "testrobot",
|
||||
OwnerID: 1,
|
||||
ProjectID: 1,
|
||||
Name: "testrobot",
|
||||
OwnerID: 1,
|
||||
}
|
||||
)
|
||||
|
||||
func TestIsAuthenticated(t *testing.T) {
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil, false, nil)
|
||||
ctx := NewSecurityContext(nil)
|
||||
assert.False(t, ctx.IsAuthenticated())
|
||||
|
||||
// authenticated
|
||||
ctx = NewSecurityContext(&model.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
}, false, nil)
|
||||
ctx = NewSecurityContext(&robot.Robot{
|
||||
Robot: model.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
},
|
||||
})
|
||||
assert.True(t, ctx.IsAuthenticated())
|
||||
}
|
||||
|
||||
func TestGetUsername(t *testing.T) {
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil, false, nil)
|
||||
ctx := NewSecurityContext(nil)
|
||||
assert.Equal(t, "", ctx.GetUsername())
|
||||
|
||||
// authenticated
|
||||
ctx = NewSecurityContext(&model.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
}, false, nil)
|
||||
ctx = NewSecurityContext(&robot.Robot{
|
||||
Robot: model.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
},
|
||||
})
|
||||
assert.Equal(t, "test", ctx.GetUsername())
|
||||
}
|
||||
|
||||
func TestGetUser(t *testing.T) {
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil)
|
||||
assert.Equal(t, "", ctx.GetUsername())
|
||||
|
||||
// authenticated
|
||||
ctx = NewSecurityContext(&robot.Robot{
|
||||
Robot: model.Robot{
|
||||
ID: 123,
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
},
|
||||
})
|
||||
assert.Equal(t, "test", ctx.User().Name)
|
||||
assert.Equal(t, int64(123), ctx.User().ID)
|
||||
}
|
||||
|
||||
func TestIsSysAdmin(t *testing.T) {
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil, false, nil)
|
||||
ctx := NewSecurityContext(nil)
|
||||
assert.False(t, ctx.IsSysAdmin())
|
||||
|
||||
// authenticated, non admin
|
||||
ctx = NewSecurityContext(&model.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
}, false, nil)
|
||||
ctx = NewSecurityContext(&robot.Robot{
|
||||
Robot: model.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
},
|
||||
})
|
||||
assert.False(t, ctx.IsSysAdmin())
|
||||
}
|
||||
|
||||
func TestIsSolutionUser(t *testing.T) {
|
||||
ctx := NewSecurityContext(nil, false, nil)
|
||||
ctx := NewSecurityContext(nil)
|
||||
assert.False(t, ctx.IsSolutionUser())
|
||||
}
|
||||
|
||||
func TestHasPullPerm(t *testing.T) {
|
||||
policies := []*types.Policy{
|
||||
{
|
||||
Resource: rbac.Resource(fmt.Sprintf("/project/%d/repository", private.ProjectID)),
|
||||
Action: rbac.ActionPull,
|
||||
robot := &robot.Robot{
|
||||
Robot: model.Robot{
|
||||
Name: "test_robot_1",
|
||||
Description: "desc",
|
||||
},
|
||||
Permissions: []*robot.Permission{
|
||||
{
|
||||
Kind: "project",
|
||||
Namespace: "library",
|
||||
Access: []*types.Policy{
|
||||
{
|
||||
Resource: rbac.Resource(fmt.Sprintf("project/%d/repository", private.ProjectID)),
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
robot := &model.Robot{
|
||||
Name: "test_robot_1",
|
||||
Description: "desc",
|
||||
}
|
||||
|
||||
ctl := &projecttesting.Controller{}
|
||||
mock.OnAnything(ctl, "Get").Return(private, nil)
|
||||
|
||||
ctx := NewSecurityContext(robot, false, policies)
|
||||
ctx := NewSecurityContext(robot)
|
||||
ctx.ctl = ctl
|
||||
resource := project.NewNamespace(private.ProjectID).Resource(rbac.ResourceRepository)
|
||||
assert.True(t, ctx.Can(context.TODO(), rbac.ActionPull, resource))
|
||||
}
|
||||
|
||||
func TestHasPushPerm(t *testing.T) {
|
||||
policies := []*types.Policy{
|
||||
{
|
||||
Resource: rbac.Resource(fmt.Sprintf("/project/%d/repository", private.ProjectID)),
|
||||
Action: rbac.ActionPush,
|
||||
robot := &robot.Robot{
|
||||
Robot: model.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
},
|
||||
Permissions: []*robot.Permission{
|
||||
{
|
||||
Kind: "project",
|
||||
Namespace: "library",
|
||||
Access: []*types.Policy{
|
||||
{
|
||||
Resource: rbac.Resource(fmt.Sprintf("project/%d/repository", private.ProjectID)),
|
||||
Action: rbac.ActionPush,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
robot := &model.Robot{
|
||||
Name: "test_robot_2",
|
||||
Description: "desc",
|
||||
}
|
||||
|
||||
ctl := &projecttesting.Controller{}
|
||||
mock.OnAnything(ctl, "Get").Return(private, nil)
|
||||
|
||||
ctx := NewSecurityContext(robot, false, policies)
|
||||
ctx := NewSecurityContext(robot)
|
||||
ctx.ctl = ctl
|
||||
resource := project.NewNamespace(private.ProjectID).Resource(rbac.ResourceRepository)
|
||||
assert.True(t, ctx.Can(context.TODO(), rbac.ActionPush, resource))
|
||||
}
|
||||
|
||||
func TestHasPushPullPerm(t *testing.T) {
|
||||
policies := []*types.Policy{
|
||||
{
|
||||
Resource: rbac.Resource(fmt.Sprintf("/project/%d/repository", private.ProjectID)),
|
||||
Action: rbac.ActionPush,
|
||||
robot := &robot.Robot{
|
||||
Robot: model.Robot{
|
||||
Name: "test_robot_3",
|
||||
Description: "desc",
|
||||
},
|
||||
{
|
||||
Resource: rbac.Resource(fmt.Sprintf("/project/%d/repository", private.ProjectID)),
|
||||
Action: rbac.ActionPull,
|
||||
Permissions: []*robot.Permission{
|
||||
{
|
||||
Kind: "project",
|
||||
Namespace: "library",
|
||||
Access: []*types.Policy{
|
||||
{
|
||||
Resource: rbac.Resource(fmt.Sprintf("project/%d/repository", private.ProjectID)),
|
||||
Action: rbac.ActionPush,
|
||||
},
|
||||
{
|
||||
Resource: rbac.Resource(fmt.Sprintf("project/%d/repository", private.ProjectID)),
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
robot := &model.Robot{
|
||||
Name: "test_robot_3",
|
||||
Description: "desc",
|
||||
}
|
||||
|
||||
ctl := &projecttesting.Controller{}
|
||||
mock.OnAnything(ctl, "Get").Return(private, nil)
|
||||
|
||||
ctx := NewSecurityContext(robot, false, policies)
|
||||
ctx := NewSecurityContext(robot)
|
||||
ctx.ctl = ctl
|
||||
resource := project.NewNamespace(private.ProjectID).Resource(rbac.ResourceRepository)
|
||||
assert.True(t, ctx.Can(context.TODO(), rbac.ActionPush, resource) && ctx.Can(context.TODO(), rbac.ActionPull, resource))
|
||||
|
@ -56,6 +56,12 @@ type Project struct {
|
||||
RegistryID int64 `orm:"column(registry_id)" json:"registry_id"`
|
||||
}
|
||||
|
||||
// NamesQuery ...
|
||||
type NamesQuery struct {
|
||||
Names []string // the names of project
|
||||
WithPublic bool // include the public projects
|
||||
}
|
||||
|
||||
// GetMetadata ...
|
||||
func (p *Project) GetMetadata(key string) (string, bool) {
|
||||
if len(p.Metadata) == 0 {
|
||||
@ -182,6 +188,30 @@ func (p *Project) FilterByMember(ctx context.Context, qs orm.QuerySeter, key str
|
||||
return qs.FilterRaw("project_id", fmt.Sprintf("IN (%s)", subQuery))
|
||||
}
|
||||
|
||||
// FilterByNames returns orm.QuerySeter with name filter
|
||||
func (p *Project) FilterByNames(ctx context.Context, qs orm.QuerySeter, key string, value interface{}) orm.QuerySeter {
|
||||
query, ok := value.(*NamesQuery)
|
||||
if !ok {
|
||||
return qs
|
||||
}
|
||||
|
||||
if len(query.Names) == 0 {
|
||||
return qs
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, v := range query.Names {
|
||||
names = append(names, `'`+v+`'`)
|
||||
}
|
||||
subQuery := fmt.Sprintf("SELECT project_id FROM project where name IN (%s)", strings.Join(names, ","))
|
||||
|
||||
if query.WithPublic {
|
||||
subQuery = fmt.Sprintf("(%s) UNION (SELECT project_id FROM project_metadata WHERE name = 'public' AND value = 'true')", subQuery)
|
||||
}
|
||||
|
||||
return qs.FilterRaw("project_id", fmt.Sprintf("IN (%s)", subQuery))
|
||||
}
|
||||
|
||||
func isTrue(i interface{}) bool {
|
||||
switch value := i.(type) {
|
||||
case bool:
|
||||
|
@ -15,7 +15,6 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
robotCtx "github.com/goharbor/harbor/src/common/security/robot"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
@ -23,11 +22,9 @@ import (
|
||||
"github.com/goharbor/harbor/src/lib/config"
|
||||
"github.com/goharbor/harbor/src/lib/log"
|
||||
"github.com/goharbor/harbor/src/lib/q"
|
||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@ -71,20 +68,6 @@ func (r *robot) Generate(req *http.Request) security.Context {
|
||||
return nil
|
||||
}
|
||||
|
||||
var accesses []*types.Policy
|
||||
for _, p := range robot.Permissions {
|
||||
for _, a := range p.Access {
|
||||
accesses = append(accesses, &types.Policy{
|
||||
Action: a.Action,
|
||||
Effect: a.Effect,
|
||||
Resource: types.Resource(fmt.Sprintf("%s/%s", p.Scope, a.Resource)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
modelRobot := &model.Robot{
|
||||
Name: name,
|
||||
}
|
||||
log.Infof("a robot security context generated for request %s %s", req.Method, req.URL.Path)
|
||||
return robotCtx.NewSecurityContext(modelRobot, robot.Level == robot_ctl.LEVELSYSTEM, accesses)
|
||||
return robotCtx.NewSecurityContext(robot)
|
||||
}
|
||||
|
@ -27,12 +27,14 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/security"
|
||||
"github.com/goharbor/harbor/src/common/security/local"
|
||||
robotSec "github.com/goharbor/harbor/src/common/security/robot"
|
||||
"github.com/goharbor/harbor/src/controller/p2p/preheat"
|
||||
"github.com/goharbor/harbor/src/controller/project"
|
||||
"github.com/goharbor/harbor/src/controller/quota"
|
||||
"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/core/api"
|
||||
"github.com/goharbor/harbor/src/lib"
|
||||
@ -44,6 +46,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/pkg/audit"
|
||||
"github.com/goharbor/harbor/src/pkg/member"
|
||||
"github.com/goharbor/harbor/src/pkg/project/metadata"
|
||||
pkgModels "github.com/goharbor/harbor/src/pkg/project/models"
|
||||
"github.com/goharbor/harbor/src/pkg/quota/types"
|
||||
"github.com/goharbor/harbor/src/pkg/retention/policy"
|
||||
"github.com/goharbor/harbor/src/pkg/robot"
|
||||
@ -414,6 +417,26 @@ func (a *projectAPI) ListProjects(ctx context.Context, params operation.ListProj
|
||||
}
|
||||
|
||||
query.Keywords["member"] = member
|
||||
} else if r, ok := secCtx.(*robotSec.SecurityContext); ok {
|
||||
// for the system level robot that covers all the project, see it as the system admin.
|
||||
var coverAll bool
|
||||
var names []string
|
||||
for _, p := range r.User().Permissions {
|
||||
if p.Scope == robotCtr.SCOPEALLPROJECT {
|
||||
coverAll = true
|
||||
break
|
||||
}
|
||||
names = append(names, p.Namespace)
|
||||
}
|
||||
if !coverAll {
|
||||
namesQuery := &pkgModels.NamesQuery{
|
||||
Names: names,
|
||||
}
|
||||
if public, ok := query.Keywords["public"]; !ok || lib.ToBool(public) {
|
||||
namesQuery.WithPublic = true
|
||||
}
|
||||
query.Keywords["names"] = namesQuery
|
||||
}
|
||||
} else {
|
||||
// can't get the user info, force to return public projects
|
||||
query.Keywords["public"] = true
|
||||
|
@ -17,6 +17,9 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/security/robot"
|
||||
robotCtr "github.com/goharbor/harbor/src/controller/robot"
|
||||
pkgModels "github.com/goharbor/harbor/src/pkg/project/models"
|
||||
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
@ -123,6 +126,25 @@ func (r *repositoryAPI) listAuthorizedProjectIDs(ctx context.Context) ([]int64,
|
||||
GroupIDs: currentUser.GroupIDs,
|
||||
WithPublic: true,
|
||||
}
|
||||
case *robot.SecurityContext:
|
||||
// for the system level robot that covers all the project, see it as the system admin.
|
||||
var coverAll bool
|
||||
var names []string
|
||||
r := secCtx.(*robot.SecurityContext).User()
|
||||
for _, p := range r.Permissions {
|
||||
if p.Scope == robotCtr.SCOPEALLPROJECT {
|
||||
coverAll = true
|
||||
break
|
||||
}
|
||||
names = append(names, p.Namespace)
|
||||
}
|
||||
if !coverAll {
|
||||
namesQuery := &pkgModels.NamesQuery{
|
||||
Names: names,
|
||||
WithPublic: true,
|
||||
}
|
||||
query.Keywords["names"] = namesQuery
|
||||
}
|
||||
default:
|
||||
query.Keywords["public"] = true
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user