From bf6a14c9ade879ead8f62c3844992750dfcfbe6e Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Sun, 20 Oct 2019 14:21:28 +0800 Subject: [PATCH] feat(role): introduce a limited guest role (#9403) Signed-off-by: He Weiwei --- docs/permissions.md | 81 +++++------ .../postgresql/0015_1.10.0_schema.up.sql | 5 +- src/common/const.go | 1 + src/common/dao/project.go | 30 ++-- src/common/models/project.go | 1 + src/common/rbac/project/util.go | 135 +++--------------- src/common/rbac/project/visitor_role.go | 24 ++++ src/common/rbac/project/visitor_role_test.go | 3 + src/common/security/local/context.go | 2 + src/core/api/project.go | 1 + src/core/api/projectmember.go | 17 ++- src/core/api/projectmember_test.go | 25 ++++ src/portal/lib/src/shared/shared.const.ts | 5 + .../add-member/add-member.component.html | 4 + .../app/project/member/member.component.html | 1 + .../project/summary/summary.component.html | 1 + .../route/member-guard-activate.service.ts | 67 ++++----- src/portal/src/app/shared/shared.const.ts | 22 ++- src/portal/src/i18n/lang/en-us-lang.json | 4 +- src/portal/src/i18n/lang/es-es-lang.json | 4 +- src/portal/src/i18n/lang/fr-fr-lang.json | 4 +- src/portal/src/i18n/lang/pt-br-lang.json | 4 +- src/portal/src/i18n/lang/tr-tr-lang.json | 4 +- src/portal/src/i18n/lang/zh-cn-lang.json | 4 +- 24 files changed, 227 insertions(+), 222 deletions(-) diff --git a/docs/permissions.md b/docs/permissions.md index 2543ef3c3..b9a7bac3b 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -10,44 +10,45 @@ System admin have all permissions for the project. The following table depicts the various user permission levels in a project. -| Action | Guest | Developer | Master | Project Admin | -| --------------------------------------- | ----- | --------- | ------ | ------------- | -| See the porject configurations | ✓ | ✓ | ✓ | ✓ | -| Edit the project configurations | | | | ✓ | -| See a list of project members | ✓ | ✓ | ✓ | ✓ | -| Create/edit/delete project members | | | | ✓ | -| See a list of project logs | ✓ | ✓ | ✓ | ✓ | -| See a list of project replications | | | ✓ | ✓ | -| See a list of project replication jobs | | | | ✓ | -| See a list of project labels | | | ✓ | ✓ | -| Create/edit/delete project lables | | | ✓ | ✓ | -| See a list of repositories | ✓ | ✓ | ✓ | ✓ | -| Create repositories | | ✓ | ✓ | ✓ | -| Edit/delete repositories | | | ✓ | ✓ | -| See a list of images | ✓ | ✓ | ✓ | ✓ | -| Retag image | ✓ | ✓ | ✓ | ✓ | -| Pull image | ✓ | ✓ | ✓ | ✓ | -| Push image | | ✓ | ✓ | ✓ | -| Scan/delete image | | | ✓ | ✓ | -| See a list of image vulnerabilities | ✓ | ✓ | ✓ | ✓ | -| See image build history | ✓ | ✓ | ✓ | ✓ | -| Add/Remove labels of image | | ✓ | ✓ | ✓ | -| See a list of helm charts | ✓ | ✓ | ✓ | ✓ | -| Download helm charts | ✓ | ✓ | ✓ | ✓ | -| Upload helm charts | | ✓ | ✓ | ✓ | -| Delete helm charts | | | ✓ | ✓ | -| See a list of helm chart versions | ✓ | ✓ | ✓ | ✓ | -| Download helm chart versions | ✓ | ✓ | ✓ | ✓ | -| Upload helm chart versions | | ✓ | ✓ | ✓ | -| Delete helm chart versions | | | ✓ | ✓ | -| Add/Remove labels of helm chart version | | ✓ | ✓ | ✓ | -| See a list of project robots | | | ✓ | ✓ | -| Create/edit/delete project robots | | | | ✓ | -| See configured CVE whitelist | ✓ | ✓ | ✓ | ✓ | -| Create/edit/remove CVE whitelist | | | | ✓ | -| Enable/disable webhooks | | ✓ | ✓ | ✓ | -| Create/delete tag retention rules | | ✓ | ✓ | ✓ | -| Enable/disable tag retention rules | | ✓ | ✓ | ✓ | -| See project quotas | ✓ | ✓ | ✓ | ✓ | -| Edit project quotas | | | | | +| Action | Limited Guest | Guest | Developer | Master | Project Admin | +| --------------------------------------- | ------------- | ----- | --------- | ------ | ------------- | +| See the porject configurations | ✓ | ✓ | ✓ | ✓ | ✓ | +| Edit the project configurations | | | | | ✓ | +| See a list of project members | | ✓ | ✓ | ✓ | ✓ | +| Create/edit/delete project members | | | | | ✓ | +| See a list of project logs | | ✓ | ✓ | ✓ | ✓ | +| See a list of project replications | | | | ✓ | ✓ | +| See a list of project replication jobs | | | | | ✓ | +| See a list of project labels | | | | ✓ | ✓ | +| Create/edit/delete project lables | | | | ✓ | ✓ | +| See a list of repositories | ✓ | ✓ | ✓ | ✓ | ✓ | +| Create repositories | | | ✓ | ✓ | ✓ | +| Edit/delete repositories | | | | ✓ | ✓ | +| See a list of images | ✓ | ✓ | ✓ | ✓ | ✓ | +| Retag image | | ✓ | ✓ | ✓ | ✓ | +| Pull image | ✓ | ✓ | ✓ | ✓ | ✓ | +| Push image | | | ✓ | ✓ | ✓ | +| Scan/delete image | | | | ✓ | ✓ | +| See a list of image vulnerabilities | ✓ | ✓ | ✓ | ✓ | ✓ | +| See image build history | ✓ | ✓ | ✓ | ✓ | ✓ | +| Add/Remove labels of image | | | ✓ | ✓ | ✓ | +| See a list of helm charts | ✓ | ✓ | ✓ | ✓ | ✓ | +| Download helm charts | ✓ | ✓ | ✓ | ✓ | ✓ | +| Upload helm charts | | | ✓ | ✓ | ✓ | +| Delete helm charts | | | | ✓ | ✓ | +| See a list of helm chart versions | ✓ | ✓ | ✓ | ✓ | ✓ | +| Download helm chart versions | ✓ | ✓ | ✓ | ✓ | ✓ | +| Upload helm chart versions | | | ✓ | ✓ | ✓ | +| Delete helm chart versions | | | | ✓ | ✓ | +| Add/Remove labels of helm chart version | | | ✓ | ✓ | ✓ | +| See a list of project robots | | | | ✓ | ✓ | +| Create/edit/delete project robots | | | | | ✓ | +| See configured CVE whitelist | ✓ | ✓ | ✓ | ✓ | ✓ | +| Create/edit/remove CVE whitelist | | | | | ✓ | +| Enable/disable webhooks | | | ✓ | ✓ | ✓ | +| Create/delete tag retention rules | | | ✓ | ✓ | ✓ | +| Enable/disable tag retention rules | | | ✓ | ✓ | ✓ | +| See project quotas | ✓ | ✓ | ✓ | ✓ | ✓ | +| Edit project quotas | | | | | | + diff --git a/make/migrations/postgresql/0015_1.10.0_schema.up.sql b/make/migrations/postgresql/0015_1.10.0_schema.up.sql index 6b7e860d9..d4cc01997 100644 --- a/make/migrations/postgresql/0015_1.10.0_schema.up.sql +++ b/make/migrations/postgresql/0015_1.10.0_schema.up.sql @@ -57,4 +57,7 @@ DROP TABLE IF EXISTS img_scan_job; DROP TRIGGER IF EXISTS TRIGGER ON img_scan_overview; DROP TABLE IF EXISTS img_scan_overview; -DROP TABLE IF EXISTS clair_vuln_timestamp \ No newline at end of file +DROP TABLE IF EXISTS clair_vuln_timestamp; + +/* Add limited guest role */ +INSERT INTO role (role_code, name) VALUES ('LRS', 'limitedGuest'); diff --git a/src/common/const.go b/src/common/const.go index c051762ff..62a577eff 100755 --- a/src/common/const.go +++ b/src/common/const.go @@ -33,6 +33,7 @@ const ( RoleDeveloper = 2 RoleGuest = 3 RoleMaster = 4 + RoleLimitedGuest = 5 LabelLevelSystem = "s" LabelLevelUser = "u" diff --git a/src/common/dao/project.go b/src/common/dao/project.go index e027ec221..764601aa3 100644 --- a/src/common/dao/project.go +++ b/src/common/dao/project.go @@ -78,7 +78,7 @@ func addProjectMember(member models.Member) (int, error) { func GetProjectByID(id int64) (*models.Project, error) { 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 = ?` queryParam := make([]interface{}, 1) 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 func GetProjects(query *models.ProjectQueryParam) ([]*models.Project, error) { 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` 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. func GetGroupProjects(groupIDs []int, query *models.ProjectQueryParam) ([]*models.Project, error) { 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 groupIDCondition := JoinNumberConditions(groupIDs) if len(groupIDs) > 0 { sql = fmt.Sprintf( - `%s union select distinct p.project_id, p.name, p.owner_id, p.creation_time, p.update_time - from project p + `%s union select distinct p.project_id, p.name, p.owner_id, p.creation_time, p.update_time + from project p 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 )`, sql, groupIDCondition) } @@ -188,11 +188,11 @@ func GetTotalGroupProjects(groupIDs []int, query *models.ProjectQueryParam) (int sql = `select count(1) ` + sqlCondition } else { sql = fmt.Sprintf( - `select count(1) - from ( select p.project_id %s union select p.project_id - from project p + `select count(1) + from ( select p.project_id %s union select p.project_id + from project p 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`, sqlCondition, groupIDCondition) } @@ -254,6 +254,8 @@ func projectQueryConditions(query *models.ProjectQueryParam) (string, []interfac roleID = 3 case common.RoleMaster: roleID = 4 + case common.RoleLimitedGuest: + roleID = 5 } params = append(params, roleID) } @@ -287,8 +289,8 @@ func DeleteProject(id int64) error { return err } name := fmt.Sprintf("%s#%d", project.Name, project.ProjectID) - sql := `update project - set deleted = true, name = ? + sql := `update project + set deleted = true, name = ? where project_id = ?` _, err = GetOrmer().Raw(sql, name, id).Exec() return err @@ -304,8 +306,8 @@ func GetRolesByGroupID(projectID int64, groupIDs []int) ([]int, error) { groupIDCondition := JoinNumberConditions(groupIDs) o := GetOrmer() sql := fmt.Sprintf( - `select distinct pm.role from project_member pm - left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id + `select distinct pm.role from project_member pm + 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 = ?`, groupIDCondition) log.Debugf("sql for GetRolesByGroupID(project ID: %d, group ids: %v):%v", projectID, groupIDs, sql) diff --git a/src/common/models/project.go b/src/common/models/project.go index 1b56284a3..38e1622bb 100644 --- a/src/common/models/project.go +++ b/src/common/models/project.go @@ -201,6 +201,7 @@ type ProjectSummary struct { MasterCount int64 `json:"master_count"` DeveloperCount int64 `json:"developer_count"` GuestCount int64 `json:"guest_count"` + LimitedGuestCount int64 `json:"limited_guest_count"` Quota struct { Hard types.ResourceList `json:"hard"` diff --git a/src/common/rbac/project/util.go b/src/common/rbac/project/util.go index 85116fe21..63ed661c8 100644 --- a/src/common/rbac/project/util.go +++ b/src/common/rbac/project/util.go @@ -48,124 +48,7 @@ var ( } // all policies for the projects - allPolicies = []*rbac.Policy{ - {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}, - } + allPolicies = computeAllPolicies() ) // PoliciesForPublicProject ... @@ -197,3 +80,19 @@ func GetAllPolicies(namespace rbac.Namespace) []*rbac.Policy { 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 +} diff --git a/src/common/rbac/project/visitor_role.go b/src/common/rbac/project/visitor_role.go index d8d594f4f..41ee7205e 100755 --- a/src/common/rbac/project/visitor_role.go +++ b/src/common/rbac/project/visitor_role.go @@ -299,6 +299,28 @@ var ( {Resource: rbac.ResourceRobot, Action: rbac.ActionRead}, {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" case common.RoleGuest: return "guest" + case common.RoleLimitedGuest: + return "limitedGuest" default: return "" } diff --git a/src/common/rbac/project/visitor_role_test.go b/src/common/rbac/project/visitor_role_test.go index b1f22d24a..e2cc7c2e2 100644 --- a/src/common/rbac/project/visitor_role_test.go +++ b/src/common/rbac/project/visitor_role_test.go @@ -35,6 +35,9 @@ func (suite *VisitorRoleTestSuite) TestGetRoleName() { 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(), "") } diff --git a/src/common/security/local/context.go b/src/common/security/local/context.go index b246ae2e8..a80130da8 100644 --- a/src/common/security/local/context.go +++ b/src/common/security/local/context.go @@ -125,6 +125,8 @@ func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int { roles = append(roles, common.RoleDeveloper) case "RS": roles = append(roles, common.RoleGuest) + case "LRS": + roles = append(roles, common.RoleLimitedGuest) } } return mergeRoles(roles, s.GetRolesByGroup(projectIDOrName)) diff --git a/src/core/api/project.go b/src/core/api/project.go index 40559991a..51bcb3d67 100644 --- a/src/core/api/project.go +++ b/src/core/api/project.go @@ -685,6 +685,7 @@ func getProjectMemberSummary(projectID int64, summary *models.ProjectSummary) { {common.RoleMaster, &summary.MasterCount}, {common.RoleDeveloper, &summary.DeveloperCount}, {common.RoleGuest, &summary.GuestCount}, + {common.RoleLimitedGuest, &summary.LimitedGuestCount}, } { wg.Add(1) go func(role int, count *int64) { diff --git a/src/core/api/projectmember.go b/src/core/api/projectmember.go index b704ad5c6..54e0707cd 100644 --- a/src/core/api/projectmember.go +++ b/src/core/api/projectmember.go @@ -191,7 +191,7 @@ func (pma *ProjectMemberAPI) Put() { pma.SendBadRequestError(err) return } - if req.Role < 1 || req.Role > 4 { + if !isValidRole(req.Role) { pma.SendBadRequestError(fmt.Errorf("Invalid role id %v", req.Role)) return } @@ -284,9 +284,22 @@ func AddProjectMember(projectID int64, request models.MemberReq) (int, error) { return 0, ErrDuplicateProjectMember } - if member.Role < 1 || member.Role > 4 { + if !isValidRole(member.Role) { // Return invalid role error return 0, ErrInvalidRole } 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 + } +} diff --git a/src/core/api/projectmember_test.go b/src/core/api/projectmember_test.go index 89aab304b..306fe138f 100644 --- a/src/core/api/projectmember_test.go +++ b/src/core/api/projectmember_test.go @@ -344,3 +344,28 @@ func TestProjectMemberAPI_PutAndDelete(t *testing.T) { 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) + } + }) + } +} diff --git a/src/portal/lib/src/shared/shared.const.ts b/src/portal/lib/src/shared/shared.const.ts index 8e76581d9..4a36ed52c 100644 --- a/src/portal/lib/src/shared/shared.const.ts +++ b/src/portal/lib/src/shared/shared.const.ts @@ -145,6 +145,11 @@ export const PROJECT_ROOTS = [ NAME: "guest", VALUE: 3, LABEL: "GROUP.GUEST" + }, + { + NAME: "limited", + VALUE: 5, + LABEL: "GROUP.LIMITED_GUEST" } ]; diff --git a/src/portal/src/app/project/member/add-member/add-member.component.html b/src/portal/src/app/project/member/add-member/add-member.component.html index ee2f5f1ff..de4028ee1 100644 --- a/src/portal/src/app/project/member/add-member/add-member.component.html +++ b/src/portal/src/app/project/member/add-member/add-member.component.html @@ -45,6 +45,10 @@ + + + + diff --git a/src/portal/src/app/project/member/member.component.html b/src/portal/src/app/project/member/member.component.html index a357c6d1f..3d67d57a8 100644 --- a/src/portal/src/app/project/member/member.component.html +++ b/src/portal/src/app/project/member/member.component.html @@ -27,6 +27,7 @@ + diff --git a/src/portal/src/app/project/summary/summary.component.html b/src/portal/src/app/project/summary/summary.component.html index 70d640c1a..be804c6ab 100644 --- a/src/portal/src/app/project/summary/summary.component.html +++ b/src/portal/src/app/project/summary/summary.component.html @@ -19,6 +19,7 @@
  • {{ summaryInformation?.master_count }} {{'SUMMARY.MASTER' | translate}}
  • {{ summaryInformation?.developer_count }} {{'SUMMARY.DEVELOPER' | translate}}
  • {{ summaryInformation?.guest_count }} {{'SUMMARY.GUEST' | translate}}
  • +
  • {{ summaryInformation?.limited_guest_count }} {{'SUMMARY.LIMITED_GUEST' | translate}}
  • diff --git a/src/portal/src/app/shared/route/member-guard-activate.service.ts b/src/portal/src/app/shared/route/member-guard-activate.service.ts index e6f504f00..ffc744e7a 100644 --- a/src/portal/src/app/shared/route/member-guard-activate.service.ts +++ b/src/portal/src/app/shared/route/member-guard-activate.service.ts @@ -21,7 +21,8 @@ import { import { SessionService } from '../../shared/session.service'; import { ProjectService } from '@harbor/ui'; import { CommonRoutes } from '@harbor/ui'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; @Injectable() export class MemberGuard implements CanActivate, CanActivateChild { @@ -31,49 +32,39 @@ export class MemberGuard implements CanActivate, CanActivateChild { private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean { - let projectId = route.params['id']; + const projectId = route.params['id']; 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 { - return new Observable((observer) => { - this.projectService.checkProjectMember(projectId) - .subscribe(res => { - this.sessionService.setProjectMembers(res); - return observer.next(true); - }, + const user = this.sessionService.getCurrentUser(); + if (user !== null) { + return this.hasProjectPerm(state.url, projectId); + } + + return this.sessionService.retrieveUser().pipe( () => { - // Add exception for repository in project detail router activation. - this.projectService.getProject(projectId).subscribe(project => { - if (project.metadata && project.metadata.public === 'true') { - return observer.next(true); - } - this.router.navigate([CommonRoutes.HARBOR_DEFAULT]); - return observer.next(false); - }, - () => { - this.router.navigate([CommonRoutes.HARBOR_DEFAULT]); - return observer.next(false); - }); - }); - }); + return this.hasProjectPerm(state.url, projectId); + }, + catchError(err => { + this.router.navigate([CommonRoutes.HARBOR_DEFAULT]); + return of(false); + }) + ); } canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean { return this.canActivate(route, state); } + + hasProjectPerm(url: string, projectId: number): Observable { + // 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); + }) + ); + } } diff --git a/src/portal/src/app/shared/shared.const.ts b/src/portal/src/app/shared/shared.const.ts index 631d9e66d..15d4d3574 100644 --- a/src/portal/src/app/shared/shared.const.ts +++ b/src/portal/src/app/shared/shared.const.ts @@ -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 RoleInfo = { 1: 'MEMBER.PROJECT_ADMIN', 2: 'MEMBER.DEVELOPER', 3: 'MEMBER.GUEST', 4: 'MEMBER.PROJECT_MASTER' }; -export const RoleMapping = { 'projectAdmin': 'MEMBER.PROJECT_ADMIN', -'master': 'MEMBER.PROJECT_MASTER', 'developer': 'MEMBER.DEVELOPER', 'guest': 'MEMBER.GUEST' }; + +export const RoleInfo = { + 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 = [ { id: 1, value: "MEMBER.PROJECT_ADMIN" }, { id: 2, value: "MEMBER.DEVELOPER" }, { id: 3, value: "MEMBER.GUEST" }, { id: 4, value: "MEMBER.PROJECT_MASTER" }, + { id: 5, value: "MEMBER.LIMITED_GUEST" }, ]; export enum Roles { @@ -75,6 +90,7 @@ export enum Roles { PROJECT_MASTER = 4, DEVELOPER = 2, GUEST = 3, + LIMITED_GUEST = 5, OTHER = 0, } export const DefaultHelmIcon = '/images/helm-gray.svg'; diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 88e2035ee..becf3e951 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -275,6 +275,7 @@ "PROJECT_MASTER": "Master", "DEVELOPER": "Developer", "GUEST": "Guest", + "LIMITED_GUEST": "Limited Guest", "DELETE": "Delete", "ITEMS": "items", "ACTIONS": "Actions", @@ -737,7 +738,8 @@ "ADMIN": "Admin(s)", "MASTER": "Master(s)", "DEVELOPER": "Developer(s)", - "GUEST": "Guest(s)" + "GUEST": "Guest(s)", + "LIMITED_GUEST": "Limited guest(s)" }, "ALERT": { "FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?" diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index a8492e61a..8e7b8886c 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -276,6 +276,7 @@ "PROJECT_MASTER": "Mantenedor", "DEVELOPER": "Desarrollador", "GUEST": "Invitado", + "LIMITED_GUEST": "Limited Guest", "DELETE": "Eliminar", "ITEMS": "elementos", "ACTIONS": "Acciones", @@ -738,7 +739,8 @@ "ADMIN": "Admin(s)", "MASTER": "Master(s)", "DEVELOPER": "Developer(s)", - "GUEST": "Guest(s)" + "GUEST": "Guest(s)", + "LIMITED_GUEST": "Limited guest(s)" }, "ALERT": { "FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?" diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index ec9a492b8..88547b1b4 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -288,6 +288,7 @@ "PROJECT_MASTER": "préposé à la maintenance", "DEVELOPER": "Développeur", "GUEST": "Invité", + "LIMITED_GUEST": "Limited Guest", "DELETE": "Supprimer", "ITEMS": "items", "ACTIONS": "Actions", @@ -724,7 +725,8 @@ "ADMIN": "Admin(s)", "MASTER": "Master(s)", "DEVELOPER": "Developer(s)", - "GUEST": "Guest(s)" + "GUEST": "Guest(s)", + "LIMITED_GUEST": "Limited guest(s)" }, "ALERT": { "FORM_CHANGE_CONFIRMATION": "Certaines modifications ne sont pas encore enregistrées. Voulez-vous annuler ?" diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 002c39964..f3823a5d1 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -273,6 +273,7 @@ "PROJECT_MASTER": "Mantenedor", "DEVELOPER": "Desenvolvedor", "GUEST": "Visitante", + "LIMITED_GUEST": "Limited Guest", "DELETE": "Remover", "ITEMS": "itens", "ACTIONS": "Ações", @@ -733,7 +734,8 @@ "ADMIN": "Admin(s)", "MASTER": "Master(s)", "DEVELOPER": "Developer(s)", - "GUEST": "Guest(s)" + "GUEST": "Guest(s)", + "LIMITED_GUEST": "Limited guest(s)" }, "ALERT": { "FORM_CHANGE_CONFIRMATION": "Algumas alterações ainda não foram salvas. Você deseja cancelar?" diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index b56259501..569d69720 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -275,6 +275,7 @@ "PROJECT_MASTER": "Uzman", "DEVELOPER": "Geliştirici", "GUEST": "Konuk", + "LIMITED_GUEST": "Limited Guest", "DELETE": "Sil", "ITEMS": "çeşit", "ACTIONS": "Eylemler", @@ -736,7 +737,8 @@ "ADMIN": "Yönetici(ler)", "MASTER": "Uzman(lar)", "DEVELOPER": "Geliştirici(ler)", - "GUEST": "Misafir(ler)" + "GUEST": "Misafir(ler)", + "LIMITED_GUEST": "Limited guest(s)" }, "ALERT": { "FORM_CHANGE_CONFIRMATION": "Bazı değişiklikler henüz kaydedilmedi. İptal etmek istiyor musun?" diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 279cc7565..167ca6f1b 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -275,6 +275,7 @@ "PROJECT_MASTER": "维护人员", "DEVELOPER": "开发人员", "GUEST": "访客", + "LIMITED_GUEST": "受限访客", "DELETE": "删除", "ITEMS": "条记录", "ACTIONS": "操作", @@ -738,7 +739,8 @@ "ADMIN": "管理员", "MASTER": "维护人员", "DEVELOPER": "开发者", - "GUEST": "访客" + "GUEST": "访客", + "LIMITED_GUEST": "受限访客" }, "ALERT": { "FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"