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:
Wang Yan 2021-08-17 16:35:36 +08:00 committed by GitHub
parent eabff82366
commit 14f7274989
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 204 additions and 81 deletions

View File

@ -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))
}
})

View File

@ -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))

View File

@ -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:

View File

@ -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)
}

View File

@ -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

View File

@ -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
}