feat(role): introduce a limited guest role (#9403)

Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
He Weiwei 2019-10-20 14:21:28 +08:00 committed by GitHub
parent 62451a57d9
commit bf6a14c9ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 227 additions and 222 deletions

View File

@ -10,44 +10,45 @@ System admin have all permissions for the project.
The following table depicts the various user permission levels in a project. The following table depicts the various user permission levels in a project.
| Action | Guest | Developer | Master | Project Admin | | Action | Limited Guest | Guest | Developer | Master | Project Admin |
| --------------------------------------- | ----- | --------- | ------ | ------------- | | --------------------------------------- | ------------- | ----- | --------- | ------ | ------------- |
| See the porject configurations | ✓ | ✓ | ✓ | ✓ | | See the porject configurations | ✓ | ✓ | ✓ | ✓ | ✓ |
| Edit the project configurations | | | | ✓ | | Edit the project configurations | | | | | ✓ |
| See a list of project members | ✓ | ✓ | ✓ | ✓ | | See a list of project members | | ✓ | ✓ | ✓ | ✓ |
| Create/edit/delete project members | | | | ✓ | | Create/edit/delete project members | | | | | ✓ |
| See a list of project logs | ✓ | ✓ | ✓ | ✓ | | See a list of project logs | | ✓ | ✓ | ✓ | ✓ |
| See a list of project replications | | | ✓ | ✓ | | See a list of project replications | | | | ✓ | ✓ |
| See a list of project replication jobs | | | | ✓ | | See a list of project replication jobs | | | | | ✓ |
| See a list of project labels | | | ✓ | ✓ | | See a list of project labels | | | | ✓ | ✓ |
| Create/edit/delete project lables | | | ✓ | ✓ | | Create/edit/delete project lables | | | | ✓ | ✓ |
| See a list of repositories | ✓ | ✓ | ✓ | ✓ | | See a list of repositories | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create repositories | | ✓ | ✓ | ✓ | | Create repositories | | | ✓ | ✓ | ✓ |
| Edit/delete repositories | | | ✓ | ✓ | | Edit/delete repositories | | | | ✓ | ✓ |
| See a list of images | ✓ | ✓ | ✓ | ✓ | | See a list of images | ✓ | ✓ | ✓ | ✓ | ✓ |
| Retag image | ✓ | ✓ | ✓ | ✓ | | Retag image | | ✓ | ✓ | ✓ | ✓ |
| Pull image | ✓ | ✓ | ✓ | ✓ | | Pull image | ✓ | ✓ | ✓ | ✓ | ✓ |
| Push image | | ✓ | ✓ | ✓ | | Push image | | | ✓ | ✓ | ✓ |
| Scan/delete image | | | ✓ | ✓ | | Scan/delete image | | | | ✓ | ✓ |
| See a list of image vulnerabilities | ✓ | ✓ | ✓ | ✓ | | See a list of image vulnerabilities | ✓ | ✓ | ✓ | ✓ | ✓ |
| See image build history | ✓ | ✓ | ✓ | ✓ | | See image build history | ✓ | ✓ | ✓ | ✓ | ✓ |
| Add/Remove labels of image | | ✓ | ✓ | ✓ | | Add/Remove labels of image | | | ✓ | ✓ | ✓ |
| See a list of helm charts | ✓ | ✓ | ✓ | ✓ | | See a list of helm charts | ✓ | ✓ | ✓ | ✓ | ✓ |
| Download helm charts | ✓ | ✓ | ✓ | ✓ | | Download helm charts | ✓ | ✓ | ✓ | ✓ | ✓ |
| Upload helm charts | | ✓ | ✓ | ✓ | | Upload helm charts | | | ✓ | ✓ | ✓ |
| Delete helm charts | | | ✓ | ✓ | | Delete helm charts | | | | ✓ | ✓ |
| See a list of helm chart versions | ✓ | ✓ | ✓ | ✓ | | See a list of helm chart versions | ✓ | ✓ | ✓ | ✓ | ✓ |
| Download helm chart versions | ✓ | ✓ | ✓ | ✓ | | Download helm chart versions | ✓ | ✓ | ✓ | ✓ | ✓ |
| Upload helm chart versions | | ✓ | ✓ | ✓ | | Upload helm chart versions | | | ✓ | ✓ | ✓ |
| Delete helm chart versions | | | ✓ | ✓ | | Delete helm chart versions | | | | ✓ | ✓ |
| Add/Remove labels of helm chart version | | ✓ | ✓ | ✓ | | Add/Remove labels of helm chart version | | | ✓ | ✓ | ✓ |
| See a list of project robots | | | ✓ | ✓ | | See a list of project robots | | | | ✓ | ✓ |
| Create/edit/delete project robots | | | | ✓ | | Create/edit/delete project robots | | | | | ✓ |
| See configured CVE whitelist | ✓ | ✓ | ✓ | ✓ | | See configured CVE whitelist | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create/edit/remove CVE whitelist | | | | ✓ | | Create/edit/remove CVE whitelist | | | | | ✓ |
| Enable/disable webhooks | | ✓ | ✓ | ✓ | | Enable/disable webhooks | | | ✓ | ✓ | ✓ |
| Create/delete tag retention rules | | ✓ | ✓ | ✓ | | Create/delete tag retention rules | | | ✓ | ✓ | ✓ |
| Enable/disable tag retention rules | | ✓ | ✓ | ✓ | | Enable/disable tag retention rules | | | ✓ | ✓ | ✓ |
| See project quotas | ✓ | ✓ | ✓ | ✓ | | See project quotas | ✓ | ✓ | ✓ | ✓ | ✓ |
| Edit project quotas | | | | | | Edit project quotas | | | | | |

View File

@ -57,4 +57,7 @@ DROP TABLE IF EXISTS img_scan_job;
DROP TRIGGER IF EXISTS TRIGGER ON img_scan_overview; DROP TRIGGER IF EXISTS TRIGGER ON img_scan_overview;
DROP TABLE IF EXISTS img_scan_overview; DROP TABLE IF EXISTS img_scan_overview;
DROP TABLE IF EXISTS clair_vuln_timestamp DROP TABLE IF EXISTS clair_vuln_timestamp;
/* Add limited guest role */
INSERT INTO role (role_code, name) VALUES ('LRS', 'limitedGuest');

View File

@ -33,6 +33,7 @@ const (
RoleDeveloper = 2 RoleDeveloper = 2
RoleGuest = 3 RoleGuest = 3
RoleMaster = 4 RoleMaster = 4
RoleLimitedGuest = 5
LabelLevelSystem = "s" LabelLevelSystem = "s"
LabelLevelUser = "u" LabelLevelUser = "u"

View File

@ -78,7 +78,7 @@ func addProjectMember(member models.Member) (int, error) {
func GetProjectByID(id int64) (*models.Project, error) { func GetProjectByID(id int64) (*models.Project, error) {
o := GetOrmer() o := GetOrmer()
sql := `select p.project_id, p.name, u.username as owner_name, p.owner_id, p.creation_time, p.update_time sql := `select p.project_id, p.name, u.username as owner_name, p.owner_id, p.creation_time, p.update_time
from project p left join harbor_user u on p.owner_id = u.user_id where p.deleted = false and p.project_id = ?` from project p left join harbor_user u on p.owner_id = u.user_id where p.deleted = false and p.project_id = ?`
queryParam := make([]interface{}, 1) queryParam := make([]interface{}, 1)
queryParam = append(queryParam, id) queryParam = append(queryParam, id)
@ -142,7 +142,7 @@ func GetTotalOfProjects(query *models.ProjectQueryParam) (int64, error) {
// GetProjects returns a project list according to the query conditions // GetProjects returns a project list according to the query conditions
func GetProjects(query *models.ProjectQueryParam) ([]*models.Project, error) { func GetProjects(query *models.ProjectQueryParam) ([]*models.Project, error) {
sqlStr, queryParam := projectQueryConditions(query) sqlStr, queryParam := projectQueryConditions(query)
sqlStr = `select distinct p.project_id, p.name, p.owner_id, sqlStr = `select distinct p.project_id, p.name, p.owner_id,
p.creation_time, p.update_time ` + sqlStr + ` order by p.name` p.creation_time, p.update_time ` + sqlStr + ` order by p.name`
sqlStr, queryParam = CreatePagination(query, sqlStr, queryParam) sqlStr, queryParam = CreatePagination(query, sqlStr, queryParam)
@ -158,15 +158,15 @@ func GetProjects(query *models.ProjectQueryParam) ([]*models.Project, error) {
// and the user is in the group which is a group member of this project. // and the user is in the group which is a group member of this project.
func GetGroupProjects(groupIDs []int, query *models.ProjectQueryParam) ([]*models.Project, error) { func GetGroupProjects(groupIDs []int, query *models.ProjectQueryParam) ([]*models.Project, error) {
sql, params := projectQueryConditions(query) sql, params := projectQueryConditions(query)
sql = `select distinct p.project_id, p.name, p.owner_id, sql = `select distinct p.project_id, p.name, p.owner_id,
p.creation_time, p.update_time ` + sql p.creation_time, p.update_time ` + sql
groupIDCondition := JoinNumberConditions(groupIDs) groupIDCondition := JoinNumberConditions(groupIDs)
if len(groupIDs) > 0 { if len(groupIDs) > 0 {
sql = fmt.Sprintf( sql = fmt.Sprintf(
`%s union select distinct p.project_id, p.name, p.owner_id, p.creation_time, p.update_time `%s union select distinct p.project_id, p.name, p.owner_id, p.creation_time, p.update_time
from project p from project p
left join project_member pm on p.project_id = pm.project_id left join project_member pm on p.project_id = pm.project_id
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g' left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g'
where ug.id in ( %s )`, where ug.id in ( %s )`,
sql, groupIDCondition) sql, groupIDCondition)
} }
@ -188,11 +188,11 @@ func GetTotalGroupProjects(groupIDs []int, query *models.ProjectQueryParam) (int
sql = `select count(1) ` + sqlCondition sql = `select count(1) ` + sqlCondition
} else { } else {
sql = fmt.Sprintf( sql = fmt.Sprintf(
`select count(1) `select count(1)
from ( select p.project_id %s union select p.project_id from ( select p.project_id %s union select p.project_id
from project p from project p
left join project_member pm on p.project_id = pm.project_id left join project_member pm on p.project_id = pm.project_id
left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g' left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g'
where ug.id in ( %s )) t`, where ug.id in ( %s )) t`,
sqlCondition, groupIDCondition) sqlCondition, groupIDCondition)
} }
@ -254,6 +254,8 @@ func projectQueryConditions(query *models.ProjectQueryParam) (string, []interfac
roleID = 3 roleID = 3
case common.RoleMaster: case common.RoleMaster:
roleID = 4 roleID = 4
case common.RoleLimitedGuest:
roleID = 5
} }
params = append(params, roleID) params = append(params, roleID)
} }
@ -287,8 +289,8 @@ func DeleteProject(id int64) error {
return err return err
} }
name := fmt.Sprintf("%s#%d", project.Name, project.ProjectID) name := fmt.Sprintf("%s#%d", project.Name, project.ProjectID)
sql := `update project sql := `update project
set deleted = true, name = ? set deleted = true, name = ?
where project_id = ?` where project_id = ?`
_, err = GetOrmer().Raw(sql, name, id).Exec() _, err = GetOrmer().Raw(sql, name, id).Exec()
return err return err
@ -304,8 +306,8 @@ func GetRolesByGroupID(projectID int64, groupIDs []int) ([]int, error) {
groupIDCondition := JoinNumberConditions(groupIDs) groupIDCondition := JoinNumberConditions(groupIDs)
o := GetOrmer() o := GetOrmer()
sql := fmt.Sprintf( sql := fmt.Sprintf(
`select distinct pm.role from project_member pm `select distinct pm.role from project_member pm
left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id
where ug.id in ( %s ) and pm.project_id = ?`, where ug.id in ( %s ) and pm.project_id = ?`,
groupIDCondition) groupIDCondition)
log.Debugf("sql for GetRolesByGroupID(project ID: %d, group ids: %v):%v", projectID, groupIDs, sql) log.Debugf("sql for GetRolesByGroupID(project ID: %d, group ids: %v):%v", projectID, groupIDs, sql)

View File

@ -201,6 +201,7 @@ type ProjectSummary struct {
MasterCount int64 `json:"master_count"` MasterCount int64 `json:"master_count"`
DeveloperCount int64 `json:"developer_count"` DeveloperCount int64 `json:"developer_count"`
GuestCount int64 `json:"guest_count"` GuestCount int64 `json:"guest_count"`
LimitedGuestCount int64 `json:"limited_guest_count"`
Quota struct { Quota struct {
Hard types.ResourceList `json:"hard"` Hard types.ResourceList `json:"hard"`

View File

@ -48,124 +48,7 @@ var (
} }
// all policies for the projects // all policies for the projects
allPolicies = []*rbac.Policy{ allPolicies = computeAllPolicies()
{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.ActionList},
{Resource: rbac.ResourceReplication, Action: rbac.ActionCreate},
{Resource: rbac.ResourceReplication, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplication, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceReplication, Action: rbac.ActionDelete},
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionCreate},
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionList},
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionList},
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionCreate},
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceReplicationExecution, Action: rbac.ActionDelete},
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionRead},
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionList},
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionCreate},
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionUpdate},
{Resource: rbac.ResourceReplicationTask, Action: rbac.ActionDelete},
{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.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.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.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.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},
}
) )
// PoliciesForPublicProject ... // PoliciesForPublicProject ...
@ -197,3 +80,19 @@ func GetAllPolicies(namespace rbac.Namespace) []*rbac.Policy {
return policies 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

@ -299,6 +299,28 @@ var (
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead}, {Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
{Resource: rbac.ResourceRobot, Action: rbac.ActionList}, {Resource: rbac.ResourceRobot, Action: rbac.ActionList},
}, },
"limitedGuest": {
{Resource: rbac.ResourceSelf, 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},
},
} }
) )
@ -319,6 +341,8 @@ func (role *visitorRole) GetRoleName() string {
return "developer" return "developer"
case common.RoleGuest: case common.RoleGuest:
return "guest" return "guest"
case common.RoleLimitedGuest:
return "limitedGuest"
default: default:
return "" return ""
} }

View File

@ -35,6 +35,9 @@ func (suite *VisitorRoleTestSuite) TestGetRoleName() {
guest := visitorRole{roleID: common.RoleGuest} guest := visitorRole{roleID: common.RoleGuest}
suite.Equal(guest.GetRoleName(), "guest") suite.Equal(guest.GetRoleName(), "guest")
limitedGuest := visitorRole{roleID: common.RoleLimitedGuest}
suite.Equal(limitedGuest.GetRoleName(), "limitedGuest")
unknow := visitorRole{roleID: 404} unknow := visitorRole{roleID: 404}
suite.Equal(unknow.GetRoleName(), "") suite.Equal(unknow.GetRoleName(), "")
} }

View File

@ -125,6 +125,8 @@ func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int {
roles = append(roles, common.RoleDeveloper) roles = append(roles, common.RoleDeveloper)
case "RS": case "RS":
roles = append(roles, common.RoleGuest) roles = append(roles, common.RoleGuest)
case "LRS":
roles = append(roles, common.RoleLimitedGuest)
} }
} }
return mergeRoles(roles, s.GetRolesByGroup(projectIDOrName)) return mergeRoles(roles, s.GetRolesByGroup(projectIDOrName))

View File

@ -685,6 +685,7 @@ func getProjectMemberSummary(projectID int64, summary *models.ProjectSummary) {
{common.RoleMaster, &summary.MasterCount}, {common.RoleMaster, &summary.MasterCount},
{common.RoleDeveloper, &summary.DeveloperCount}, {common.RoleDeveloper, &summary.DeveloperCount},
{common.RoleGuest, &summary.GuestCount}, {common.RoleGuest, &summary.GuestCount},
{common.RoleLimitedGuest, &summary.LimitedGuestCount},
} { } {
wg.Add(1) wg.Add(1)
go func(role int, count *int64) { go func(role int, count *int64) {

View File

@ -191,7 +191,7 @@ func (pma *ProjectMemberAPI) Put() {
pma.SendBadRequestError(err) pma.SendBadRequestError(err)
return return
} }
if req.Role < 1 || req.Role > 4 { if !isValidRole(req.Role) {
pma.SendBadRequestError(fmt.Errorf("Invalid role id %v", req.Role)) pma.SendBadRequestError(fmt.Errorf("Invalid role id %v", req.Role))
return return
} }
@ -284,9 +284,22 @@ func AddProjectMember(projectID int64, request models.MemberReq) (int, error) {
return 0, ErrDuplicateProjectMember return 0, ErrDuplicateProjectMember
} }
if member.Role < 1 || member.Role > 4 { if !isValidRole(member.Role) {
// Return invalid role error // Return invalid role error
return 0, ErrInvalidRole return 0, ErrInvalidRole
} }
return project.AddProjectMember(member) return project.AddProjectMember(member)
} }
func isValidRole(role int) bool {
switch role {
case common.RoleProjectAdmin,
common.RoleMaster,
common.RoleDeveloper,
common.RoleGuest,
common.RoleLimitedGuest:
return true
default:
return false
}
}

View File

@ -344,3 +344,28 @@ func TestProjectMemberAPI_PutAndDelete(t *testing.T) {
runCodeCheckingCases(t, cases...) runCodeCheckingCases(t, cases...)
} }
func Test_isValidRole(t *testing.T) {
type args struct {
role int
}
tests := []struct {
name string
args args
want bool
}{
{"project admin", args{1}, true},
{"master", args{4}, true},
{"developer", args{2}, true},
{"guest", args{3}, true},
{"limited guest", args{5}, true},
{"unknow", args{6}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isValidRole(tt.args.role); got != tt.want {
t.Errorf("isValidRole() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -145,6 +145,11 @@ export const PROJECT_ROOTS = [
NAME: "guest", NAME: "guest",
VALUE: 3, VALUE: 3,
LABEL: "GROUP.GUEST" LABEL: "GROUP.GUEST"
},
{
NAME: "limited",
VALUE: 5,
LABEL: "GROUP.LIMITED_GUEST"
} }
]; ];

View File

@ -45,6 +45,10 @@
<input clrRadio type="radio" name="member_role" id="checkrads_guest" [value]=3 [(ngModel)]="member.role_id"> <input clrRadio type="radio" name="member_role" id="checkrads_guest" [value]=3 [(ngModel)]="member.role_id">
<label for="checkrads_guest">{{'MEMBER.GUEST' | translate}}</label> <label for="checkrads_guest">{{'MEMBER.GUEST' | translate}}</label>
</clr-radio-wrapper> </clr-radio-wrapper>
<clr-radio-wrapper>
<input clrRadio type="radio" name="member_role" id="checkrads_limited_guest" [value]=5 [(ngModel)]="member.role_id">
<label for="checkrads_limited_guest">{{'MEMBER.LIMITED_GUEST' | translate}}</label>
</clr-radio-wrapper>
</div> </div>
</div> </div>
</form> </form>

View File

@ -27,6 +27,7 @@
<button clrDropdownItem (click)="changeMembersRole(selectedRow, 4)" [disabled]="!(selectedRow.length && hasUpdateMemberPermission) || onlySelf">{{'MEMBER.PROJECT_MASTER' | translate}}</button> <button clrDropdownItem (click)="changeMembersRole(selectedRow, 4)" [disabled]="!(selectedRow.length && hasUpdateMemberPermission) || onlySelf">{{'MEMBER.PROJECT_MASTER' | translate}}</button>
<button clrDropdownItem (click)="changeMembersRole(selectedRow, 2)" [disabled]="!(selectedRow.length && hasUpdateMemberPermission) || onlySelf">{{'MEMBER.DEVELOPER' | translate}}</button> <button clrDropdownItem (click)="changeMembersRole(selectedRow, 2)" [disabled]="!(selectedRow.length && hasUpdateMemberPermission) || onlySelf">{{'MEMBER.DEVELOPER' | translate}}</button>
<button clrDropdownItem (click)="changeMembersRole(selectedRow, 3)" [disabled]="!(selectedRow.length && hasUpdateMemberPermission) || onlySelf">{{'MEMBER.GUEST' | translate}}</button> <button clrDropdownItem (click)="changeMembersRole(selectedRow, 3)" [disabled]="!(selectedRow.length && hasUpdateMemberPermission) || onlySelf">{{'MEMBER.GUEST' | translate}}</button>
<button clrDropdownItem (click)="changeMembersRole(selectedRow, 5)" [disabled]="!(selectedRow.length && hasUpdateMemberPermission) || onlySelf">{{'MEMBER.LIMITED_GUEST' | translate}}</button>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<button clrDropdownItem (click)="openDeleteMembersDialog(selectedRow)" [disabled]="!(selectedRow.length && hasDeleteMemberPermission) || onlySelf">{{'MEMBER.REMOVE' | translate}}</button> <button clrDropdownItem (click)="openDeleteMembersDialog(selectedRow)" [disabled]="!(selectedRow.length && hasDeleteMemberPermission) || onlySelf">{{'MEMBER.REMOVE' | translate}}</button>
</clr-dropdown-menu> </clr-dropdown-menu>

View File

@ -19,6 +19,7 @@
<li>{{ summaryInformation?.master_count }} {{'SUMMARY.MASTER' | translate}}</li> <li>{{ summaryInformation?.master_count }} {{'SUMMARY.MASTER' | translate}}</li>
<li>{{ summaryInformation?.developer_count }} {{'SUMMARY.DEVELOPER' | translate}}</li> <li>{{ summaryInformation?.developer_count }} {{'SUMMARY.DEVELOPER' | translate}}</li>
<li>{{ summaryInformation?.guest_count }} {{'SUMMARY.GUEST' | translate}}</li> <li>{{ summaryInformation?.guest_count }} {{'SUMMARY.GUEST' | translate}}</li>
<li>{{ summaryInformation?.limited_guest_count }} {{'SUMMARY.LIMITED_GUEST' | translate}}</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -21,7 +21,8 @@ import {
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../../shared/session.service';
import { ProjectService } from '@harbor/ui'; import { ProjectService } from '@harbor/ui';
import { CommonRoutes } from '@harbor/ui'; import { CommonRoutes } from '@harbor/ui';
import { Observable } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
@Injectable() @Injectable()
export class MemberGuard implements CanActivate, CanActivateChild { export class MemberGuard implements CanActivate, CanActivateChild {
@ -31,49 +32,39 @@ export class MemberGuard implements CanActivate, CanActivateChild {
private router: Router) {} private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
let projectId = route.params['id']; const projectId = route.params['id'];
this.sessionService.setProjectMembers([]); this.sessionService.setProjectMembers([]);
return new Observable((observer) => {
let user = this.sessionService.getCurrentUser();
if (user === null) {
this.sessionService.retrieveUser()
.subscribe(() => {
this.checkMemberStatus(state.url, projectId).subscribe((res) => observer.next(res));
}, error => {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
observer.next(false);
});
} else {
this.checkMemberStatus(state.url, projectId).subscribe((res) => observer.next(res));
}
});
}
checkMemberStatus(url: string, projectId: number): Observable<boolean> { const user = this.sessionService.getCurrentUser();
return new Observable<boolean>((observer) => { if (user !== null) {
this.projectService.checkProjectMember(projectId) return this.hasProjectPerm(state.url, projectId);
.subscribe(res => { }
this.sessionService.setProjectMembers(res);
return observer.next(true); return this.sessionService.retrieveUser().pipe(
},
() => { () => {
// Add exception for repository in project detail router activation. return this.hasProjectPerm(state.url, projectId);
this.projectService.getProject(projectId).subscribe(project => { },
if (project.metadata && project.metadata.public === 'true') { catchError(err => {
return observer.next(true); this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
} return of(false);
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]); })
return observer.next(false); );
},
() => {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return observer.next(false);
});
});
});
} }
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean { canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
return this.canActivate(route, state); return this.canActivate(route, state);
} }
hasProjectPerm(url: string, projectId: number): Observable<boolean> {
// Note: current user will have the permission to visit the project when the user can get response from GET /projects/:id API.
return this.projectService.getProject(projectId).pipe(
map(() => {
return true;
}),
catchError(err => {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return of(false);
})
);
}
} }

View File

@ -60,14 +60,29 @@ export const enum ConfirmationButtons {
} }
export const ProjectTypes = { 0: 'PROJECT.ALL_PROJECTS', 1: 'PROJECT.PRIVATE_PROJECTS', 2: 'PROJECT.PUBLIC_PROJECTS' }; export const ProjectTypes = { 0: 'PROJECT.ALL_PROJECTS', 1: 'PROJECT.PRIVATE_PROJECTS', 2: 'PROJECT.PUBLIC_PROJECTS' };
export const RoleInfo = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST', 4: 'MEMBER.PROJECT_MASTER' };
export const RoleMapping = { 'projectAdmin': 'MEMBER.PROJECT_ADMIN', export const RoleInfo = {
'master': 'MEMBER.PROJECT_MASTER', 'developer': 'MEMBER.DEVELOPER', 'guest': 'MEMBER.GUEST' }; 1: "MEMBER.PROJECT_ADMIN",
2: "MEMBER.DEVELOPER",
3: "MEMBER.GUEST",
4: "MEMBER.PROJECT_MASTER",
5: "MEMBER.LIMITED_GUEST",
};
export const RoleMapping = {
"projectAdmin": "MEMBER.PROJECT_ADMIN",
"master": "MEMBER.PROJECT_MASTER",
"developer": "MEMBER.DEVELOPER",
"guest": "MEMBER.GUEST",
"limitedGuest": "MEMBER.LIMITED_GUEST",
};
export const ProjectRoles = [ export const ProjectRoles = [
{ id: 1, value: "MEMBER.PROJECT_ADMIN" }, { id: 1, value: "MEMBER.PROJECT_ADMIN" },
{ id: 2, value: "MEMBER.DEVELOPER" }, { id: 2, value: "MEMBER.DEVELOPER" },
{ id: 3, value: "MEMBER.GUEST" }, { id: 3, value: "MEMBER.GUEST" },
{ id: 4, value: "MEMBER.PROJECT_MASTER" }, { id: 4, value: "MEMBER.PROJECT_MASTER" },
{ id: 5, value: "MEMBER.LIMITED_GUEST" },
]; ];
export enum Roles { export enum Roles {
@ -75,6 +90,7 @@ export enum Roles {
PROJECT_MASTER = 4, PROJECT_MASTER = 4,
DEVELOPER = 2, DEVELOPER = 2,
GUEST = 3, GUEST = 3,
LIMITED_GUEST = 5,
OTHER = 0, OTHER = 0,
} }
export const DefaultHelmIcon = '/images/helm-gray.svg'; export const DefaultHelmIcon = '/images/helm-gray.svg';

View File

@ -275,6 +275,7 @@
"PROJECT_MASTER": "Master", "PROJECT_MASTER": "Master",
"DEVELOPER": "Developer", "DEVELOPER": "Developer",
"GUEST": "Guest", "GUEST": "Guest",
"LIMITED_GUEST": "Limited Guest",
"DELETE": "Delete", "DELETE": "Delete",
"ITEMS": "items", "ITEMS": "items",
"ACTIONS": "Actions", "ACTIONS": "Actions",
@ -737,7 +738,8 @@
"ADMIN": "Admin(s)", "ADMIN": "Admin(s)",
"MASTER": "Master(s)", "MASTER": "Master(s)",
"DEVELOPER": "Developer(s)", "DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)" "GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
}, },
"ALERT": { "ALERT": {
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?" "FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"

View File

@ -276,6 +276,7 @@
"PROJECT_MASTER": "Mantenedor", "PROJECT_MASTER": "Mantenedor",
"DEVELOPER": "Desarrollador", "DEVELOPER": "Desarrollador",
"GUEST": "Invitado", "GUEST": "Invitado",
"LIMITED_GUEST": "Limited Guest",
"DELETE": "Eliminar", "DELETE": "Eliminar",
"ITEMS": "elementos", "ITEMS": "elementos",
"ACTIONS": "Acciones", "ACTIONS": "Acciones",
@ -738,7 +739,8 @@
"ADMIN": "Admin(s)", "ADMIN": "Admin(s)",
"MASTER": "Master(s)", "MASTER": "Master(s)",
"DEVELOPER": "Developer(s)", "DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)" "GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
}, },
"ALERT": { "ALERT": {
"FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?" "FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?"

View File

@ -288,6 +288,7 @@
"PROJECT_MASTER": "préposé à la maintenance", "PROJECT_MASTER": "préposé à la maintenance",
"DEVELOPER": "Développeur", "DEVELOPER": "Développeur",
"GUEST": "Invité", "GUEST": "Invité",
"LIMITED_GUEST": "Limited Guest",
"DELETE": "Supprimer", "DELETE": "Supprimer",
"ITEMS": "items", "ITEMS": "items",
"ACTIONS": "Actions", "ACTIONS": "Actions",
@ -724,7 +725,8 @@
"ADMIN": "Admin(s)", "ADMIN": "Admin(s)",
"MASTER": "Master(s)", "MASTER": "Master(s)",
"DEVELOPER": "Developer(s)", "DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)" "GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
}, },
"ALERT": { "ALERT": {
"FORM_CHANGE_CONFIRMATION": "Certaines modifications ne sont pas encore enregistrées. Voulez-vous annuler ?" "FORM_CHANGE_CONFIRMATION": "Certaines modifications ne sont pas encore enregistrées. Voulez-vous annuler ?"

View File

@ -273,6 +273,7 @@
"PROJECT_MASTER": "Mantenedor", "PROJECT_MASTER": "Mantenedor",
"DEVELOPER": "Desenvolvedor", "DEVELOPER": "Desenvolvedor",
"GUEST": "Visitante", "GUEST": "Visitante",
"LIMITED_GUEST": "Limited Guest",
"DELETE": "Remover", "DELETE": "Remover",
"ITEMS": "itens", "ITEMS": "itens",
"ACTIONS": "Ações", "ACTIONS": "Ações",
@ -733,7 +734,8 @@
"ADMIN": "Admin(s)", "ADMIN": "Admin(s)",
"MASTER": "Master(s)", "MASTER": "Master(s)",
"DEVELOPER": "Developer(s)", "DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)" "GUEST": "Guest(s)",
"LIMITED_GUEST": "Limited guest(s)"
}, },
"ALERT": { "ALERT": {
"FORM_CHANGE_CONFIRMATION": "Algumas alterações ainda não foram salvas. Você deseja cancelar?" "FORM_CHANGE_CONFIRMATION": "Algumas alterações ainda não foram salvas. Você deseja cancelar?"

View File

@ -275,6 +275,7 @@
"PROJECT_MASTER": "Uzman", "PROJECT_MASTER": "Uzman",
"DEVELOPER": "Geliştirici", "DEVELOPER": "Geliştirici",
"GUEST": "Konuk", "GUEST": "Konuk",
"LIMITED_GUEST": "Limited Guest",
"DELETE": "Sil", "DELETE": "Sil",
"ITEMS": "çeşit", "ITEMS": "çeşit",
"ACTIONS": "Eylemler", "ACTIONS": "Eylemler",
@ -736,7 +737,8 @@
"ADMIN": "Yönetici(ler)", "ADMIN": "Yönetici(ler)",
"MASTER": "Uzman(lar)", "MASTER": "Uzman(lar)",
"DEVELOPER": "Geliştirici(ler)", "DEVELOPER": "Geliştirici(ler)",
"GUEST": "Misafir(ler)" "GUEST": "Misafir(ler)",
"LIMITED_GUEST": "Limited guest(s)"
}, },
"ALERT": { "ALERT": {
"FORM_CHANGE_CONFIRMATION": "Bazı değişiklikler henüz kaydedilmedi. İptal etmek istiyor musun?" "FORM_CHANGE_CONFIRMATION": "Bazı değişiklikler henüz kaydedilmedi. İptal etmek istiyor musun?"

View File

@ -275,6 +275,7 @@
"PROJECT_MASTER": "维护人员", "PROJECT_MASTER": "维护人员",
"DEVELOPER": "开发人员", "DEVELOPER": "开发人员",
"GUEST": "访客", "GUEST": "访客",
"LIMITED_GUEST": "受限访客",
"DELETE": "删除", "DELETE": "删除",
"ITEMS": "条记录", "ITEMS": "条记录",
"ACTIONS": "操作", "ACTIONS": "操作",
@ -738,7 +739,8 @@
"ADMIN": "管理员", "ADMIN": "管理员",
"MASTER": "维护人员", "MASTER": "维护人员",
"DEVELOPER": "开发者", "DEVELOPER": "开发者",
"GUEST": "访客" "GUEST": "访客",
"LIMITED_GUEST": "受限访客"
}, },
"ALERT": { "ALERT": {
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?" "FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"