refactor(rbac): refactor rbac impl to improve performance (#9988)

1. Introduce `Evaluator` interface which do the permission checking.
2. `admin`, `lazy`, `rbac`, `namespace` and `evaluartor` set are implemented the
`Evaluator` interface.
3. Move project rbac implemention from `project` to `rbac` pkg to reduce
the name  conflict with project instance of model.
4. Do permission checking in security context by `Evaluator`.
5. Cache the regexp in rbac evaluator for casbin.
6. Cache evaluator in namespace evaluator to improve performance.

Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
He Weiwei 2020-03-12 23:42:53 +08:00 committed by GitHub
parent 8ffa79801b
commit 2a243ef7a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 2161 additions and 1941 deletions

View File

@ -30,6 +30,7 @@ import (
ierror "github.com/goharbor/harbor/src/internal/error"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot"
"github.com/goharbor/harbor/src/pkg/robot/model"
sca "github.com/goharbor/harbor/src/pkg/scan"
@ -522,7 +523,7 @@ func (bc *basicController) makeRobotAccount(projectID int64, repository string)
Name: UUID,
Description: "for scan",
ProjectID: projectID,
Access: []*rbac.Policy{{Resource: resource, Action: rbac.ActionScannerPull}},
Access: []*types.Policy{{Resource: resource, Action: rbac.ActionScannerPull}},
}
rb, err := bc.rc.CreateRobotAccount(robotReq)

View File

@ -29,6 +29,7 @@ import (
jm "github.com/goharbor/harbor/src/common/job/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/robot/model"
sca "github.com/goharbor/harbor/src/pkg/scan"
@ -167,8 +168,8 @@ func (suite *ControllerTestSuite) SetupSuite() {
rc := &MockRobotController{}
resource := fmt.Sprintf("/project/%d/repository", suite.artifact.ProjectID)
access := []*rbac.Policy{{
Resource: rbac.Resource(resource),
access := []*types.Policy{{
Resource: types.Resource(resource),
Action: rbac.ActionScannerPull,
}}

View File

@ -1,168 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/casbin/casbin"
"github.com/casbin/casbin/model"
"github.com/casbin/casbin/persist"
"github.com/casbin/casbin/util"
)
var (
errNotImplemented = errors.New("Not implemented")
)
// Syntax for models see https://casbin.org/docs/en/syntax-for-models
const modelText = `
# Request definition
[request_definition]
r = sub, obj, act
# Policy definition
[policy_definition]
p = sub, obj, act, eft
# Role definition
[role_definition]
g = _, _
# Policy effect
[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
# Matchers
[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && (r.act == p.act || p.act == '*')
`
// keyMatch2 determines whether key1 matches the pattern of key2, its behavior most likely the builtin KeyMatch2
// except that the match of ("/project/1/robot", "/project/1") will return false
func keyMatch2(key1 string, key2 string) bool {
key2 = strings.Replace(key2, "/*", "/.*", -1)
re := regexp.MustCompile(`(.*):[^/]+(.*)`)
for {
if !strings.Contains(key2, "/:") {
break
}
key2 = re.ReplaceAllString(key2, "$1[^/]+$2")
}
return util.RegexMatch(key1, "^"+key2+"$")
}
func keyMatch2Func(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)
name2 := args[1].(string)
return bool(keyMatch2(name1, name2)), nil
}
type userAdapter struct {
User
}
func (a *userAdapter) getRolePolicyLines(role Role) []string {
lines := []string{}
roleName := role.GetRoleName()
// returns empty policy lines if role name is empty
if roleName == "" {
return lines
}
for _, policy := range role.GetPolicies() {
line := fmt.Sprintf("p, %s, %s, %s, %s", roleName, policy.Resource, policy.Action, policy.GetEffect())
lines = append(lines, line)
}
return lines
}
func (a *userAdapter) getUserPolicyLines() []string {
lines := []string{}
username := a.GetUserName()
// returns empty policy lines if username is empty
if username == "" {
return lines
}
for _, policy := range a.GetPolicies() {
line := fmt.Sprintf("p, %s, %s, %s, %s", username, policy.Resource, policy.Action, policy.GetEffect())
lines = append(lines, line)
}
return lines
}
func (a *userAdapter) getUserAllPolicyLines() []string {
lines := []string{}
username := a.GetUserName()
// returns empty policy lines if username is empty
if username == "" {
return lines
}
lines = append(lines, a.getUserPolicyLines()...)
for _, role := range a.GetRoles() {
lines = append(lines, a.getRolePolicyLines(role)...)
lines = append(lines, fmt.Sprintf("g, %s, %s", username, role.GetRoleName()))
}
return lines
}
func (a *userAdapter) LoadPolicy(model model.Model) error {
for _, line := range a.getUserAllPolicyLines() {
persist.LoadPolicyLine(line, model)
}
return nil
}
func (a *userAdapter) SavePolicy(model model.Model) error {
return errNotImplemented
}
func (a *userAdapter) AddPolicy(sec string, ptype string, rule []string) error {
return errNotImplemented
}
func (a *userAdapter) RemovePolicy(sec string, ptype string, rule []string) error {
return errNotImplemented
}
func (a *userAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
return errNotImplemented
}
func enforcerForUser(user User) *casbin.Enforcer {
m := model.Model{}
m.LoadModelFromText(modelText)
e := casbin.NewEnforcer(m, &userAdapter{User: user})
e.AddFunction("keyMatch2", keyMatch2Func)
return e
}

View File

@ -1,61 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"fmt"
)
// Namespace the namespace interface
type Namespace interface {
// Kind returns the kind of namespace
Kind() string
// Resource returns new resource for subresources with the namespace
Resource(subresources ...Resource) Resource
// Identity returns identity attached with namespace
Identity() interface{}
// IsPublic returns true if namespace is public
IsPublic() bool
}
type projectNamespace struct {
projectID int64
isPublic bool
}
func (ns *projectNamespace) Kind() string {
return "project"
}
func (ns *projectNamespace) Resource(subresources ...Resource) Resource {
return Resource(fmt.Sprintf("/project/%d", ns.projectID)).Subresource(subresources...)
}
func (ns *projectNamespace) Identity() interface{} {
return ns.projectID
}
func (ns *projectNamespace) IsPublic() bool {
return ns.isPublic
}
// NewProjectNamespace returns namespace for project
func NewProjectNamespace(projectID int64, isPublic ...bool) Namespace {
isPublicNamespace := false
if len(isPublic) > 0 {
isPublicNamespace = isPublic[0]
}
return &projectNamespace{projectID: projectID, isPublic: isPublicNamespace}
}

View File

@ -1,42 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"testing"
"github.com/stretchr/testify/suite"
)
type ProjectNamespaceTestSuite struct {
suite.Suite
}
func (suite *ProjectNamespaceTestSuite) TestResource() {
var namespace Namespace
namespace = &projectNamespace{projectID: int64(1)}
suite.Equal(namespace.Resource(Resource("image")), Resource("/project/1/image"))
}
func (suite *ProjectNamespaceTestSuite) TestIdentity() {
namespace, _ := Resource("/project/1/image").GetNamespace()
suite.Equal(namespace.Identity(), int64(1))
}
func TestProjectNamespaceTestSuite(t *testing.T) {
suite.Run(t, new(ProjectNamespaceTestSuite))
}

View File

@ -1,46 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"errors"
"regexp"
"strconv"
)
var (
namespaceParsers = map[string]namespaceParser{
"project": projectNamespaceParser,
}
)
type namespaceParser func(resource Resource) (Namespace, error)
func projectNamespaceParser(resource Resource) (Namespace, error) {
parserRe := regexp.MustCompile("^/project/([^/]*)/?")
matches := parserRe.FindStringSubmatch(resource.String())
if len(matches) <= 1 {
return nil, errors.New("not support resource")
}
projectID, err := strconv.ParseInt(matches[1], 10, 64)
if err != nil {
return nil, err
}
return &projectNamespace{projectID: projectID}, nil
}

View File

@ -1,105 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"github.com/goharbor/harbor/src/common/rbac"
)
var (
// subresource policies for public project
publicProjectPolicies = []*rbac.Policy{
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionList},
{Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead},
}
// all policies for the projects
allPolicies = computeAllPolicies()
)
// PoliciesForPublicProject ...
func PoliciesForPublicProject(namespace rbac.Namespace) []*rbac.Policy {
policies := []*rbac.Policy{}
for _, policy := range publicProjectPolicies {
policies = append(policies, &rbac.Policy{
Resource: namespace.Resource(policy.Resource),
Action: policy.Action,
Effect: policy.Effect,
})
}
return policies
}
// GetAllPolicies returns all policies for namespace of the project
func GetAllPolicies(namespace rbac.Namespace) []*rbac.Policy {
policies := []*rbac.Policy{}
for _, policy := range allPolicies {
policies = append(policies, &rbac.Policy{
Resource: namespace.Resource(policy.Resource),
Action: policy.Action,
Effect: policy.Effect,
})
}
return policies
}
func computeAllPolicies() []*rbac.Policy {
var results []*rbac.Policy
mp := map[string]bool{}
for _, policies := range rolePoliciesMap {
for _, policy := range policies {
if !mp[policy.String()] {
results = append(results, policy)
mp[policy.String()] = true
}
}
}
return results
}

View File

@ -1,83 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"github.com/goharbor/harbor/src/common/rbac"
)
// visitorContext the context interface for the project visitor
type visitorContext interface {
IsAuthenticated() bool
// GetUsername returns the username of user related to the context
GetUsername() string
// IsSysAdmin returns whether the user is system admin
IsSysAdmin() bool
}
// visitor implement the rbac.User interface for project visitor
type visitor struct {
ctx visitorContext
namespace rbac.Namespace
projectRoles []int
}
// GetUserName returns username of the visitor
func (v *visitor) GetUserName() string {
// anonymous username for unauthenticated Visitor
if !v.ctx.IsAuthenticated() {
return "anonymous"
}
return v.ctx.GetUsername()
}
// GetPolicies returns policies of the visitor
func (v *visitor) GetPolicies() []*rbac.Policy {
if v.ctx.IsSysAdmin() {
return GetAllPolicies(v.namespace)
}
if v.namespace.IsPublic() {
return PoliciesForPublicProject(v.namespace)
}
return nil
}
// GetRoles returns roles of the visitor
func (v *visitor) GetRoles() []rbac.Role {
// Ignore roles when visitor is anonymous or system admin
if !v.ctx.IsAuthenticated() || v.ctx.IsSysAdmin() {
return nil
}
roles := []rbac.Role{}
for _, roleID := range v.projectRoles {
roles = append(roles, &visitorRole{roleID: roleID, namespace: v.namespace})
}
return roles
}
// NewUser returns rbac.User interface for the project visitor
func NewUser(ctx visitorContext, namespace rbac.Namespace, projectRoles ...int) rbac.User {
return &visitor{
ctx: ctx,
namespace: namespace,
projectRoles: projectRoles,
}
}

View File

@ -1,435 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/rbac"
)
var (
rolePoliciesMap = map[string][]*rbac.Policy{
"projectAdmin": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceSelf, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceSelf, Action: rbac.ActionDelete},
{Resource: rbac.ResourceMember, Action: rbac.ActionCreate},
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceMember, Action: rbac.ActionDelete},
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionRead},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionDelete},
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
{Resource: rbac.ResourceReplication, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplication, Action: rbac.ActionList},
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionList},
{Resource: rbac.ResourceLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceLabelResource, Action: rbac.ActionList},
{Resource: rbac.ResourceQuota, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceRepository, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPush},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionCreate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionRead},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionDelete},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionList},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate}, // upload helm chart
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, // download helm chart
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate}, // upload helm chart version
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead}, // read and download helm chart version
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceRobot, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceRobot, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionCreate},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionDelete},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionList},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionRead},
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionDelete},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionList},
{Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead},
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionDelete},
},
"master": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionRead},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceMetadata, Action: rbac.ActionDelete},
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
{Resource: rbac.ResourceQuota, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplication, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplication, Action: rbac.ActionList},
{Resource: rbac.ResourceLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceRepository, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPush},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionCreate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionRead},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionDelete},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionList},
{Resource: rbac.ResourceTagRetention, Action: rbac.ActionOperate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceImmutableTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
{Resource: rbac.ResourceNotificationPolicy, Action: rbac.ActionList},
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionDelete},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionList},
{Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead},
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceTag, Action: rbac.ActionDelete},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionDelete},
},
"developer": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceQuota, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPush},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete},
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionList},
{Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead},
{Resource: rbac.ResourceTag, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionCreate},
{Resource: rbac.ResourceArtifactLabel, Action: rbac.ActionDelete},
},
"guest": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionRead},
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceQuota, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionList},
{Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead},
},
"limitedGuest": {
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
{Resource: rbac.ResourceQuota, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionRead},
{Resource: rbac.ResourceArtifact, Action: rbac.ActionList},
{Resource: rbac.ResourceArtifactAddition, Action: rbac.ActionRead},
},
}
)
// visitorRole implement the rbac.Role interface
type visitorRole struct {
namespace rbac.Namespace
roleID int
}
// GetRoleName returns role name for the visitor role
func (role *visitorRole) GetRoleName() string {
switch role.roleID {
case common.RoleProjectAdmin:
return "projectAdmin"
case common.RoleMaster:
return "master"
case common.RoleDeveloper:
return "developer"
case common.RoleGuest:
return "guest"
case common.RoleLimitedGuest:
return "limitedGuest"
default:
return ""
}
}
// GetPolicies returns policies for the visitor role
func (role *visitorRole) GetPolicies() []*rbac.Policy {
policies := []*rbac.Policy{}
roleName := role.GetRoleName()
if roleName == "" {
return policies
}
for _, policy := range rolePoliciesMap[roleName] {
policies = append(policies, &rbac.Policy{
Resource: role.namespace.Resource(policy.Resource),
Action: policy.Action,
Effect: policy.Effect,
})
}
return policies
}

View File

@ -1,47 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/stretchr/testify/suite"
)
type VisitorRoleTestSuite struct {
suite.Suite
}
func (suite *VisitorRoleTestSuite) TestGetRoleName() {
projectAdmin := visitorRole{roleID: common.RoleProjectAdmin}
suite.Equal(projectAdmin.GetRoleName(), "projectAdmin")
developer := visitorRole{roleID: common.RoleDeveloper}
suite.Equal(developer.GetRoleName(), "developer")
guest := visitorRole{roleID: common.RoleGuest}
suite.Equal(guest.GetRoleName(), "guest")
limitedGuest := visitorRole{roleID: common.RoleLimitedGuest}
suite.Equal(limitedGuest.GetRoleName(), "limitedGuest")
unknow := visitorRole{roleID: 404}
suite.Equal(unknow.GetRoleName(), "")
}
func TestVisitorRoleTestSuite(t *testing.T) {
suite.Run(t, new(VisitorRoleTestSuite))
}

View File

@ -1,93 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/stretchr/testify/suite"
)
type fakeVisitorContext struct {
username string
isSysAdmin bool
}
func (ctx *fakeVisitorContext) IsAuthenticated() bool {
return ctx.username != ""
}
func (ctx *fakeVisitorContext) GetUsername() string {
return ctx.username
}
func (ctx *fakeVisitorContext) IsSysAdmin() bool {
return ctx.IsAuthenticated() && ctx.isSysAdmin
}
var (
anonymousCtx = &fakeVisitorContext{}
authenticatedCtx = &fakeVisitorContext{username: "user"}
sysAdminCtx = &fakeVisitorContext{username: "admin", isSysAdmin: true}
)
type VisitorTestSuite struct {
suite.Suite
}
func (suite *VisitorTestSuite) TestGetPolicies() {
namespace := rbac.NewProjectNamespace(1, false)
publicNamespace := rbac.NewProjectNamespace(1, true)
anonymous := NewUser(anonymousCtx, namespace)
suite.Nil(anonymous.GetPolicies())
anonymousForPublicProject := NewUser(anonymousCtx, publicNamespace)
suite.Equal(anonymousForPublicProject.GetPolicies(), PoliciesForPublicProject(publicNamespace))
authenticated := NewUser(authenticatedCtx, namespace)
suite.Nil(authenticated.GetPolicies())
authenticatedForPublicProject := NewUser(authenticatedCtx, publicNamespace)
suite.Equal(authenticatedForPublicProject.GetPolicies(), PoliciesForPublicProject(publicNamespace))
systemAdmin := NewUser(sysAdminCtx, namespace)
suite.Equal(systemAdmin.GetPolicies(), GetAllPolicies(namespace))
systemAdminForPublicProject := NewUser(sysAdminCtx, publicNamespace)
suite.Equal(systemAdminForPublicProject.GetPolicies(), GetAllPolicies(publicNamespace))
}
func (suite *VisitorTestSuite) TestGetRoles() {
namespace := rbac.NewProjectNamespace(1, false)
anonymous := NewUser(anonymousCtx, namespace)
suite.Nil(anonymous.GetRoles())
authenticated := NewUser(authenticatedCtx, namespace)
suite.Empty(authenticated.GetRoles())
authenticated = NewUser(authenticatedCtx, namespace, common.RoleProjectAdmin)
suite.Len(authenticated.GetRoles(), 1)
authenticated = NewUser(authenticatedCtx, namespace, common.RoleProjectAdmin, common.RoleDeveloper)
suite.Len(authenticated.GetRoles(), 2)
}
func TestVisitorTestSuite(t *testing.T) {
suite.Run(t, new(VisitorTestSuite))
}

View File

@ -0,0 +1,81 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/pkg/permission/evaluator"
"github.com/goharbor/harbor/src/pkg/permission/evaluator/namespace"
"github.com/goharbor/harbor/src/pkg/permission/evaluator/rbac"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// NewProjectRBACEvaluator returns permission evaluator for project
func NewProjectRBACEvaluator(ctx security.Context, pm promgr.ProjectManager) evaluator.Evaluator {
return namespace.New(ProjectNamespaceKind, func(ns types.Namespace) evaluator.Evaluator {
project, err := pm.Get(ns.Identity())
if err != nil || project == nil {
if err != nil {
log.Warningf("Failed to get info of project %d for permission evaluator, error: %v", ns.Identity(), err)
}
return nil
}
if ctx.IsAuthenticated() {
roles := ctx.GetProjectRoles(project.ProjectID)
return rbac.New(NewProjectRBACUser(project, ctx.GetUsername(), roles...))
} else if project.IsPublic() {
// anonymous access and the project is public
return rbac.New(NewProjectRBACUser(project, "anonymous"))
} else {
return nil
}
})
}
// NewProjectRobotEvaluator returns robot permission evaluator for project
func NewProjectRobotEvaluator(ctx security.Context, pm promgr.ProjectManager,
robotFactory func(types.Namespace) types.RBACUser) evaluator.Evaluator {
return namespace.New(ProjectNamespaceKind, func(ns types.Namespace) evaluator.Evaluator {
project, err := pm.Get(ns.Identity())
if err != nil || project == nil {
if err != nil {
log.Warningf("Failed to get info of project %d for permission evaluator, error: %v", ns.Identity(), err)
}
return nil
}
if ctx.IsAuthenticated() {
evaluators := evaluator.Evaluators{
rbac.New(robotFactory(ns)), // robot account access
}
if project.IsPublic() {
// authenticated access and the project is public
evaluators = evaluators.Add(rbac.New(NewProjectRBACUser(project, ctx.GetUsername())))
}
return evaluators
} else if project.IsPublic() {
// anonymous access and the project is public
return rbac.New(NewProjectRBACUser(project, "anonymous"))
} else {
return nil
}
})
}

View File

@ -0,0 +1,116 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"testing"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
promgr "github.com/goharbor/harbor/src/core/promgr/mocks"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/testing/common/security"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var (
projectID = int64(1)
projectAdminSecurity = makeMockSecurity("projectAdmin", common.RoleProjectAdmin)
guestSecurity = makeMockSecurity("guest", common.RoleGuest)
anonymousSecurity = makeMockSecurity("")
publicProjectManager = makeMockProjectManager(projectID, true)
privateProjectManager = makeMockProjectManager(projectID, false)
)
func makeMockSecurity(username string, roles ...int) *security.Context {
var isAuthenticated bool
if username != "" {
isAuthenticated = true
}
ctx := &security.Context{}
ctx.On("IsAuthenticated").Return(isAuthenticated)
ctx.On("GetUsername").Return(username)
ctx.On("GetProjectRoles", mock.AnythingOfType("int64")).Return(roles)
return ctx
}
func makeMockProjectManager(projectID int64, isPublic bool) *promgr.ProjectManager {
pm := &promgr.ProjectManager{}
project := &models.Project{ProjectID: projectID}
if isPublic {
project.SetMetadata(models.ProMetaPublic, "true")
} else {
project.SetMetadata(models.ProMetaPublic, "false")
}
pm.On("Get", projectID).Return(project, nil)
return pm
}
func makeResource(subresource ...types.Resource) types.Resource {
return NewProjectNamespace(projectID).Resource(subresource...)
}
func TestAnonymousAccess(t *testing.T) {
assert := assert.New(t)
evaluator1 := NewProjectRBACEvaluator(anonymousSecurity, publicProjectManager)
assert.True(evaluator1.HasPermission(makeResource(ResourceRepository), ActionPull))
evaluator2 := NewProjectRBACEvaluator(anonymousSecurity, privateProjectManager)
assert.False(evaluator2.HasPermission(makeResource(ResourceRepository), ActionPull))
evaluator3 := NewProjectRobotEvaluator(anonymousSecurity, publicProjectManager, func(ns types.Namespace) types.RBACUser { return nil })
assert.True(evaluator3.HasPermission(makeResource(ResourceRepository), ActionPull))
evaluator4 := NewProjectRobotEvaluator(anonymousSecurity, privateProjectManager, func(ns types.Namespace) types.RBACUser { return nil })
assert.False(evaluator4.HasPermission(makeResource(ResourceRepository), ActionPull))
}
func TestProjectRoleAccess(t *testing.T) {
assert := assert.New(t)
evaluator1 := NewProjectRBACEvaluator(projectAdminSecurity, publicProjectManager)
assert.True(evaluator1.HasPermission(makeResource(ResourceRepository), ActionPush))
evaluator2 := NewProjectRBACEvaluator(guestSecurity, publicProjectManager)
assert.False(evaluator2.HasPermission(makeResource(ResourceRepository), ActionPush))
}
func BenchmarkProjectRBACEvaluator(b *testing.B) {
evaluator := NewProjectRBACEvaluator(projectAdminSecurity, publicProjectManager)
resource := NewProjectNamespace(projectID).Resource(ResourceRepository)
b.ResetTimer()
for i := 0; i < b.N; i++ {
evaluator.HasPermission(resource, ActionPull)
}
}
func BenchmarkProjectRBACEvaluatorParallel(b *testing.B) {
evaluator := NewProjectRBACEvaluator(projectAdminSecurity, publicProjectManager)
resource := NewProjectNamespace(projectID).Resource(ResourceRepository)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
evaluator.HasPermission(resource, ActionPull)
}
})
}

View File

@ -0,0 +1,77 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"fmt"
"regexp"
"strconv"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
const (
// ProjectNamespaceKind kind for project namespace
ProjectNamespaceKind = "project"
)
var (
projectNamespaceRe = regexp.MustCompile("^/project/([^/]*)/?")
)
type projectNamespace struct {
projectID int64
}
func (ns *projectNamespace) Kind() string {
return ProjectNamespaceKind
}
func (ns *projectNamespace) Resource(subresources ...types.Resource) types.Resource {
return types.Resource(fmt.Sprintf("/project/%d", ns.projectID)).Subresource(subresources...)
}
func (ns *projectNamespace) Identity() interface{} {
return ns.projectID
}
func (ns *projectNamespace) GetPolicies() []*types.Policy {
return GetPoliciesOfProject(ns.projectID)
}
// NewProjectNamespace returns namespace for project
func NewProjectNamespace(projectID int64) types.Namespace {
return &projectNamespace{projectID: projectID}
}
// ProjectNamespaceParse ...
func ProjectNamespaceParse(resource types.Resource) (types.Namespace, bool) {
matches := projectNamespaceRe.FindStringSubmatch(resource.String())
if len(matches) <= 1 {
return nil, false
}
projectID, err := strconv.ParseInt(matches[1], 10, 64)
if err != nil {
return nil, false
}
return NewProjectNamespace(projectID), true
}
func init() {
types.RegistryNamespaceParse(ProjectNamespaceKind, ProjectNamespaceParse)
}

View File

@ -0,0 +1,436 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
var (
rolePoliciesMap = map[string][]*types.Policy{
"projectAdmin": {
{Resource: ResourceSelf, Action: ActionRead},
{Resource: ResourceSelf, Action: ActionUpdate},
{Resource: ResourceSelf, Action: ActionDelete},
{Resource: ResourceMember, Action: ActionCreate},
{Resource: ResourceMember, Action: ActionRead},
{Resource: ResourceMember, Action: ActionUpdate},
{Resource: ResourceMember, Action: ActionDelete},
{Resource: ResourceMember, Action: ActionList},
{Resource: ResourceMetadata, Action: ActionCreate},
{Resource: ResourceMetadata, Action: ActionRead},
{Resource: ResourceMetadata, Action: ActionUpdate},
{Resource: ResourceMetadata, Action: ActionDelete},
{Resource: ResourceLog, Action: ActionList},
{Resource: ResourceReplication, Action: ActionRead},
{Resource: ResourceReplication, Action: ActionList},
{Resource: ResourceReplicationJob, Action: ActionRead},
{Resource: ResourceReplicationJob, Action: ActionList},
{Resource: ResourceLabel, Action: ActionCreate},
{Resource: ResourceLabel, Action: ActionRead},
{Resource: ResourceLabel, Action: ActionUpdate},
{Resource: ResourceLabel, Action: ActionDelete},
{Resource: ResourceLabel, Action: ActionList},
{Resource: ResourceLabelResource, Action: ActionList},
{Resource: ResourceQuota, Action: ActionRead},
{Resource: ResourceRepository, Action: ActionCreate},
{Resource: ResourceRepository, Action: ActionRead},
{Resource: ResourceRepository, Action: ActionUpdate},
{Resource: ResourceRepository, Action: ActionDelete},
{Resource: ResourceRepository, Action: ActionList},
{Resource: ResourceRepository, Action: ActionPull},
{Resource: ResourceRepository, Action: ActionPush},
{Resource: ResourceTagRetention, Action: ActionCreate},
{Resource: ResourceTagRetention, Action: ActionRead},
{Resource: ResourceTagRetention, Action: ActionUpdate},
{Resource: ResourceTagRetention, Action: ActionDelete},
{Resource: ResourceTagRetention, Action: ActionList},
{Resource: ResourceTagRetention, Action: ActionOperate},
{Resource: ResourceImmutableTag, Action: ActionCreate},
{Resource: ResourceImmutableTag, Action: ActionUpdate},
{Resource: ResourceImmutableTag, Action: ActionDelete},
{Resource: ResourceImmutableTag, Action: ActionList},
{Resource: ResourceRepositoryLabel, Action: ActionCreate},
{Resource: ResourceRepositoryLabel, Action: ActionDelete},
{Resource: ResourceRepositoryLabel, Action: ActionList},
{Resource: ResourceRepositoryTag, Action: ActionRead},
{Resource: ResourceRepositoryTag, Action: ActionDelete},
{Resource: ResourceRepositoryTag, Action: ActionList},
{Resource: ResourceRepositoryTagScanJob, Action: ActionCreate},
{Resource: ResourceRepositoryTagScanJob, Action: ActionRead},
{Resource: ResourceRepositoryTagVulnerability, Action: ActionList},
{Resource: ResourceRepositoryTagManifest, Action: ActionRead},
{Resource: ResourceRepositoryTagLabel, Action: ActionCreate},
{Resource: ResourceRepositoryTagLabel, Action: ActionDelete},
{Resource: ResourceRepositoryTagLabel, Action: ActionList},
{Resource: ResourceHelmChart, Action: ActionCreate}, // upload helm chart
{Resource: ResourceHelmChart, Action: ActionRead}, // download helm chart
{Resource: ResourceHelmChart, Action: ActionDelete},
{Resource: ResourceHelmChart, Action: ActionList},
{Resource: ResourceHelmChartVersion, Action: ActionCreate}, // upload helm chart version
{Resource: ResourceHelmChartVersion, Action: ActionRead}, // read and download helm chart version
{Resource: ResourceHelmChartVersion, Action: ActionDelete},
{Resource: ResourceHelmChartVersion, Action: ActionList},
{Resource: ResourceHelmChartVersionLabel, Action: ActionCreate},
{Resource: ResourceHelmChartVersionLabel, Action: ActionDelete},
{Resource: ResourceConfiguration, Action: ActionRead},
{Resource: ResourceConfiguration, Action: ActionUpdate},
{Resource: ResourceRobot, Action: ActionCreate},
{Resource: ResourceRobot, Action: ActionRead},
{Resource: ResourceRobot, Action: ActionUpdate},
{Resource: ResourceRobot, Action: ActionDelete},
{Resource: ResourceRobot, Action: ActionList},
{Resource: ResourceNotificationPolicy, Action: ActionCreate},
{Resource: ResourceNotificationPolicy, Action: ActionUpdate},
{Resource: ResourceNotificationPolicy, Action: ActionDelete},
{Resource: ResourceNotificationPolicy, Action: ActionList},
{Resource: ResourceNotificationPolicy, Action: ActionRead},
{Resource: ResourceScan, Action: ActionCreate},
{Resource: ResourceScan, Action: ActionRead},
{Resource: ResourceScanner, Action: ActionRead},
{Resource: ResourceScanner, Action: ActionCreate},
{Resource: ResourceArtifact, Action: ActionCreate},
{Resource: ResourceArtifact, Action: ActionRead},
{Resource: ResourceArtifact, Action: ActionDelete},
{Resource: ResourceArtifact, Action: ActionList},
{Resource: ResourceArtifactAddition, Action: ActionRead},
{Resource: ResourceTag, Action: ActionCreate},
{Resource: ResourceTag, Action: ActionDelete},
{Resource: ResourceArtifactLabel, Action: ActionCreate},
{Resource: ResourceArtifactLabel, Action: ActionDelete},
},
"master": {
{Resource: ResourceSelf, Action: ActionRead},
{Resource: ResourceMember, Action: ActionRead},
{Resource: ResourceMember, Action: ActionList},
{Resource: ResourceMetadata, Action: ActionCreate},
{Resource: ResourceMetadata, Action: ActionRead},
{Resource: ResourceMetadata, Action: ActionUpdate},
{Resource: ResourceMetadata, Action: ActionDelete},
{Resource: ResourceLog, Action: ActionList},
{Resource: ResourceQuota, Action: ActionRead},
{Resource: ResourceReplication, Action: ActionRead},
{Resource: ResourceReplication, Action: ActionList},
{Resource: ResourceLabel, Action: ActionCreate},
{Resource: ResourceLabel, Action: ActionRead},
{Resource: ResourceLabel, Action: ActionUpdate},
{Resource: ResourceLabel, Action: ActionDelete},
{Resource: ResourceLabel, Action: ActionList},
{Resource: ResourceRepository, Action: ActionCreate},
{Resource: ResourceRepository, Action: ActionRead},
{Resource: ResourceRepository, Action: ActionUpdate},
{Resource: ResourceRepository, Action: ActionDelete},
{Resource: ResourceRepository, Action: ActionList},
{Resource: ResourceRepository, Action: ActionPush},
{Resource: ResourceRepository, Action: ActionPull},
{Resource: ResourceTagRetention, Action: ActionCreate},
{Resource: ResourceTagRetention, Action: ActionRead},
{Resource: ResourceTagRetention, Action: ActionUpdate},
{Resource: ResourceTagRetention, Action: ActionDelete},
{Resource: ResourceTagRetention, Action: ActionList},
{Resource: ResourceTagRetention, Action: ActionOperate},
{Resource: ResourceImmutableTag, Action: ActionCreate},
{Resource: ResourceImmutableTag, Action: ActionUpdate},
{Resource: ResourceImmutableTag, Action: ActionDelete},
{Resource: ResourceImmutableTag, Action: ActionList},
{Resource: ResourceRepositoryLabel, Action: ActionCreate},
{Resource: ResourceRepositoryLabel, Action: ActionDelete},
{Resource: ResourceRepositoryLabel, Action: ActionList},
{Resource: ResourceRepositoryTag, Action: ActionRead},
{Resource: ResourceRepositoryTag, Action: ActionDelete},
{Resource: ResourceRepositoryTag, Action: ActionList},
{Resource: ResourceRepositoryTagScanJob, Action: ActionCreate},
{Resource: ResourceRepositoryTagScanJob, Action: ActionRead},
{Resource: ResourceRepositoryTagVulnerability, Action: ActionList},
{Resource: ResourceRepositoryTagManifest, Action: ActionRead},
{Resource: ResourceRepositoryTagLabel, Action: ActionCreate},
{Resource: ResourceRepositoryTagLabel, Action: ActionDelete},
{Resource: ResourceRepositoryTagLabel, Action: ActionList},
{Resource: ResourceHelmChart, Action: ActionCreate},
{Resource: ResourceHelmChart, Action: ActionRead},
{Resource: ResourceHelmChart, Action: ActionDelete},
{Resource: ResourceHelmChart, Action: ActionList},
{Resource: ResourceHelmChartVersion, Action: ActionCreate},
{Resource: ResourceHelmChartVersion, Action: ActionRead},
{Resource: ResourceHelmChartVersion, Action: ActionDelete},
{Resource: ResourceHelmChartVersion, Action: ActionList},
{Resource: ResourceHelmChartVersionLabel, Action: ActionCreate},
{Resource: ResourceHelmChartVersionLabel, Action: ActionDelete},
{Resource: ResourceConfiguration, Action: ActionRead},
{Resource: ResourceRobot, Action: ActionRead},
{Resource: ResourceRobot, Action: ActionList},
{Resource: ResourceNotificationPolicy, Action: ActionList},
{Resource: ResourceScan, Action: ActionCreate},
{Resource: ResourceScan, Action: ActionRead},
{Resource: ResourceScanner, Action: ActionRead},
{Resource: ResourceArtifact, Action: ActionCreate},
{Resource: ResourceArtifact, Action: ActionRead},
{Resource: ResourceArtifact, Action: ActionDelete},
{Resource: ResourceArtifact, Action: ActionList},
{Resource: ResourceArtifactAddition, Action: ActionRead},
{Resource: ResourceTag, Action: ActionCreate},
{Resource: ResourceTag, Action: ActionDelete},
{Resource: ResourceArtifactLabel, Action: ActionCreate},
{Resource: ResourceArtifactLabel, Action: ActionDelete},
},
"developer": {
{Resource: ResourceSelf, Action: ActionRead},
{Resource: ResourceMember, Action: ActionRead},
{Resource: ResourceMember, Action: ActionList},
{Resource: ResourceLog, Action: ActionList},
{Resource: ResourceLabel, Action: ActionRead},
{Resource: ResourceLabel, Action: ActionList},
{Resource: ResourceQuota, Action: ActionRead},
{Resource: ResourceRepository, Action: ActionCreate},
{Resource: ResourceRepository, Action: ActionRead},
{Resource: ResourceRepository, Action: ActionUpdate},
{Resource: ResourceRepository, Action: ActionList},
{Resource: ResourceRepository, Action: ActionPush},
{Resource: ResourceRepository, Action: ActionPull},
{Resource: ResourceRepositoryLabel, Action: ActionCreate},
{Resource: ResourceRepositoryLabel, Action: ActionDelete},
{Resource: ResourceRepositoryLabel, Action: ActionList},
{Resource: ResourceRepositoryTag, Action: ActionRead},
{Resource: ResourceRepositoryTag, Action: ActionList},
{Resource: ResourceRepositoryTagVulnerability, Action: ActionList},
{Resource: ResourceRepositoryTagManifest, Action: ActionRead},
{Resource: ResourceRepositoryTagLabel, Action: ActionCreate},
{Resource: ResourceRepositoryTagLabel, Action: ActionDelete},
{Resource: ResourceRepositoryTagLabel, Action: ActionList},
{Resource: ResourceHelmChart, Action: ActionCreate},
{Resource: ResourceHelmChart, Action: ActionRead},
{Resource: ResourceHelmChart, Action: ActionList},
{Resource: ResourceHelmChartVersion, Action: ActionCreate},
{Resource: ResourceHelmChartVersion, Action: ActionRead},
{Resource: ResourceHelmChartVersion, Action: ActionList},
{Resource: ResourceHelmChartVersionLabel, Action: ActionCreate},
{Resource: ResourceHelmChartVersionLabel, Action: ActionDelete},
{Resource: ResourceConfiguration, Action: ActionRead},
{Resource: ResourceRobot, Action: ActionRead},
{Resource: ResourceRobot, Action: ActionList},
{Resource: ResourceScan, Action: ActionRead},
{Resource: ResourceScanner, Action: ActionRead},
{Resource: ResourceArtifact, Action: ActionCreate},
{Resource: ResourceArtifact, Action: ActionRead},
{Resource: ResourceArtifact, Action: ActionList},
{Resource: ResourceArtifactAddition, Action: ActionRead},
{Resource: ResourceTag, Action: ActionCreate},
{Resource: ResourceArtifactLabel, Action: ActionCreate},
{Resource: ResourceArtifactLabel, Action: ActionDelete},
},
"guest": {
{Resource: ResourceSelf, Action: ActionRead},
{Resource: ResourceMember, Action: ActionRead},
{Resource: ResourceMember, Action: ActionList},
{Resource: ResourceLog, Action: ActionList},
{Resource: ResourceLabel, Action: ActionRead},
{Resource: ResourceLabel, Action: ActionList},
{Resource: ResourceQuota, Action: ActionRead},
{Resource: ResourceRepository, Action: ActionRead},
{Resource: ResourceRepository, Action: ActionList},
{Resource: ResourceRepository, Action: ActionPull},
{Resource: ResourceRepositoryLabel, Action: ActionList},
{Resource: ResourceRepositoryTag, Action: ActionRead},
{Resource: ResourceRepositoryTag, Action: ActionList},
{Resource: ResourceRepositoryTagLabel, Action: ActionList},
{Resource: ResourceRepositoryTagVulnerability, Action: ActionList},
{Resource: ResourceRepositoryTagManifest, Action: ActionRead},
{Resource: ResourceHelmChart, Action: ActionRead},
{Resource: ResourceHelmChart, Action: ActionList},
{Resource: ResourceHelmChartVersion, Action: ActionRead},
{Resource: ResourceHelmChartVersion, Action: ActionList},
{Resource: ResourceConfiguration, Action: ActionRead},
{Resource: ResourceRobot, Action: ActionRead},
{Resource: ResourceRobot, Action: ActionList},
{Resource: ResourceScan, Action: ActionRead},
{Resource: ResourceScanner, Action: ActionRead},
{Resource: ResourceArtifact, Action: ActionRead},
{Resource: ResourceArtifact, Action: ActionList},
{Resource: ResourceArtifactAddition, Action: ActionRead},
},
"limitedGuest": {
{Resource: ResourceSelf, Action: ActionRead},
{Resource: ResourceQuota, Action: ActionRead},
{Resource: ResourceRepository, Action: ActionList},
{Resource: ResourceRepository, Action: ActionPull},
{Resource: ResourceRepositoryTag, Action: ActionRead},
{Resource: ResourceRepositoryTag, Action: ActionList},
{Resource: ResourceRepositoryTagVulnerability, Action: ActionList},
{Resource: ResourceRepositoryTagManifest, Action: ActionRead},
{Resource: ResourceHelmChart, Action: ActionRead},
{Resource: ResourceHelmChart, Action: ActionList},
{Resource: ResourceHelmChartVersion, Action: ActionRead},
{Resource: ResourceHelmChartVersion, Action: ActionList},
{Resource: ResourceConfiguration, Action: ActionRead},
{Resource: ResourceScan, Action: ActionRead},
{Resource: ResourceScanner, Action: ActionRead},
{Resource: ResourceArtifact, Action: ActionRead},
{Resource: ResourceArtifact, Action: ActionList},
{Resource: ResourceArtifactAddition, Action: ActionRead},
},
}
)
// projectRBACRole implement the RBACRole interface
type projectRBACRole struct {
projectID int64
roleID int
}
// GetRoleName returns role name for the visitor role
func (role *projectRBACRole) GetRoleName() string {
switch role.roleID {
case common.RoleProjectAdmin:
return "projectAdmin"
case common.RoleMaster:
return "master"
case common.RoleDeveloper:
return "developer"
case common.RoleGuest:
return "guest"
case common.RoleLimitedGuest:
return "limitedGuest"
default:
return ""
}
}
// GetPolicies returns policies for the visitor role
func (role *projectRBACRole) GetPolicies() []*types.Policy {
policies := []*types.Policy{}
roleName := role.GetRoleName()
if roleName == "" {
return policies
}
namespace := NewProjectNamespace(role.projectID)
for _, policy := range rolePoliciesMap[roleName] {
policies = append(policies, &types.Policy{
Resource: namespace.Resource(policy.Resource),
Action: policy.Action,
Effect: policy.Effect,
})
}
return policies
}

View File

@ -0,0 +1,59 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
type projectRBACUser struct {
project *models.Project
username string
projectRoles []int
}
// GetUserName returns username of the visitor
func (user *projectRBACUser) GetUserName() string {
return user.username
}
// GetPolicies returns policies of the visitor
func (user *projectRBACUser) GetPolicies() []*types.Policy {
if user.project.IsPublic() {
return getPoliciesForPublicProject(user.project.ProjectID)
}
return nil
}
// GetRoles returns roles of the visitor
func (user *projectRBACUser) GetRoles() []types.RBACRole {
roles := []types.RBACRole{}
for _, roleID := range user.projectRoles {
roles = append(roles, &projectRBACRole{projectID: user.project.ProjectID, roleID: roleID})
}
return roles
}
// NewProjectRBACUser returns RBACUser for the project
func NewProjectRBACUser(project *models.Project, username string, projectRoles ...int) types.RBACUser {
return &projectRBACUser{
project: project,
username: username,
projectRoles: projectRoles,
}
}

View File

@ -0,0 +1,106 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"github.com/goharbor/harbor/src/pkg/permission/types"
)
var (
// subresource policies for public project
publicProjectPolicies = []*types.Policy{
{Resource: ResourceSelf, Action: ActionRead},
{Resource: ResourceLabel, Action: ActionRead},
{Resource: ResourceLabel, Action: ActionList},
{Resource: ResourceRepository, Action: ActionList},
{Resource: ResourceRepository, Action: ActionPull},
{Resource: ResourceRepositoryLabel, Action: ActionList},
{Resource: ResourceRepositoryTag, Action: ActionRead},
{Resource: ResourceRepositoryTag, Action: ActionList},
{Resource: ResourceRepositoryTagLabel, Action: ActionList},
{Resource: ResourceRepositoryTagVulnerability, Action: ActionList},
{Resource: ResourceRepositoryTagManifest, Action: ActionRead},
{Resource: ResourceHelmChart, Action: ActionRead},
{Resource: ResourceHelmChart, Action: ActionList},
{Resource: ResourceHelmChartVersion, Action: ActionRead},
{Resource: ResourceHelmChartVersion, Action: ActionList},
{Resource: ResourceScan, Action: ActionRead},
{Resource: ResourceScanner, Action: ActionRead},
{Resource: ResourceArtifact, Action: ActionRead},
{Resource: ResourceArtifact, Action: ActionList},
{Resource: ResourceArtifactAddition, Action: ActionRead},
}
// sub policies for the projects
subPoliciesForProject = computeSubPoliciesForProject()
)
func getPoliciesForPublicProject(projectID int64) []*types.Policy {
policies := []*types.Policy{}
namespace := NewProjectNamespace(projectID)
for _, policy := range publicProjectPolicies {
policies = append(policies, &types.Policy{
Resource: namespace.Resource(policy.Resource),
Action: policy.Action,
Effect: policy.Effect,
})
}
return policies
}
// GetPoliciesOfProject returns all policies for namespace of the project
func GetPoliciesOfProject(projectID int64) []*types.Policy {
policies := []*types.Policy{}
namespace := NewProjectNamespace(projectID)
for _, policy := range subPoliciesForProject {
policies = append(policies, &types.Policy{
Resource: namespace.Resource(policy.Resource),
Action: policy.Action,
Effect: policy.Effect,
})
}
return policies
}
func computeSubPoliciesForProject() []*types.Policy {
var results []*types.Policy
mp := map[string]bool{}
for _, policies := range rolePoliciesMap {
for _, policy := range policies {
if !mp[policy.String()] {
results = append(results, policy)
mp[policy.String()] = true
}
}
}
return results
}

View File

@ -15,139 +15,11 @@
package rbac
import (
"errors"
"fmt"
"path"
"strings"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
const (
// EffectAllow allow effect
EffectAllow = Effect("allow")
// EffectDeny deny effect
EffectDeny = Effect("deny")
)
// Resource alias type for types.Resource
type Resource = types.Resource
// Resource the type of resource
type Resource string
// RelativeTo returns relative resource to other resource
func (res Resource) RelativeTo(other Resource) (Resource, error) {
prefix := other.String()
str := res.String()
if !strings.HasPrefix(str, prefix) {
return Resource(""), errors.New("value error")
}
relative := strings.TrimPrefix(str, prefix)
if strings.HasPrefix(relative, "/") {
relative = relative[1:]
}
if relative == "" {
relative = "."
}
return Resource(relative), nil
}
func (res Resource) String() string {
return string(res)
}
// Subresource returns subresource
func (res Resource) Subresource(resources ...Resource) Resource {
elements := []string{res.String()}
for _, resource := range resources {
elements = append(elements, resource.String())
}
return Resource(path.Join(elements...))
}
// GetNamespace returns namespace from resource
func (res Resource) GetNamespace() (Namespace, error) {
for _, parser := range namespaceParsers {
namespace, err := parser(res)
if err == nil {
return namespace, nil
}
}
return nil, fmt.Errorf("no namespace found for %s", res)
}
// Action the type of action
type Action string
func (act Action) String() string {
return string(act)
}
// Effect the type of effect
type Effect string
func (eff Effect) String() string {
return string(eff)
}
// Policy the type of policy
type Policy struct {
Resource
Action
Effect
}
// GetEffect returns effect of resource, default is allow
func (p *Policy) GetEffect() string {
eft := p.Effect
if eft == "" {
eft = EffectAllow
}
return eft.String()
}
func (p *Policy) String() string {
return p.Resource.String() + ":" + p.Action.String() + ":" + p.GetEffect()
}
// Role the interface of rbac role
type Role interface {
// GetRoleName returns the role identity, if empty string role's policies will be ignore
GetRoleName() string
GetPolicies() []*Policy
}
// User the interface of rbac user
type User interface {
// GetUserName returns the user identity, if empty string user's all policies will be ignore
GetUserName() string
GetPolicies() []*Policy
GetRoles() []Role
}
// BaseUser the type implement User interface whose policies are empty
type BaseUser struct{}
// GetRoles returns roles of the user
func (u *BaseUser) GetRoles() []Role {
return nil
}
// GetUserName returns user identity
func (u *BaseUser) GetUserName() string {
return ""
}
// GetPolicies returns policies of the user
func (u *BaseUser) GetPolicies() []*Policy {
return nil
}
// HasPermission returns whether the user has action permission on resource
func HasPermission(user User, resource Resource, action Action) bool {
return enforcerForUser(user).Enforce(user.GetUserName(), resource.String(), action.String())
}
// Action alias type for types.Action
type Action = types.Action

View File

@ -1,439 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"reflect"
"testing"
)
type role struct {
RoleName string
}
func (r *role) GetRoleName() string {
return r.RoleName
}
func (r *role) GetPolicies() []*Policy {
return []*Policy{
{Resource: "/project", Action: "create"},
{Resource: "/project", Action: "update"},
}
}
type userWithRoles struct {
Username string
RoleName string
BaseUser
}
func (u *userWithRoles) GetUserName() string {
return u.Username
}
func (u *userWithRoles) GetRoles() []Role {
return []Role{
&role{RoleName: u.RoleName},
}
}
type userWithoutRoles struct {
Username string
UserPolicies []*Policy
BaseUser
}
func (u *userWithoutRoles) GetUserName() string {
return u.Username
}
func (u *userWithoutRoles) GetPolicies() []*Policy {
return u.UserPolicies
}
func TestHasPermissionUserWithRoles(t *testing.T) {
type args struct {
user User
resource Resource
action Action
}
tests := []struct {
name string
args args
want bool
}{
{
name: "project create for project admin",
args: args{
&userWithRoles{Username: "project admin", RoleName: "projectAdmin"},
"/project",
"create",
},
want: true,
},
{
name: "project update for project admin",
args: args{
&userWithRoles{Username: "project admin", RoleName: "projectAdmin"},
"/project",
"update",
},
want: true,
},
{
name: "project delete for project admin",
args: args{
&userWithRoles{Username: "project admin", RoleName: "projectAdmin"},
"/project",
"delete",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HasPermission(tt.args.user, tt.args.resource, tt.args.action); got != tt.want {
t.Errorf("HasPermission() = %v, want %v", got, tt.want)
}
})
}
}
func TestHasPermissionUserWithoutRoles(t *testing.T) {
type args struct {
user User
resource Resource
action Action
}
tests := []struct {
name string
args args
want bool
}{
{
name: "project create for user without roles",
args: args{
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}},
"/project",
"create",
},
want: true,
},
{
name: "project delete test for user without roles",
args: args{
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}},
"/project",
"delete",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HasPermission(tt.args.user, tt.args.resource, tt.args.action); got != tt.want {
t.Errorf("HasPermission() = %v, want %v", got, tt.want)
}
})
}
}
func TestHasPermissionUsernameEmpty(t *testing.T) {
type args struct {
user User
resource Resource
action Action
}
tests := []struct {
name string
args args
want bool
}{
{
name: "project create for user without roles",
args: args{
&userWithoutRoles{Username: "", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}},
"/project",
"create",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HasPermission(tt.args.user, tt.args.resource, tt.args.action); got != tt.want {
t.Errorf("HasPermission() = %v, want %v", got, tt.want)
}
})
}
}
func TestHasPermissionRoleNameEmpty(t *testing.T) {
type args struct {
user User
resource Resource
action Action
}
tests := []struct {
name string
args args
want bool
}{
{
name: "project create for project admin",
args: args{
&userWithRoles{Username: "project admin", RoleName: ""},
"/project",
"create",
},
want: false,
},
{
name: "project update for project admin",
args: args{
&userWithRoles{Username: "project admin", RoleName: ""},
"/project",
"update",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HasPermission(tt.args.user, tt.args.resource, tt.args.action); got != tt.want {
t.Errorf("HasPermission() = %v, want %v", got, tt.want)
}
})
}
}
func TestHasPermissionResourceKeyMatch(t *testing.T) {
type args struct {
user User
resource Resource
action Action
}
tests := []struct {
name string
args args
want bool
}{
{
name: "project member create for resource key match",
args: args{
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project/1/*", Action: "*"}}},
"/project/1/member",
"create",
},
want: true,
},
{
name: "project member create for resource key match",
args: args{
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project/:id/*", Action: "*"}}},
"/project/1/member",
"create",
},
want: true,
},
{
name: "project repository create test for resource key match",
args: args{
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project/1/*", Action: "create"}}},
"/project/1/repository",
"create",
},
want: true,
},
{
name: "project repository delete test for resource key match",
args: args{
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project/1/*", Action: "create"}}},
"/project/1/repository",
"delete",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HasPermission(tt.args.user, tt.args.resource, tt.args.action); got != tt.want {
t.Errorf("HasPermission() = %v, want %v", got, tt.want)
}
})
}
}
func TestHasPermissionPolicyDeny(t *testing.T) {
type args struct {
user User
resource Resource
action Action
}
tests := []struct {
name string
args args
want bool
}{
{
name: "project member create for resource deny",
args: args{
&userWithoutRoles{
Username: "user1",
UserPolicies: []*Policy{
{Resource: "/project/1/*", Action: "*"},
{Resource: "/project/1/member", Action: "create", Effect: EffectDeny},
},
},
"/project/1/member",
"create",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HasPermission(tt.args.user, tt.args.resource, tt.args.action); got != tt.want {
t.Errorf("HasPermission() = %v, want %v", got, tt.want)
}
})
}
}
func TestResource_Subresource(t *testing.T) {
type args struct {
resources []Resource
}
tests := []struct {
name string
res Resource
args args
want Resource
}{
{
name: "subresource image",
res: Resource("/project/1"),
args: args{
resources: []Resource{"image"},
},
want: Resource("/project/1/image"),
},
{
name: "subresource image build-history",
res: Resource("/project/1"),
args: args{
resources: []Resource{"image", "12", "build-history"},
},
want: Resource("/project/1/image/12/build-history"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.res.Subresource(tt.args.resources...); got != tt.want {
t.Errorf("Resource.Subresource() = %v, want %v", got, tt.want)
}
})
}
}
func TestResource_GetNamespace(t *testing.T) {
tests := []struct {
name string
res Resource
want Namespace
wantErr bool
}{
{
name: "project namespace",
res: Resource("/project/1"),
want: &projectNamespace{int64(1), false},
wantErr: false,
},
{
name: "unknow namespace",
res: Resource("/unknow/1"),
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.res.GetNamespace()
if (err != nil) != tt.wantErr {
t.Errorf("Resource.GetNamespace() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Resource.GetNamespace() = %v, want %v", got, tt.want)
}
})
}
}
func TestResource_RelativeTo(t *testing.T) {
type args struct {
other Resource
}
tests := []struct {
name string
res Resource
args args
want Resource
wantErr bool
}{
{
name: "/project/1/image",
res: Resource("/project/1/image"),
args: args{other: Resource("/project/1")},
want: Resource("image"),
wantErr: false,
},
{
name: "/project/1",
res: Resource("/project/1"),
args: args{other: Resource("/project/1")},
want: Resource("."),
wantErr: false,
},
{
name: "/project/1",
res: Resource("/project/1"),
args: args{other: Resource("/system")},
want: Resource(""),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.res.RelativeTo(tt.args.other)
if (err != nil) != tt.wantErr {
t.Errorf("Resource.RelativeTo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Resource.RelativeTo() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -18,7 +18,7 @@ import (
"context"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// Context abstracts the operations related with authN and authZ
@ -38,7 +38,7 @@ type Context interface {
// Get user's role in provided project
GetProjectRoles(projectIDOrName interface{}) []int
// Can returns whether the user can do action on resource
Can(action rbac.Action, resource rbac.Resource) bool
Can(action types.Action, resource types.Resource) bool
}
type securityKey struct{}

View File

@ -15,19 +15,25 @@
package local
import (
"sync"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/pkg/permission/evaluator"
"github.com/goharbor/harbor/src/pkg/permission/evaluator/admin"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// SecurityContext implements security.Context interface based on database
type SecurityContext struct {
user *models.User
pm promgr.ProjectManager
user *models.User
pm promgr.ProjectManager
evaluator evaluator.Evaluator
once sync.Once
}
// NewSecurityContext ...
@ -77,20 +83,18 @@ func (s *SecurityContext) IsSolutionUser() bool {
}
// Can returns whether the user can do action on resource
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
ns, err := resource.GetNamespace()
if err == nil {
switch ns.Kind() {
case "project":
projectID := ns.Identity().(int64)
isPublicProject, _ := s.pm.IsPublic(projectID)
projectNamespace := rbac.NewProjectNamespace(projectID, isPublicProject)
user := project.NewUser(s, projectNamespace, s.GetProjectRoles(projectID)...)
return rbac.HasPermission(user, resource, action)
func (s *SecurityContext) Can(action types.Action, resource types.Resource) bool {
s.once.Do(func() {
var evaluators evaluator.Evaluators
if s.IsSysAdmin() {
evaluators = evaluators.Add(admin.New(s.GetUsername()))
}
}
evaluators = evaluators.Add(rbac.NewProjectRBACEvaluator(s, s.pm))
return false
s.evaluator = evaluators
})
return s.evaluator != nil && s.evaluator.HasPermission(resource, action)
}
// GetProjectRoles ...

View File

@ -15,21 +15,27 @@
package robot
import (
"sync"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/pkg/permission/evaluator"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot/model"
)
// SecurityContext implements security.Context interface based on database
type SecurityContext struct {
robot *model.Robot
pm promgr.ProjectManager
policy []*rbac.Policy
robot *model.Robot
pm promgr.ProjectManager
policy []*types.Policy
evaluator evaluator.Evaluator
once sync.Once
}
// NewSecurityContext ...
func NewSecurityContext(robot *model.Robot, pm promgr.ProjectManager, policy []*rbac.Policy) *SecurityContext {
func NewSecurityContext(robot *model.Robot, pm promgr.ProjectManager, policy []*types.Policy) *SecurityContext {
return &SecurityContext{
robot: robot,
pm: pm,
@ -77,18 +83,14 @@ func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int {
}
// Can returns whether the robot can do action on resource
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
ns, err := resource.GetNamespace()
if err == nil {
switch ns.Kind() {
case "project":
projectID := ns.Identity().(int64)
isPublicProject, _ := s.pm.IsPublic(projectID)
projectNamespace := rbac.NewProjectNamespace(projectID, isPublicProject)
robot := NewRobot(s.GetUsername(), projectNamespace, s.policy)
return rbac.HasPermission(robot, resource, action)
func (s *SecurityContext) Can(action types.Action, resource types.Resource) bool {
s.once.Do(func() {
robotFactory := func(ns types.Namespace) types.RBACUser {
return NewRobot(s.GetUsername(), ns, s.policy)
}
}
return false
s.evaluator = rbac.NewProjectRobotEvaluator(s, s.pm, robotFactory)
})
return s.evaluator != nil && s.evaluator.HasPermission(resource, action)
}

View File

@ -17,15 +17,16 @@ package robot
import (
"fmt"
"os"
"strconv"
"testing"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -40,45 +41,7 @@ var (
)
func TestMain(m *testing.M) {
dbHost := os.Getenv("POSTGRESQL_HOST")
if len(dbHost) == 0 {
log.Fatalf("environment variable POSTGRES_HOST is not set")
}
dbUser := os.Getenv("POSTGRESQL_USR")
if len(dbUser) == 0 {
log.Fatalf("environment variable POSTGRES_USR is not set")
}
dbPortStr := os.Getenv("POSTGRESQL_PORT")
if len(dbPortStr) == 0 {
log.Fatalf("environment variable POSTGRES_PORT is not set")
}
dbPort, err := strconv.Atoi(dbPortStr)
if err != nil {
log.Fatalf("invalid POSTGRESQL_PORT: %v", err)
}
dbPassword := os.Getenv("POSTGRESQL_PWD")
dbDatabase := os.Getenv("POSTGRESQL_DATABASE")
if len(dbDatabase) == 0 {
log.Fatalf("environment variable POSTGRESQL_DATABASE is not set")
}
database := &models.Database{
Type: "postgresql",
PostGreSQL: &models.PostGreSQL{
Host: dbHost,
Port: dbPort,
Username: dbUser,
Password: dbPassword,
Database: dbDatabase,
},
}
log.Infof("POSTGRES_HOST: %s, POSTGRES_USR: %s, POSTGRES_PORT: %d, POSTGRES_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword)
if err := dao.InitDatabase(database); err != nil {
log.Fatalf("failed to initialize database: %v", err)
}
test.InitDatabaseFromEnv()
// add project
id, err := dao.AddProject(*private)
@ -136,7 +99,7 @@ func TestIsSolutionUser(t *testing.T) {
}
func TestHasPullPerm(t *testing.T) {
policies := []*rbac.Policy{
policies := []*types.Policy{
{
Resource: rbac.Resource(fmt.Sprintf("/project/%d/repository", private.ProjectID)),
Action: rbac.ActionPull,
@ -153,7 +116,7 @@ func TestHasPullPerm(t *testing.T) {
}
func TestHasPushPerm(t *testing.T) {
policies := []*rbac.Policy{
policies := []*types.Policy{
{
Resource: rbac.Resource(fmt.Sprintf("/project/%d/repository", private.ProjectID)),
Action: rbac.ActionPush,
@ -170,7 +133,7 @@ func TestHasPushPerm(t *testing.T) {
}
func TestHasPushPullPerm(t *testing.T) {
policies := []*rbac.Policy{
policies := []*types.Policy{
{
Resource: rbac.Resource(fmt.Sprintf("/project/%d/repository", private.ProjectID)),
Action: rbac.ActionPush,

View File

@ -1,15 +1,14 @@
package robot
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// robot implement the rbac.User interface for project robot account
type robot struct {
username string
namespace rbac.Namespace
policies []*rbac.Policy
namespace types.Namespace
policies []*types.Policy
}
// GetUserName get the robot name.
@ -18,22 +17,17 @@ func (r *robot) GetUserName() string {
}
// GetPolicies ...
func (r *robot) GetPolicies() []*rbac.Policy {
policies := []*rbac.Policy{}
if r.namespace.IsPublic() {
policies = append(policies, project.PoliciesForPublicProject(r.namespace)...)
}
policies = append(policies, r.policies...)
return policies
func (r *robot) GetPolicies() []*types.Policy {
return r.policies
}
// GetRoles robot has no definition of role, always return nil here.
func (r *robot) GetRoles() []rbac.Role {
func (r *robot) GetRoles() []types.RBACRole {
return nil
}
// NewRobot ...
func NewRobot(username string, namespace rbac.Namespace, policies []*rbac.Policy) rbac.User {
func NewRobot(username string, namespace types.Namespace, policies []*types.Policy) types.RBACUser {
return &robot{
username: username,
namespace: namespace,
@ -41,28 +35,13 @@ func NewRobot(username string, namespace rbac.Namespace, policies []*rbac.Policy
}
}
func filterPolicies(namespace rbac.Namespace, policies []*rbac.Policy) []*rbac.Policy {
var results []*rbac.Policy
if len(policies) == 0 {
return results
}
mp := getAllPolicies(namespace)
func filterPolicies(namespace types.Namespace, policies []*types.Policy) []*types.Policy {
var results []*types.Policy
for _, policy := range policies {
if mp[policy.String()] {
if types.ResourceAllowedInNamespace(policy.Resource, namespace) {
results = append(results, policy)
}
}
return results
}
// getAllPolicies gets all of supported policies supported in project and external policies supported for robot account
func getAllPolicies(namespace rbac.Namespace) map[string]bool {
mp := map[string]bool{}
for _, policy := range project.GetAllPolicies(namespace) {
mp[policy.String()] = true
}
scannerPull := &rbac.Policy{Resource: namespace.Resource(rbac.ResourceRepository), Action: rbac.ActionScannerPull}
mp[scannerPull.String()] = true
return mp
}

View File

@ -18,21 +18,22 @@ import (
"testing"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/stretchr/testify/assert"
)
func TestGetPolicies(t *testing.T) {
rbacPolicy := &rbac.Policy{
rbacPolicy := &types.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies := []*types.Policy{}
policies = append(policies, rbacPolicy)
robot := robot{
username: "test",
namespace: rbac.NewProjectNamespace(1, false),
namespace: rbac.NewProjectNamespace(1),
policies: policies,
}
@ -42,13 +43,13 @@ func TestGetPolicies(t *testing.T) {
}
func TestNewRobot(t *testing.T) {
policies := []*rbac.Policy{
policies := []*types.Policy{
{Resource: "/project/1/repository", Action: "pull"},
{Resource: "/project/1/repository", Action: "scanner-pull"},
{Resource: "/project/library/repository", Action: "pull"},
{Resource: "/project/library/repository", Action: "push"},
}
robot := NewRobot("test", rbac.NewProjectNamespace(1, false), policies)
robot := NewRobot("test", rbac.NewProjectNamespace(1), policies)
assert.Len(t, robot.GetPolicies(), 2)
}

View File

@ -19,9 +19,9 @@ import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/secret"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// SecurityContext implements security.Context interface based on secret store
@ -79,7 +79,7 @@ func (s *SecurityContext) IsSolutionUser() bool {
// Can returns whether the user can do action on resource
// returns true if the corresponding user of the secret
// is jobservice or core service, otherwise returns false
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
func (s *SecurityContext) Can(action types.Action, resource types.Resource) bool {
if s.store == nil {
return false
}

View File

@ -10,7 +10,6 @@ import (
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common/api"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/core/promgr/metamgr"
)
@ -265,58 +264,3 @@ func (mpm *mockProjectManager) GetPublic() ([]*models.Project, error) {
func (mpm *mockProjectManager) GetMetadataManager() metamgr.ProjectMetadataManager {
return nil
}
// mock security context
type mockSecurityContext struct{}
// IsAuthenticated returns whether the context has been authenticated or not
func (msc *mockSecurityContext) IsAuthenticated() bool {
return true
}
// GetUsername returns the username of user related to the context
func (msc *mockSecurityContext) GetUsername() string {
return "amdin"
}
// IsSysAdmin returns whether the user is system admin
func (msc *mockSecurityContext) IsSysAdmin() bool {
return true
}
// IsSolutionUser returns whether the user is solution user
func (msc *mockSecurityContext) IsSolutionUser() bool {
return false
}
// Can returns whether the user can do action on resource
func (msc *mockSecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
namespace, err := resource.GetNamespace()
if err != nil || namespace.Kind() != "project" {
return false
}
projectIDOrName := namespace.Identity()
if projectIDOrName == nil {
return false
}
if ns, ok := projectIDOrName.(string); ok {
if ns == "library" {
return true
}
}
return false
}
// Get current user's all project
func (msc *mockSecurityContext) GetMyProjects() ([]*models.Project, error) {
return []*models.Project{{ProjectID: 0, Name: "library"}}, nil
}
// Get user's role in provided project
func (msc *mockSecurityContext) GetProjectRoles(projectIDOrName interface{}) []int {
return []int{0, 1, 2, 3}
}

View File

@ -22,7 +22,6 @@ import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/robot"
"github.com/goharbor/harbor/src/pkg/robot/model"
@ -220,8 +219,7 @@ func validateRobotReq(p *models.Project, robotReq *model.RobotCreate) error {
return errors.New("access required")
}
namespace, _ := rbac.Resource(fmt.Sprintf("/project/%d", p.ProjectID)).GetNamespace()
policies := project.GetAllPolicies(namespace)
policies := rbac.GetPoliciesOfProject(p.ProjectID)
mp := map[string]bool{}
for _, policy := range policies {

View File

@ -23,6 +23,7 @@ import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot/model"
)
@ -32,13 +33,13 @@ var (
)
func TestRobotAPIPost(t *testing.T) {
res := rbac.Resource("/project/1")
res := types.Resource("/project/1")
rbacPolicy := &rbac.Policy{
rbacPolicy := &types.Policy{
Resource: res.Subresource(rbac.ResourceRepository),
Action: "pull",
}
policies := []*rbac.Policy{}
policies := []*types.Policy{}
policies = append(policies, rbacPolicy)
tokenDuration := time.Duration(30) * time.Minute
@ -114,7 +115,7 @@ func TestRobotAPIPost(t *testing.T) {
Name: "test",
Description: "resource not exist",
ExpiresAt: expiresAt,
Access: []*rbac.Policy{
Access: []*types.Policy{
{Resource: res.Subresource("foo"), Action: rbac.ActionCreate},
},
},
@ -130,7 +131,7 @@ func TestRobotAPIPost(t *testing.T) {
Name: "test",
Description: "action not exist",
ExpiresAt: expiresAt,
Access: []*rbac.Policy{
Access: []*types.Policy{
{Resource: res.Subresource(rbac.ResourceRepository), Action: "foo"},
},
},
@ -146,7 +147,7 @@ func TestRobotAPIPost(t *testing.T) {
Name: "test",
Description: "policy not exit",
ExpiresAt: expiresAt,
Access: []*rbac.Policy{
Access: []*types.Policy{
{Resource: res.Subresource(rbac.ResourceMember), Action: rbac.ActionPush},
},
},

View File

@ -17,7 +17,6 @@ package api
import (
"errors"
"fmt"
"github.com/goharbor/harbor/src/core/filter"
"net/http"
"regexp"
"strconv"
@ -26,11 +25,12 @@ import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/rbac/project"
"github.com/goharbor/harbor/src/common/security/local"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/filter"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// UserAPI handles request to /api/users/{}
@ -484,16 +484,12 @@ func (ua *UserAPI) ListUserPermissions() {
relative := ua.Ctx.Input.Query("relative") == "true"
scope := rbac.Resource(ua.Ctx.Input.Query("scope"))
policies := []*rbac.Policy{}
policies := []*types.Policy{}
namespace, err := scope.GetNamespace()
if err == nil {
switch namespace.Kind() {
case "project":
for _, policy := range project.GetAllPolicies(namespace) {
if ua.SecurityCtx.Can(policy.Action, policy.Resource) {
policies = append(policies, policy)
}
if ns, ok := types.NamespaceFromResource(scope); ok {
for _, policy := range ns.GetPolicies() {
if ua.SecurityCtx.Can(policy.Action, policy.Resource) {
policies = append(policies, policy)
}
}
}

View File

@ -0,0 +1,191 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import (
metamgr "github.com/goharbor/harbor/src/core/promgr/metamgr"
mock "github.com/stretchr/testify/mock"
models "github.com/goharbor/harbor/src/common/models"
)
// ProjectManager is an autogenerated mock type for the ProjectManager type
type ProjectManager struct {
mock.Mock
}
// Create provides a mock function with given fields: _a0
func (_m *ProjectManager) Create(_a0 *models.Project) (int64, error) {
ret := _m.Called(_a0)
var r0 int64
if rf, ok := ret.Get(0).(func(*models.Project) int64); ok {
r0 = rf(_a0)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(*models.Project) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: projectIDOrName
func (_m *ProjectManager) Delete(projectIDOrName interface{}) error {
ret := _m.Called(projectIDOrName)
var r0 error
if rf, ok := ret.Get(0).(func(interface{}) error); ok {
r0 = rf(projectIDOrName)
} else {
r0 = ret.Error(0)
}
return r0
}
// Exists provides a mock function with given fields: projectIDOrName
func (_m *ProjectManager) Exists(projectIDOrName interface{}) (bool, error) {
ret := _m.Called(projectIDOrName)
var r0 bool
if rf, ok := ret.Get(0).(func(interface{}) bool); ok {
r0 = rf(projectIDOrName)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(interface{}) error); ok {
r1 = rf(projectIDOrName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Get provides a mock function with given fields: projectIDOrName
func (_m *ProjectManager) Get(projectIDOrName interface{}) (*models.Project, error) {
ret := _m.Called(projectIDOrName)
var r0 *models.Project
if rf, ok := ret.Get(0).(func(interface{}) *models.Project); ok {
r0 = rf(projectIDOrName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Project)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(interface{}) error); ok {
r1 = rf(projectIDOrName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMetadataManager provides a mock function with given fields:
func (_m *ProjectManager) GetMetadataManager() metamgr.ProjectMetadataManager {
ret := _m.Called()
var r0 metamgr.ProjectMetadataManager
if rf, ok := ret.Get(0).(func() metamgr.ProjectMetadataManager); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(metamgr.ProjectMetadataManager)
}
}
return r0
}
// GetPublic provides a mock function with given fields:
func (_m *ProjectManager) GetPublic() ([]*models.Project, error) {
ret := _m.Called()
var r0 []*models.Project
if rf, ok := ret.Get(0).(func() []*models.Project); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Project)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// IsPublic provides a mock function with given fields: projectIDOrName
func (_m *ProjectManager) IsPublic(projectIDOrName interface{}) (bool, error) {
ret := _m.Called(projectIDOrName)
var r0 bool
if rf, ok := ret.Get(0).(func(interface{}) bool); ok {
r0 = rf(projectIDOrName)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(interface{}) error); ok {
r1 = rf(projectIDOrName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: query
func (_m *ProjectManager) List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error) {
ret := _m.Called(query)
var r0 *models.ProjectQueryResult
if rf, ok := ret.Get(0).(func(*models.ProjectQueryParam) *models.ProjectQueryResult); ok {
r0 = rf(query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.ProjectQueryResult)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*models.ProjectQueryParam) error); ok {
r1 = rf(query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: projectIDOrName, project
func (_m *ProjectManager) Update(projectIDOrName interface{}, project *models.Project) error {
ret := _m.Called(projectIDOrName, project)
var r0 error
if rf, ok := ret.Get(0).(func(interface{}, *models.Project) error); ok {
r0 = rf(projectIDOrName, project)
} else {
r0 = ret.Error(0)
}
return r0
}

View File

@ -0,0 +1,39 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package admin
import (
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/permission/evaluator"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
var _ evaluator.Evaluator = &Evaluator{}
// Evaluator the permission evaluator for the system administrator
type Evaluator struct {
username string
}
// HasPermission always return true for the system administrator
func (e *Evaluator) HasPermission(resource types.Resource, action types.Action) bool {
log.Debugf("system administrator %s require %s action for resource %s", e.username, action, resource)
return true
}
// New returns evaluator.Evaluator for the system administrator
func New(username string) *Evaluator {
return &Evaluator{username: username}
}

View File

@ -0,0 +1,64 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package evaluator
import (
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// Evaluator the permission evaluator
type Evaluator interface {
// HasPermission returns true when user has action permission for the resource
HasPermission(resource types.Resource, action types.Action) bool
}
// Evaluators evaluator set
type Evaluators []Evaluator
// Add adds an evaluator to a given slice of evaluators
func (evaluators Evaluators) Add(newEvaluators ...Evaluator) Evaluators {
for _, newEvaluator := range newEvaluators {
if newEvaluator == nil {
continue
}
if items, ok := newEvaluator.(Evaluators); ok {
evaluators = evaluators.Add(items...)
} else {
exists := false
for _, evaluator := range evaluators {
if evaluator == newEvaluator {
exists = true
}
}
if !exists {
evaluators = append(evaluators, newEvaluator)
}
}
}
return evaluators
}
// HasPermission returns true when one of evaluator has action permission for the resource
func (evaluators Evaluators) HasPermission(resource types.Resource, action types.Action) bool {
for _, evaluator := range evaluators {
if evaluator != nil && evaluator.HasPermission(resource, action) {
return true
}
}
return false
}

View File

@ -12,28 +12,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
package evaluator
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/stretchr/testify/assert"
)
type ProjectParserTestSuite struct {
suite.Suite
type mockEvaluator struct {
name string
}
func (suite *ProjectParserTestSuite) TestParse() {
namespace, err := projectNamespaceParser(Resource("/project/1/image"))
suite.Equal(namespace, &projectNamespace{projectID: 1})
suite.Nil(err)
namespace, err = projectNamespaceParser(Resource("/fake/1/image"))
suite.Nil(namespace)
suite.Error(err)
func (e *mockEvaluator) HasPermission(resource types.Resource, action types.Action) bool {
return true
}
func TestProjectParserTestSuite(t *testing.T) {
suite.Run(t, new(ProjectParserTestSuite))
func TestEvaluatorsAdd(t *testing.T) {
eva1 := &mockEvaluator{name: "eva1"}
eva2 := &mockEvaluator{name: "eva2"}
eva3 := Evaluators{eva1, eva2}
var es1 Evaluators
assert.Len(t, es1.Add(eva3), 2)
assert.Len(t, es1.Add(eva1, eva2, eva3), 2)
}

View File

@ -0,0 +1,48 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lazy
import (
"sync"
"github.com/goharbor/harbor/src/pkg/permission/evaluator"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// EvaluatorFactory the permission evaluator factory
type EvaluatorFactory func() evaluator.Evaluator
var _ evaluator.Evaluator = &Evaluator{}
// Evaluator lazy permission evaluator
type Evaluator struct {
factory EvaluatorFactory
evaluator evaluator.Evaluator
once sync.Once
}
// HasPermission returns true when user has action permission for the resource
func (l *Evaluator) HasPermission(resource types.Resource, action types.Action) bool {
l.once.Do(func() {
l.evaluator = l.factory()
})
return l.evaluator != nil && l.evaluator.HasPermission(resource, action)
}
// New returns lazy evaluator
func New(factory EvaluatorFactory) *Evaluator {
return &Evaluator{factory: factory}
}

View File

@ -0,0 +1,64 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package namespace
import (
"fmt"
"sync"
"github.com/goharbor/harbor/src/pkg/permission/evaluator"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// EvaluatorFactory returns the evaluator.Evaluator of the namespace
type EvaluatorFactory func(types.Namespace) evaluator.Evaluator
var _ evaluator.Evaluator = &Evaluator{}
// Evaluator evaluator for the namespace
type Evaluator struct {
factory EvaluatorFactory
namespaceKind string
cache sync.Map
}
// HasPermission returns true when user has action permission for the resource
func (e *Evaluator) HasPermission(resource types.Resource, action types.Action) bool {
ns, ok := types.NamespaceFromResource(resource)
if ok && ns.Kind() == e.namespaceKind {
var eva evaluator.Evaluator
key := fmt.Sprintf("%s:%v", ns.Kind(), ns.Identity())
value, ok := e.cache.Load(key)
if !ok {
eva = e.factory(ns)
e.cache.Store(key, eva)
} else {
eva, _ = value.(evaluator.Evaluator) // maybe value is nil
}
return eva != nil && eva.HasPermission(resource, action)
}
return false
}
// New returns permission evaluator for which support namespace
func New(namespaceKind string, factory EvaluatorFactory) *Evaluator {
return &Evaluator{
namespaceKind: namespaceKind,
factory: factory,
}
}

View File

@ -0,0 +1,53 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"github.com/casbin/casbin"
"github.com/casbin/casbin/model"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// Syntax for models see https://casbin.org/docs/en/syntax-for-models
const modelText = `
# Request definition
[request_definition]
r = sub, obj, act
# Policy definition
[policy_definition]
p = sub, obj, act, eft
# Role definition
[role_definition]
g = _, _
# Policy effect
[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
# Matchers
[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && (r.act == p.act || p.act == '*')
`
func makeEnforcer(rbacUser types.RBACUser) *casbin.Enforcer {
m := model.Model{}
m.LoadModelFromText(modelText)
e := casbin.NewEnforcer(m, &adapter{rbacUser: rbacUser})
e.AddFunction("keyMatch2", keyMatch2Func)
return e
}

View File

@ -0,0 +1,104 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"errors"
"fmt"
"github.com/casbin/casbin/model"
"github.com/casbin/casbin/persist"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
var (
errNotImplemented = errors.New("not implemented")
)
func policyLinesOfRole(rbacRole types.RBACRole) []string {
lines := []string{}
roleName := rbacRole.GetRoleName()
// returns empty policy lines if role name is empty
if roleName == "" {
return lines
}
for _, policy := range rbacRole.GetPolicies() {
line := fmt.Sprintf("p, %s, %s, %s, %s", roleName, policy.Resource, policy.Action, policy.GetEffect())
lines = append(lines, line)
}
return lines
}
func policyLinesOfRBACUser(rbacUser types.RBACUser) []string {
lines := []string{}
username := rbacUser.GetUserName()
for _, policy := range rbacUser.GetPolicies() {
line := fmt.Sprintf("p, %s, %s, %s, %s", username, policy.Resource, policy.Action, policy.GetEffect())
lines = append(lines, line)
}
return lines
}
type adapter struct {
rbacUser types.RBACUser
}
func (a *adapter) getPolicyLines() []string {
lines := []string{}
username := a.rbacUser.GetUserName()
// returns empty policy lines if username is empty
if username == "" {
return lines
}
lines = append(lines, policyLinesOfRBACUser(a.rbacUser)...)
for _, role := range a.rbacUser.GetRoles() {
lines = append(lines, policyLinesOfRole(role)...)
lines = append(lines, fmt.Sprintf("g, %s, %s", username, role.GetRoleName()))
}
return lines
}
func (a *adapter) LoadPolicy(model model.Model) error {
for _, line := range a.getPolicyLines() {
persist.LoadPolicyLine(line, model)
}
return nil
}
func (a *adapter) SavePolicy(model model.Model) error {
return errNotImplemented
}
func (a *adapter) AddPolicy(sec string, ptype string, rule []string) error {
return errNotImplemented
}
func (a *adapter) RemovePolicy(sec string, ptype string, rule []string) error {
return errNotImplemented
}
func (a *adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
return errNotImplemented
}

View File

@ -0,0 +1,102 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"math/rand"
"regexp"
"strings"
"sync"
"time"
"github.com/goharbor/harbor/src/common/utils/log"
)
type regexpStore struct {
entries sync.Map
}
func (s *regexpStore) Get(key string, build func(string) *regexp.Regexp) *regexp.Regexp {
value, ok := s.entries.Load(key)
if !ok {
value = build(key)
s.entries.Store(key, value)
}
return value.(*regexp.Regexp)
}
func (s *regexpStore) Purge() {
var keys []interface{}
s.entries.Range(func(key, value interface{}) bool {
keys = append(keys, key)
return true
})
for _, key := range keys {
s.entries.Delete(key)
}
}
var (
store = &regexpStore{}
)
func init() {
startRegexpStorePurging(store, time.Hour*24)
}
func startRegexpStorePurging(s *regexpStore, intervalDuration time.Duration) {
go func() {
rand.Seed(time.Now().Unix())
jitter := time.Duration(rand.Int()%60) * time.Minute
log.Debugf("Starting regexp store purge in %s", jitter)
time.Sleep(jitter)
for {
s.Purge()
log.Debugf("Starting regexp store purge in %s", intervalDuration)
time.Sleep(intervalDuration)
}
}()
}
func keyMatch2Build(key2 string) *regexp.Regexp {
re := regexp.MustCompile(`(.*):[^/]+(.*)`)
key2 = strings.Replace(key2, "/*", "/.*", -1)
for {
if !strings.Contains(key2, "/:") {
break
}
key2 = re.ReplaceAllString(key2, "$1[^/]+$2")
}
return regexp.MustCompile("^" + key2 + "$")
}
// keyMatch2 determines whether key1 matches the pattern of key2, its behavior most likely the builtin KeyMatch2
// except that the match of ("/project/1/robot", "/project/1") will return false
func keyMatch2(key1 string, key2 string) bool {
return store.Get(key2, keyMatch2Build).MatchString(key1)
}
func keyMatch2Func(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)
name2 := args[1].(string)
return keyMatch2(name1, name2), nil
}

View File

@ -16,6 +16,8 @@ package rbac
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_keyMatch2(t *testing.T) {
@ -57,3 +59,26 @@ func Test_keyMatch2(t *testing.T) {
})
}
}
func TestRegexpStore(t *testing.T) {
assert := assert.New(t)
s := &regexpStore{}
sLen := func() int {
var l int
s.entries.Range(func(key, value interface{}) bool {
l++
return true
})
return l
}
r1 := s.Get("key1", keyMatch2Build)
r2 := s.Get("key1", keyMatch2Build)
assert.Equal(r1, r2)
s.Purge()
assert.Equal(0, sLen())
}

View File

@ -0,0 +1,48 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"sync"
"github.com/casbin/casbin"
"github.com/goharbor/harbor/src/pkg/permission/evaluator"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
var _ evaluator.Evaluator = &Evaluator{}
// Evaluator the permission evaluator for rbac user
type Evaluator struct {
rbacUser types.RBACUser
enforcer *casbin.Enforcer
once sync.Once
}
// HasPermission returns true when the rbac user has action permission for the resource
func (e *Evaluator) HasPermission(resource types.Resource, action types.Action) bool {
e.once.Do(func() {
e.enforcer = makeEnforcer(e.rbacUser)
})
return e.enforcer.Enforce(e.rbacUser.GetUserName(), resource.String(), action.String())
}
// New returns evaluator.Evaluator for the RBACUser
func New(rbacUser types.RBACUser) *Evaluator {
return &Evaluator{
rbacUser: rbacUser,
}
}

View File

@ -0,0 +1,77 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"sync"
)
var (
parsesMu sync.RWMutex
parses = map[string]NamespaceParse{}
)
// NamespaceParse parse namespace from the resource
type NamespaceParse func(Resource) (Namespace, bool)
// Namespace the namespace interface
type Namespace interface {
// Kind returns the kind of namespace
Kind() string
// Resource returns new resource for subresources with the namespace
Resource(subresources ...Resource) Resource
// Identity returns identity attached with namespace
Identity() interface{}
// GetPolicies returns all policies of the namespace
GetPolicies() []*Policy
}
// RegistryNamespaceParse ...
func RegistryNamespaceParse(name string, parse NamespaceParse) {
parsesMu.Lock()
defer parsesMu.Unlock()
if parse == nil {
panic("permission: Register namespace parse is nil")
}
if _, dup := parses[name]; dup {
panic("permission: Register called twice for namespace parse " + name)
}
parses[name] = parse
}
// NamespaceFromResource returns namespace from resource
func NamespaceFromResource(resource Resource) (Namespace, bool) {
parsesMu.RLock()
defer parsesMu.RUnlock()
for _, parse := range parses {
if ns, ok := parse(resource); ok {
return ns, true
}
}
return nil, false
}
// ResourceAllowedInNamespace returns true when resource's namespace equal the ns
func ResourceAllowedInNamespace(resource Resource, ns Namespace) bool {
n, ok := NamespaceFromResource(resource)
if ok {
return n.Kind() == ns.Kind() && n.Identity() == ns.Identity()
}
return false
}

View File

@ -0,0 +1,33 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
// RBACRole the interface of rbac role
type RBACRole interface {
// GetRoleName returns the role identity, if empty string role's policies will be ignore
GetRoleName() string
// GetPolicies returns the policies of the role
GetPolicies() []*Policy
}
// RBACUser the interface of rbac user
type RBACUser interface {
// GetUserName returns the user identity, if empty string user's all policies will be ignore
GetUserName() string
// GetPolicies returns special policies of the user
GetPolicies() []*Policy
// GetRoles returns roles the user owned
GetRoles() []RBACRole
}

View File

@ -0,0 +1,62 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
import (
"errors"
"fmt"
"path"
"strings"
)
// Resource the type of resource
type Resource string
func (res Resource) String() string {
return string(res)
}
// RelativeTo returns relative resource to other resource
func (res Resource) RelativeTo(other Resource) (Resource, error) {
prefix := other.String()
str := res.String()
if !strings.HasPrefix(str, prefix) {
return Resource(""), errors.New("value error")
}
relative := strings.TrimPrefix(strings.TrimPrefix(str, prefix), "/")
if relative == "" {
relative = "."
}
return Resource(relative), nil
}
// Subresource returns subresource
func (res Resource) Subresource(resources ...Resource) Resource {
elements := []string{res.String()}
for _, resource := range resources {
elements = append(elements, resource.String())
}
return Resource(path.Join(elements...))
}
// GetNamespace returns namespace from resource
func (res Resource) GetNamespace() (Namespace, error) {
return nil, fmt.Errorf("no namespace found for %s", res)
}

View File

@ -0,0 +1,57 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package types
const (
// EffectAllow allow effect
EffectAllow = Effect("allow")
// EffectDeny deny effect
EffectDeny = Effect("deny")
)
// Action the type of action
type Action string
func (act Action) String() string {
return string(act)
}
// Effect the type of effect
type Effect string
func (eff Effect) String() string {
return string(eff)
}
// Policy the type of policy
type Policy struct {
Resource
Action
Effect
}
// GetEffect returns effect of resource, default is allow
func (p *Policy) GetEffect() string {
eft := p.Effect
if eft == "" {
eft = EffectAllow
}
return eft.String()
}
func (p *Policy) String() string {
return p.Resource.String() + ":" + p.Action.String() + ":" + p.GetEffect()
}

View File

@ -1,17 +1,19 @@
package robot
import (
"testing"
"time"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils/test"
core_cfg "github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/robot/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"testing"
"time"
)
type ControllerTestSuite struct {
@ -41,11 +43,11 @@ func (s *ControllerTestSuite) TestRobotAccount() {
res := rbac.Resource("/project/1")
rbacPolicy := &rbac.Policy{
rbacPolicy := &types.Policy{
Resource: res.Subresource(rbac.ResourceRepository),
Action: "pull",
}
policies := []*rbac.Policy{}
policies := []*types.Policy{}
policies = append(policies, rbacPolicy)
tokenDuration := time.Duration(30) * time.Minute

View File

@ -1,11 +1,12 @@
package model
import (
"time"
"github.com/astaxie/beego/orm"
"github.com/astaxie/beego/validation"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"time"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// RobotTable is the name of table in DB that holds the robot object
@ -45,13 +46,13 @@ type RobotQuery struct {
// RobotCreate ...
type RobotCreate struct {
Name string `json:"name"`
ProjectID int64 `json:"pid"`
Description string `json:"description"`
Disabled bool `json:"disabled"`
ExpiresAt int64 `json:"expires_at"`
Visible bool `json:"-"`
Access []*rbac.Policy `json:"access"`
Name string `json:"name"`
ProjectID int64 `json:"pid"`
Description string `json:"description"`
Disabled bool `json:"disabled"`
ExpiresAt int64 `json:"expires_at"`
Visible bool `json:"-"`
Access []*types.Policy `json:"access"`
}
// Pagination ...

View File

@ -2,16 +2,17 @@ package robot
import (
"errors"
"github.com/dgrijalva/jwt-go"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/pkg/permission/types"
)
// Claim implements the interface of jwt.Claims
type Claim struct {
jwt.StandardClaims
TokenID int64 `json:"id"`
ProjectID int64 `json:"pid"`
Access []*rbac.Policy `json:"access"`
TokenID int64 `json:"id"`
ProjectID int64 `json:"pid"`
Access []*types.Policy `json:"access"`
}
// Valid valid the claims "tokenID, projectID and access".

View File

@ -1,18 +1,19 @@
package robot
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/stretchr/testify/assert"
"testing"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/stretchr/testify/assert"
)
func TestValid(t *testing.T) {
rbacPolicy := &rbac.Policy{
rbacPolicy := &types.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies := []*types.Policy{}
policies = append(policies, rbacPolicy)
rClaims := &Claim{
@ -25,11 +26,11 @@ func TestValid(t *testing.T) {
func TestUnValidTokenID(t *testing.T) {
rbacPolicy := &rbac.Policy{
rbacPolicy := &types.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies := []*types.Policy{}
policies = append(policies, rbacPolicy)
rClaims := &Claim{
@ -42,11 +43,11 @@ func TestUnValidTokenID(t *testing.T) {
func TestUnValidProjectID(t *testing.T) {
rbacPolicy := &rbac.Policy{
rbacPolicy := &types.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies := []*types.Policy{}
policies = append(policies, rbacPolicy)
rClaims := &Claim{

View File

@ -6,8 +6,8 @@ import (
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/pkg/permission/types"
robot_claim "github.com/goharbor/harbor/src/pkg/token/claims/robot"
"github.com/stretchr/testify/assert"
)
@ -22,11 +22,11 @@ func TestMain(m *testing.M) {
}
func TestNew(t *testing.T) {
rbacPolicy := &rbac.Policy{
rbacPolicy := &types.Policy{
Resource: "/project/libray/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies := []*types.Policy{}
policies = append(policies, rbacPolicy)
tokenID := int64(123)
@ -50,11 +50,11 @@ func TestNew(t *testing.T) {
}
func TestRaw(t *testing.T) {
rbacPolicy := &rbac.Policy{
rbacPolicy := &types.Policy{
Resource: "/project/library/repository",
Action: "pull",
}
policies := []*rbac.Policy{}
policies := []*types.Policy{}
policies = append(policies, rbacPolicy)
tokenID := int64(123)

View File

@ -27,7 +27,10 @@ import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/core/promgr/metamgr"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/server/middleware"
securitytesting "github.com/goharbor/harbor/src/testing/common/security"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/stretchr/testify/assert"
)
@ -77,56 +80,6 @@ func (mockPM) GetMetadataManager() metamgr.ProjectMetadataManager {
panic("implement me")
}
type mockSC struct{}
func (mockSC) Name() string {
return "mock"
}
func (mockSC) IsAuthenticated() bool {
return true
}
func (mockSC) GetUsername() string {
return "mock"
}
func (mockSC) IsSysAdmin() bool {
return false
}
func (mockSC) IsSolutionUser() bool {
return false
}
func (mockSC) GetMyProjects() ([]*models.Project, error) {
panic("implement me")
}
func (mockSC) GetProjectRoles(projectIDOrName interface{}) []int {
panic("implement me")
}
func (mockSC) Can(action rbac.Action, resource rbac.Resource) bool {
ns, _ := resource.GetNamespace()
perms := map[int64]map[rbac.Action]struct{}{
1: {
rbac.ActionPull: {},
rbac.ActionPush: {},
},
2: {
rbac.ActionPull: {},
},
}
pid := ns.Identity().(int64)
m, ok := perms[pid]
if !ok {
return false
}
_, ok = m[action]
return ok
}
func TestMain(m *testing.M) {
checker = reqChecker{
pm: mockPM{},
@ -141,7 +94,28 @@ func TestMiddleware(t *testing.T) {
w.WriteHeader(200)
})
baseCtx := security.NewContext(context.Background(), mockSC{})
sc := &securitytesting.Context{}
sc.On("IsAuthenticated").Return(true)
sc.On("IsSysAdmin").Return(false)
mock.OnAnything(sc, "Can").Return(func(action types.Action, resource types.Resource) bool {
perms := map[string]map[rbac.Action]struct{}{
"/project/1/repository": {
rbac.ActionPull: {},
rbac.ActionPush: {},
},
"/project/2/repository": {
rbac.ActionPull: {},
},
}
m, ok := perms[resource.String()]
if !ok {
return false
}
_, ok = m[action]
return ok
})
baseCtx := security.NewContext(context.Background(), sc)
ar1 := &middleware.ArtifactInfo{
Repository: "project_1/hello-world",
Reference: "v1",

View File

@ -0,0 +1,17 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
//go:generate mockery -case snake -dir ../../common/security -name Context -output ./security -outpkg security

View File

@ -0,0 +1,138 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package security
import (
models "github.com/goharbor/harbor/src/common/models"
mock "github.com/stretchr/testify/mock"
types "github.com/goharbor/harbor/src/pkg/permission/types"
)
// Context is an autogenerated mock type for the Context type
type Context struct {
mock.Mock
}
// Can provides a mock function with given fields: action, resource
func (_m *Context) Can(action types.Action, resource types.Resource) bool {
ret := _m.Called(action, resource)
var r0 bool
if rf, ok := ret.Get(0).(func(types.Action, types.Resource) bool); ok {
r0 = rf(action, resource)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// GetMyProjects provides a mock function with given fields:
func (_m *Context) GetMyProjects() ([]*models.Project, error) {
ret := _m.Called()
var r0 []*models.Project
if rf, ok := ret.Get(0).(func() []*models.Project); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Project)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetProjectRoles provides a mock function with given fields: projectIDOrName
func (_m *Context) GetProjectRoles(projectIDOrName interface{}) []int {
ret := _m.Called(projectIDOrName)
var r0 []int
if rf, ok := ret.Get(0).(func(interface{}) []int); ok {
r0 = rf(projectIDOrName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
return r0
}
// GetUsername provides a mock function with given fields:
func (_m *Context) GetUsername() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// IsAuthenticated provides a mock function with given fields:
func (_m *Context) IsAuthenticated() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// IsSolutionUser provides a mock function with given fields:
func (_m *Context) IsSolutionUser() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// IsSysAdmin provides a mock function with given fields:
func (_m *Context) IsSysAdmin() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// Name provides a mock function with given fields:
func (_m *Context) Name() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}