From 751d404519f4930b383a8e62e689b3f5a9ab3b99 Mon Sep 17 00:00:00 2001 From: stonezdj Date: Fri, 2 Apr 2021 14:22:18 +0800 Subject: [PATCH] Refactor project member api to new programming model Add pkg/member/dao Add pkg/member/models Add pkg/member/manager Add controller/member Remove the old project member API Signed-off-by: stonezdj --- api/v2.0/legacy_swagger.yaml | 164 ------- api/v2.0/swagger.yaml | 214 +++++++++ src/common/dao/project/projectmember_test.go | 403 ----------------- src/controller/member/controller.go | 234 ++++++++++ src/controller/member/controller_test.go | 15 + src/core/api/api_test.go | 19 +- src/core/api/harborapi_test.go | 1 - src/core/api/projectmember.go | 307 ------------- src/core/api/projectmember_test.go | 414 ------------------ src/core/auth/ldap/ldap_test.go | 64 +-- src/lib/orm/query.go | 19 + src/lib/orm/test/orm_test.go | 14 + src/pkg/exporter/project_collector_test.go | 11 +- .../member/dao/dao.go} | 139 ++++-- src/pkg/member/dao/dao_test.go | 271 ++++++++++++ src/pkg/member/manager.go | 101 +++++ src/pkg/member/models/member.go | 33 ++ src/server/v2.0/handler/handler.go | 1 + src/server/v2.0/handler/member.go | 171 ++++++++ src/server/v2.0/handler/project.go | 16 +- src/server/v2.0/route/legacy.go | 1 - tests/apitests/python/library/base.py | 5 +- tests/apitests/python/library/project.py | 38 +- 23 files changed, 1250 insertions(+), 1405 deletions(-) delete mode 100644 src/common/dao/project/projectmember_test.go create mode 100644 src/controller/member/controller.go create mode 100644 src/controller/member/controller_test.go delete mode 100644 src/core/api/projectmember.go delete mode 100644 src/core/api/projectmember_test.go rename src/{common/dao/project/projectmember.go => pkg/member/dao/dao.go} (54%) create mode 100644 src/pkg/member/dao/dao_test.go create mode 100644 src/pkg/member/manager.go create mode 100644 src/pkg/member/models/member.go create mode 100644 src/server/v2.0/handler/member.go diff --git a/api/v2.0/legacy_swagger.yaml b/api/v2.0/legacy_swagger.yaml index e113344f8..d7d48a348 100644 --- a/api/v2.0/legacy_swagger.yaml +++ b/api/v2.0/legacy_swagger.yaml @@ -176,170 +176,6 @@ paths: description: Project or metadata does not exist. '500': description: Internal server errors. - '/projects/{project_id}/members': - get: - summary: Get all project member information - description: Get all project member information - parameters: - - name: project_id - in: path - type: integer - format: int64 - required: true - description: Relevant project ID. - - name: entityname - in: query - type: string - description: The entity name to search. - tags: - - Products - responses: - '200': - description: Get project members successfully. - schema: - type: array - items: - $ref: '#/definitions/ProjectMemberEntity' - '400': - description: The project id is invalid. - '401': - description: User need to log in first. - '403': - description: User in session does not have permission to the project. - '404': - description: Project ID does not exist. - '500': - description: Unexpected internal errors. - post: - summary: Create project member - description: 'Create project member relationship, the member can be one of the user_member and group_member, The user_member need to specify user_id or username. If the user already exist in harbor DB, specify the user_id, If does not exist in harbor DB, it will SearchAndOnBoard the user. The group_member need to specify id or ldap_group_dn. If the group already exist in harbor DB. specify the user group''s id, If does not exist, it will SearchAndOnBoard the group. ' - tags: - - Products - parameters: - - name: project_id - in: path - type: integer - format: int64 - required: true - description: Relevant project ID. - - name: project_member - in: body - schema: - $ref: '#/definitions/ProjectMember' - responses: - '201': - description: Project member created successfully. - headers: - Location: - type: string - description: The URL of the created resource - '400': - description: 'Illegal format of project member or project id is invalid, or LDAP DN is invalid.' - '401': - description: User need to log in first. - '403': - description: User in session does not have permission to the project. - '409': - description: A user group with same group name already exist or an LDAP user group with same DN already exist. - '500': - description: Unexpected internal errors. - '/projects/{project_id}/members/{mid}': - get: - summary: Get the project member information - description: Get the project member information - tags: - - Products - parameters: - - name: project_id - in: path - type: integer - format: int64 - required: true - description: Relevant project ID. - - name: mid - in: path - type: integer - format: int64 - required: true - description: The member ID - responses: - '200': - description: Project member retrieved successfully. - schema: - $ref: '#/definitions/ProjectMemberEntity' - '400': - description: 'Illegal format of project member or invalid project id, member id.' - '401': - description: User need to log in first. - '403': - description: User in session does not have permission to the project. - '404': - description: Project or projet member does not exist. - '500': - description: Unexpected internal errors. - put: - summary: Update project member - description: Update project member relationship - tags: - - Products - parameters: - - name: project_id - in: path - type: integer - format: int64 - required: true - description: Relevant project ID. - - name: mid - in: path - type: integer - format: int64 - required: true - description: Member ID. - - name: role - in: body - schema: - $ref: '#/definitions/RoleRequest' - responses: - '200': - description: Project member updated successfully. - '400': - description: 'Invalid role id, it should be 1,2 or 3, or invalid project id, or invalid member id.' - '401': - description: User need to log in first. - '403': - description: User in session does not have permission to the project. - '404': - description: project or project member does not exist. - '500': - description: Unexpected internal errors. - delete: - summary: Delete project member - tags: - - Products - parameters: - - name: project_id - in: path - type: integer - format: int64 - required: true - description: Relevant project ID. - - name: mid - in: path - type: integer - format: int64 - required: true - description: Member ID. - responses: - '200': - description: Project member deleted successfully. - '400': - description: The project id or project member id is invalid. - '401': - description: User need to log in first. - '403': - description: User in session does not have permission to the project. - '500': - description: Unexpected internal errors. /statistics: get: summary: Get projects number and repositories number relevant to the user diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index d394e4acf..f74504f54 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -447,6 +447,170 @@ paths: $ref: '#/responses/404' '500': $ref: '#/responses/500' + '/projects/{project_name_or_id}/members': + get: + summary: Get all project member information + description: Get all project member information + operationId: listProjectMembers + parameters: + - $ref: '#/parameters/requestId' + - $ref: '#/parameters/isResourceName' + - $ref: '#/parameters/projectNameOrId' + - $ref: '#/parameters/page' + - $ref: '#/parameters/pageSize' + - name: entityname + in: query + type: string + description: The entity name to search. + tags: + - member + responses: + '200': + description: Get project members successfully. + headers: + X-Total-Count: + description: The total count of members + type: integer + Link: + description: Link refers to the previous page and next page + type: string + schema: + type: array + items: + $ref: '#/definitions/ProjectMemberEntity' + + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '404': + $ref: '#/responses/404' + '500': + $ref: '#/responses/500' + post: + summary: Create project member + operationId: createProjectMember + description: 'Create project member relationship, the member can be one of the user_member and group_member, The user_member need to specify user_id or username. If the user already exist in harbor DB, specify the user_id, If does not exist in harbor DB, it will SearchAndOnBoard the user. The group_member need to specify id or ldap_group_dn. If the group already exist in harbor DB. specify the user group''s id, If does not exist, it will SearchAndOnBoard the group. ' + tags: + - member + parameters: + - $ref: '#/parameters/requestId' + - $ref: '#/parameters/isResourceName' + - $ref: '#/parameters/projectNameOrId' + - name: project_member + in: body + schema: + $ref: '#/definitions/ProjectMember' + responses: + '201': + description: Project member created successfully. + headers: + Location: + type: string + description: The URL of the created resource + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '409': + $ref: '#/responses/409' + '500': + $ref: '#/responses/500' + '/projects/{project_name_or_id}/members/{mid}': + get: + summary: Get the project member information + description: Get the project member information + operationId: getProjectMember + tags: + - member + parameters: + - $ref: '#/parameters/requestId' + - $ref: '#/parameters/isResourceName' + - $ref: '#/parameters/projectNameOrId' + - name: mid + in: path + type: integer + format: int64 + required: true + description: The member ID + responses: + '200': + description: Project member retrieved successfully. + schema: + $ref: '#/definitions/ProjectMemberEntity' + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '404': + $ref: '#/responses/404' + '500': + $ref: '#/responses/500' + put: + summary: Update project member + description: Update project member relationship + operationId: updateProjectMember + tags: + - member + parameters: + - $ref: '#/parameters/requestId' + - $ref: '#/parameters/isResourceName' + - $ref: '#/parameters/projectNameOrId' + - name: mid + in: path + type: integer + format: int64 + required: true + description: Member ID. + - name: role + in: body + schema: + $ref: '#/definitions/RoleRequest' + responses: + '200': + $ref: '#/responses/200' + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '404': + $ref: '#/responses/404' + '500': + $ref: '#/responses/500' + delete: + summary: Delete project member + operationId: deleteProjectMember + tags: + - member + parameters: + - $ref: '#/parameters/requestId' + - $ref: '#/parameters/isResourceName' + - $ref: '#/parameters/projectNameOrId' + - name: mid + in: path + type: integer + format: int64 + required: true + description: Member ID. + responses: + '200': + $ref: '#/responses/200' + '400': + $ref: '#/responses/400' + '401': + $ref: '#/responses/401' + '403': + $ref: '#/responses/403' + '500': + $ref: '#/responses/500' /repositories: get: summary: List all authorized repositories @@ -4783,6 +4947,7 @@ parameters: required: true type: integer format: int64 + responses: '200': description: Success @@ -7392,6 +7557,55 @@ definitions: editable: type: boolean description: The configure item can be updated or not + ProjectMemberEntity: + type: object + properties: + id: + type: integer + description: the project member id + project_id: + type: integer + description: the project id + entity_name: + type: string + description: the name of the group member. + role_name: + type: string + description: the name of the role + role_id: + type: integer + description: the role id + entity_id: + type: integer + description: 'the id of entity, if the member is a user, it is user_id in user table. if the member is a user group, it is the user group''s ID in user_group table.' + entity_type: + type: string + description: 'the entity''s type, u for user entity, g for group entity.' + ProjectMember: + type: object + properties: + role_id: + type: integer + description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest, 4 for maintainer' + member_user: + $ref: '#/definitions/UserEntity' + member_group: + $ref: '#/definitions/UserGroup' + RoleRequest: + type: object + properties: + role_id: + type: integer + description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest, 4 for maintainer' + UserEntity: + type: object + properties: + user_id: + type: integer + description: The ID of the user. + username: + type: string + description: The name of the user. UserProfile: type: object properties: diff --git a/src/common/dao/project/projectmember_test.go b/src/common/dao/project/projectmember_test.go deleted file mode 100644 index 2055e363b..000000000 --- a/src/common/dao/project/projectmember_test.go +++ /dev/null @@ -1,403 +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 ( - "fmt" - "os" - "testing" - - "github.com/goharbor/harbor/src/controller/config" - "github.com/goharbor/harbor/src/lib/orm" - "github.com/goharbor/harbor/src/pkg/usergroup" - "github.com/goharbor/harbor/src/pkg/usergroup/model" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "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/core/auth/db" - _ "github.com/goharbor/harbor/src/core/auth/ldap" - "github.com/goharbor/harbor/src/lib/log" -) - -func TestMain(m *testing.M) { - - // databases := []string{"mysql", "sqlite"} - databases := []string{"postgresql"} - for _, database := range databases { - log.Infof("run test cases for database: %s", database) - - result := 1 - switch database { - case "postgresql": - dao.PrepareTestForPostgresSQL() - default: - log.Fatalf("invalid database: %s", database) - } - - // Extract to test utils - initSqls := []string{ - "insert into harbor_user (username, email, password, realname) values ('member_test_01', 'member_test_01@example.com', '123456', 'member_test_01')", - "insert into project (name, owner_id) values ('member_test_01', 1)", - "insert into user_group (group_name, group_type, ldap_group_dn) values ('test_group_01', 1, 'CN=harbor_users,OU=sample,OU=vmware,DC=harbor,DC=com')", - "update project set owner_id = (select user_id from harbor_user where username = 'member_test_01') where name = 'member_test_01'", - "insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_01') , (select user_id from harbor_user where username = 'member_test_01'), 'u', 1)", - "insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_01') , (select id from user_group where group_name = 'test_group_01'), 'g', 1)", - - "insert into harbor_user (username, email, password, realname) values ('member_test_02', 'member_test_02@example.com', '123456', 'member_test_02')", - "insert into project (name, owner_id) values ('member_test_02', 1)", - "insert into user_group (group_name, group_type, ldap_group_dn) values ('test_group_02', 1, 'CN=harbor_users,OU=sample,OU=vmware,DC=harbor,DC=com')", - "update project set owner_id = (select user_id from harbor_user where username = 'member_test_02') where name = 'member_test_02'", - "insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_02') , (select user_id from harbor_user where username = 'member_test_02'), 'u', 1)", - "insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_02') , (select id from user_group where group_name = 'test_group_02'), 'g', 1)", - } - - clearSqls := []string{ - "delete from project where name='member_test_01' or name='member_test_02'", - "delete from harbor_user where username='member_test_01' or username='member_test_02' or username='pm_sample'", - "delete from user_group", - "delete from project_member where id > 1", - } - dao.PrepareTestData(clearSqls, initSqls) - config.Init() - result = m.Run() - - if result != 0 { - os.Exit(result) - } - } - -} - -func TestDeleteProjectMemberByID(t *testing.T) { - currentProject, err := dao.GetProjectByName("member_test_01") - - if currentProject == nil || err != nil { - fmt.Println("Failed to load project!") - } else { - fmt.Printf("Load project %+v", currentProject) - } - var addMember = models.Member{ - ProjectID: currentProject.ProjectID, - EntityID: 1, - EntityType: common.UserMember, - Role: common.RoleDeveloper, - } - - pmid, err := AddProjectMember(addMember) - - if err != nil { - t.Fatalf("Failed to add project member error: %v", err) - } - - type args struct { - pmid int - } - tests := []struct { - name string - args args - wantErr bool - }{ - {"Delete created", args{pmid}, false}, - {"Delete non exist", args{-13}, false}, - {"Delete non exist", args{13}, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := DeleteProjectMemberByID(tt.args.pmid); (err != nil) != tt.wantErr { - t.Errorf("DeleteProjectMemberByID() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } - -} -func TestAddProjectMember(t *testing.T) { - - currentProject, err := dao.GetProjectByName("member_test_01") - if err != nil { - t.Errorf("Error occurred when GetProjectByName: %v", err) - } - member := models.Member{ - ProjectID: currentProject.ProjectID, - EntityID: 1, - EntityType: common.UserMember, - Role: common.RoleProjectAdmin, - } - - log.Debugf("Current project id %v", currentProject.ProjectID) - pmid, err := AddProjectMember(member) - if err != nil { - t.Errorf("Error occurred in AddProjectMember: %v", err) - } - if pmid == 0 { - t.Errorf("Error add project member, pmid=0") - } - - queryMember := models.Member{ - ProjectID: currentProject.ProjectID, - ID: pmid, - } - - memberList, err := GetProjectMember(queryMember) - if err != nil { - t.Errorf("Failed to query project member, %v, error: %v", queryMember, err) - } - - if len(memberList) == 0 { - t.Errorf("Failed to query project member, %v", queryMember) - } - - _, err = AddProjectMember(models.Member{ - ProjectID: -1, - EntityID: 1, - EntityType: common.UserMember, - Role: common.RoleProjectAdmin, - }) - if err == nil { - t.Fatal("Should failed with negative projectID") - } - _, err = AddProjectMember(models.Member{ - ProjectID: 1, - EntityID: -1, - EntityType: common.UserMember, - Role: common.RoleProjectAdmin, - }) - if err == nil { - t.Fatal("Should failed with negative entityID") - } -} -func TestUpdateProjectMemberRole(t *testing.T) { - currentProject, err := dao.GetProjectByName("member_test_01") - user := models.User{ - Username: "pm_sample", - Email: "pm_sample@example.com", - Realname: "pm_sample", - Password: "1234567d", - } - o := dao.GetOrmer() - userID, err := o.Insert(&user) - if err != nil { - t.Errorf("Error occurred when add user: %v", err) - } - member := models.Member{ - ProjectID: currentProject.ProjectID, - EntityID: int(userID), - EntityType: common.UserMember, - Role: common.RoleProjectAdmin, - } - - pmid, err := AddProjectMember(member) - if err != nil { - t.Errorf("Error occurred in UpdateProjectMember: %v", err) - } - - UpdateProjectMemberRole(pmid, common.RoleDeveloper) - - queryMember := models.Member{ - ProjectID: currentProject.ProjectID, - EntityID: int(userID), - EntityType: common.UserMember, - } - - memberList, err := GetProjectMember(queryMember) - if err != nil { - t.Errorf("Error occurred in GetProjectMember: %v", err) - } - if len(memberList) != 1 { - t.Errorf("Error occurred in Failed, size: %d, condition:%+v", len(memberList), queryMember) - } - memberItem := memberList[0] - if memberItem.Role != common.RoleDeveloper || memberItem.Entityname != user.Username { - t.Errorf("member doesn't match!") - } - - memberList2, err := SearchMemberByName(currentProject.ProjectID, "pm_sample") - if err != nil { - t.Errorf("Error occurred when SearchMemberByName: %v", err) - } - if len(memberList2) == 0 { - t.Errorf("Failed to search user pm_sample, project_id:%v, entityname:%v", - currentProject.ProjectID, "pm_sample") - } - - memberList3, err := SearchMemberByName(currentProject.ProjectID, "") - if err != nil { - t.Errorf("Error occurred when SearchMemberByName: %v", err) - } - if len(memberList3) == 0 { - t.Errorf("Failed to search user pm_sample, project_id:%v, entityname is empty", - currentProject.ProjectID) - } -} - -func TestGetProjectMember(t *testing.T) { - currentProject, err := dao.GetProjectByName("member_test_01") - if err != nil { - t.Errorf("Error occurred when GetProjectByName: %v", err) - } - var memberList1 = []*models.Member{ - { - ID: 346, - Entityname: "admin", - Rolename: "projectAdmin", - Role: 1, - EntityID: 1, - EntityType: "u"}, - } - var memberList2 = []*models.Member{ - { - ID: 398, - Entityname: "test_group_01", - Rolename: "projectAdmin", - Role: 1, - EntityType: "g"}, - } - type args struct { - queryMember models.Member - } - tests := []struct { - name string - args args - want []*models.Member - wantErr bool - }{ - {"Query default project member", args{models.Member{ProjectID: currentProject.ProjectID, Entityname: "admin"}}, memberList1, false}, - {"Query default project member group", args{models.Member{ProjectID: currentProject.ProjectID, Entityname: "test_group_01"}}, memberList2, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := GetProjectMember(tt.args.queryMember) - if (err != nil) != tt.wantErr { - t.Errorf("GetProjectMember() error = %v, wantErr %v", err, tt.wantErr) - return - } - if len(got) != 1 { - t.Errorf("Error occurred when query project member") - } - itemGot := got[0] - itemWant := tt.want[0] - - if itemGot.Entityname != itemWant.Entityname || itemGot.Role != itemWant.Role || itemGot.EntityType != itemWant.EntityType { - t.Errorf("test failed, got:%+v, want:%+v", itemGot, itemWant) - } - }) - } - -} - -func TestGetTotalOfProjectMembers(t *testing.T) { - currentProject, _ := dao.GetProjectByName("member_test_02") - - type args struct { - projectID int64 - roles []int - } - tests := []struct { - name string - args args - want int64 - wantErr bool - }{ - {"Get total of project admin", args{currentProject.ProjectID, []int{common.RoleProjectAdmin}}, 2, false}, - {"Get total of maintainer", args{currentProject.ProjectID, []int{common.RoleMaintainer}}, 0, false}, - {"Get total of developer", args{currentProject.ProjectID, []int{common.RoleDeveloper}}, 0, false}, - {"Get total of guest", args{currentProject.ProjectID, []int{common.RoleGuest}}, 0, false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := GetTotalOfProjectMembers(tt.args.projectID, tt.args.roles...) - if (err != nil) != tt.wantErr { - t.Errorf("GetTotalOfProjectMembers() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("GetTotalOfProjectMembers() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestListRoles(t *testing.T) { - // nil user - roles, err := ListRoles(nil, 1) - require.Nil(t, err) - assert.Len(t, roles, 0) - - user, err := dao.GetUser(models.User{Username: "member_test_01"}) - require.Nil(t, err) - project, err := dao.GetProjectByName("member_test_01") - require.Nil(t, err) - - // user with empty groups - roles, err = ListRoles(user, project.ProjectID) - require.Nil(t, err) - assert.Len(t, roles, 1) - - // user with a group whose ID doesn't exist - user.GroupIDs = []int{9999} - roles, err = ListRoles(user, project.ProjectID) - require.Nil(t, err) - require.Len(t, roles, 1) - assert.Equal(t, common.RoleProjectAdmin, roles[0]) - ctx := orm.Context() - // user with a valid group - groupID, err := usergroup.Mgr.Create(ctx, model.UserGroup{ - GroupName: "group_for_list_role", - GroupType: 1, - LdapGroupDN: "CN=list_role_users,OU=sample,OU=vmware,DC=harbor,DC=com", - }) - require.Nil(t, err) - defer usergroup.Mgr.Delete(orm.Context(), groupID) - - memberID, err := AddProjectMember(models.Member{ - ProjectID: project.ProjectID, - Role: common.RoleDeveloper, - EntityID: groupID, - EntityType: "g", - }) - require.Nil(t, err) - defer DeleteProjectMemberByID(memberID) - - user.GroupIDs = []int{groupID} - roles, err = ListRoles(user, project.ProjectID) - require.Nil(t, err) - require.Len(t, roles, 2) - assert.Equal(t, common.RoleProjectAdmin, roles[0]) - assert.Equal(t, common.RoleDeveloper, roles[1]) -} - -func PrepareGroupTest() { - initSqls := []string{ - `insert into user_group (group_name, group_type, ldap_group_dn) values ('harbor_group_01', 1, 'cn=harbor_user,dc=example,dc=com')`, - `insert into harbor_user (username, email, password, realname) values ('sample01', 'sample01@example.com', 'harbor12345', 'sample01')`, - `insert into project (name, owner_id) values ('group_project', 1)`, - `insert into project (name, owner_id) values ('group_project_private', 1)`, - `insert into project_metadata (project_id, name, value) values ((select project_id from project where name = 'group_project'), 'public', 'false')`, - `insert into project_metadata (project_id, name, value) values ((select project_id from project where name = 'group_project_private'), 'public', 'false')`, - `insert into project_member (project_id, entity_id, entity_type, role) values ((select project_id from project where name = 'group_project'), (select id from user_group where group_name = 'harbor_group_01'),'g', 2)`, - } - - clearSqls := []string{ - `delete from project_metadata where project_id in (select project_id from project where name in ('group_project', 'group_project_private'))`, - `delete from project where name in ('group_project', 'group_project_private')`, - `delete from project_member where project_id in (select project_id from project where name in ('group_project', 'group_project_private'))`, - `delete from user_group where group_name = 'harbor_group_01'`, - `delete from harbor_user where username = 'sample01'`, - } - dao.PrepareTestData(clearSqls, initSqls) -} diff --git a/src/controller/member/controller.go b/src/controller/member/controller.go new file mode 100644 index 000000000..71642474c --- /dev/null +++ b/src/controller/member/controller.go @@ -0,0 +1,234 @@ +// 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 member + +import ( + "context" + "fmt" + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/core/auth" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/member" + "github.com/goharbor/harbor/src/pkg/member/models" + "github.com/goharbor/harbor/src/pkg/project" + "github.com/goharbor/harbor/src/pkg/user" + "github.com/goharbor/harbor/src/pkg/usergroup" + ugModel "github.com/goharbor/harbor/src/pkg/usergroup/model" +) + +// Controller defines the operation related to project member +type Controller interface { + // Get get the project member with ID + Get(ctx context.Context, projectNameOrID interface{}, memberID int) (*models.Member, error) + // Create add project member to project + Create(ctx context.Context, projectNameOrID interface{}, req Request) (int, error) + // Delete member from project + Delete(ctx context.Context, projectNameOrID interface{}, memberID int) error + // List list all project members with condition + List(ctx context.Context, projectNameOrID interface{}, entityName string, query *q.Query) ([]*models.Member, error) + // UpdateRole update the project member role + UpdateRole(ctx context.Context, projectNameOrID interface{}, memberID int, role int) error + // Count get the total amount of project members + Count(ctx context.Context, projectNameOrID interface{}, query *q.Query) (int, error) +} + +// Request - Project Member Request +type Request struct { + ProjectID int64 `json:"project_id"` + Role int `json:"role_id,omitempty"` + MemberUser User `json:"member_user,omitempty"` + MemberGroup UserGroup `json:"member_group,omitempty"` +} + +// User ... +type User struct { + UserID int `json:"user_id"` + Username string `json:"username"` +} + +// UserGroup ... +type UserGroup struct { + ID int `json:"id,omitempty"` + GroupName string `json:"group_name,omitempty"` + GroupType int `json:"group_type,omitempty"` + LdapGroupDN string `json:"ldap_group_dn,omitempty"` +} + +// ErrDuplicateProjectMember ... +var ErrDuplicateProjectMember = errors.New("The project member specified already exist") + +// ErrInvalidRole ... +var ErrInvalidRole = errors.New("Failed to update project member, role is not in 1,2,3") + +type controller struct { + userManager user.Manager + mgr member.Manager + projectMgr project.Manager +} + +// NewController ... +func NewController() Controller { + return &controller{mgr: member.Mgr, projectMgr: project.Mgr, userManager: user.New()} +} + +func (c *controller) Count(ctx context.Context, projectNameOrID interface{}, query *q.Query) (int, error) { + p, err := c.projectMgr.Get(ctx, projectNameOrID) + if err != nil { + return 0, err + } + return c.mgr.GetTotalOfProjectMembers(ctx, p.ProjectID, query) +} + +func (c *controller) UpdateRole(ctx context.Context, projectNameOrID interface{}, memberID int, role int) error { + p, err := c.projectMgr.Get(ctx, projectNameOrID) + if err != nil { + return err + } + if p == nil { + return errors.BadRequestError(nil).WithMessage("project is not found") + } + return c.mgr.UpdateRole(ctx, p.ProjectID, memberID, role) +} + +func (c *controller) Get(ctx context.Context, projectNameOrID interface{}, memberID int) (*models.Member, error) { + p, err := c.projectMgr.Get(ctx, projectNameOrID) + if err != nil { + return nil, err + } + if p == nil { + return nil, errors.BadRequestError(nil).WithMessage("project is not found") + } + return c.mgr.Get(ctx, p.ProjectID, memberID) +} + +func (c *controller) Create(ctx context.Context, projectNameOrID interface{}, req Request) (int, error) { + p, err := c.projectMgr.Get(ctx, projectNameOrID) + if err != nil { + return 0, err + } + if p == nil { + return 0, errors.BadRequestError(nil).WithMessage("project is not found") + } + var member models.Member + member.ProjectID = p.ProjectID + member.Role = req.Role + member.EntityType = common.GroupMember + + if req.MemberUser.UserID > 0 { + member.EntityID = req.MemberUser.UserID + member.EntityType = common.UserMember + } else if req.MemberGroup.ID > 0 { + member.EntityID = req.MemberGroup.ID + } else if len(req.MemberUser.Username) > 0 { + // If username is provided, search userid by username + var userID int + member.EntityType = common.UserMember + u, err := c.userManager.GetByName(ctx, req.MemberUser.Username) + if err != nil { + return 0, err + } + if u != nil { + userID = u.UserID + } else { + userID, err = auth.SearchAndOnBoardUser(req.MemberUser.Username) + if err != nil { + return 0, err + } + } + member.EntityID = userID + } else if len(req.MemberGroup.LdapGroupDN) > 0 { + req.MemberGroup.GroupType = common.LDAPGroupType + // If groupname provided, use the provided groupname to name this group + groupID, err := auth.SearchAndOnBoardGroup(req.MemberGroup.LdapGroupDN, req.MemberGroup.GroupName) + if err != nil { + return 0, err + } + member.EntityID = groupID + } else if len(req.MemberGroup.GroupName) > 0 && req.MemberGroup.GroupType == common.HTTPGroupType || req.MemberGroup.GroupType == common.OIDCGroupType { + ugs, err := usergroup.Mgr.List(ctx, ugModel.UserGroup{GroupName: req.MemberGroup.GroupName, GroupType: req.MemberGroup.GroupType}) + if err != nil { + return 0, err + } + if len(ugs) == 0 { + groupID, err := auth.SearchAndOnBoardGroup(req.MemberGroup.GroupName, "") + if err != nil { + return 0, err + } + member.EntityID = groupID + } else { + member.EntityID = ugs[0].ID + } + + } + if member.EntityID <= 0 { + return 0, fmt.Errorf("Can not get valid member entity, request: %+v", req) + } + + // Check if member already exist in current project + memberList, err := c.mgr.List(ctx, models.Member{ + ProjectID: member.ProjectID, + EntityID: member.EntityID, + EntityType: member.EntityType, + }, nil) + if err != nil { + return 0, err + } + if len(memberList) > 0 { + return 0, ErrDuplicateProjectMember + } + + if !isValidRole(member.Role) { + // Return invalid role error + return 0, ErrInvalidRole + } + return c.mgr.AddProjectMember(ctx, member) +} + +func isValidRole(role int) bool { + switch role { + case common.RoleProjectAdmin, + common.RoleMaintainer, + common.RoleDeveloper, + common.RoleGuest, + common.RoleLimitedGuest: + return true + default: + return false + } +} + +func (c *controller) List(ctx context.Context, projectNameOrID interface{}, entityName string, query *q.Query) ([]*models.Member, error) { + p, err := c.projectMgr.Get(ctx, projectNameOrID) + if err != nil { + return nil, err + } + if p == nil { + return nil, errors.BadRequestError(nil).WithMessage("project is not found") + } + pm := models.Member{ + ProjectID: p.ProjectID, + Entityname: entityName, + } + return c.mgr.List(ctx, pm, query) +} + +func (c *controller) Delete(ctx context.Context, projectNameOrID interface{}, memberID int) error { + p, err := c.projectMgr.Get(ctx, projectNameOrID) + if err != nil { + return err + } + return c.mgr.Delete(ctx, p.ProjectID, memberID) +} diff --git a/src/controller/member/controller_test.go b/src/controller/member/controller_test.go new file mode 100644 index 000000000..0e06aeea3 --- /dev/null +++ b/src/controller/member/controller_test.go @@ -0,0 +1,15 @@ +// 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 member diff --git a/src/core/api/api_test.go b/src/core/api/api_test.go index 72279e62b..1fd1d5026 100644 --- a/src/core/api/api_test.go +++ b/src/core/api/api_test.go @@ -16,6 +16,7 @@ package api import ( "encoding/json" "fmt" + "github.com/goharbor/harbor/src/lib/orm" "io/ioutil" "net/http" "net/http/httptest" @@ -30,9 +31,10 @@ import ( "github.com/dghubble/sling" "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/dao/project" common_http "github.com/goharbor/harbor/src/common/http" "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/pkg/member" + memberModels "github.com/goharbor/harbor/src/pkg/member/models" htesting "github.com/goharbor/harbor/src/testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -249,8 +251,8 @@ func prepare() error { if err != nil { return err } - - if projAdminPMID, err = project.AddProjectMember(models.Member{ + ctx := orm.Context() + if projAdminPMID, err = member.Mgr.AddProjectMember(ctx, memberModels.Member{ ProjectID: 1, Role: common.RoleProjectAdmin, EntityID: int(projAdminID), @@ -268,8 +270,7 @@ func prepare() error { if err != nil { return err } - - if projAdminRobotPMID, err = project.AddProjectMember(models.Member{ + if projAdminRobotPMID, err = member.Mgr.AddProjectMember(ctx, memberModels.Member{ ProjectID: 1, Role: common.RoleProjectAdmin, EntityID: int(projAdminRobotID), @@ -288,7 +289,7 @@ func prepare() error { return err } - if projDeveloperPMID, err = project.AddProjectMember(models.Member{ + if projDeveloperPMID, err = member.Mgr.AddProjectMember(ctx, memberModels.Member{ ProjectID: 1, Role: common.RoleDeveloper, EntityID: int(projDeveloperID), @@ -307,7 +308,7 @@ func prepare() error { return err } - if projGuestPMID, err = project.AddProjectMember(models.Member{ + if projGuestPMID, err = member.Mgr.AddProjectMember(ctx, memberModels.Member{ ProjectID: 1, Role: common.RoleGuest, EntityID: int(projGuestID), @@ -325,7 +326,7 @@ func prepare() error { if err != nil { return err } - if projLimitedGuestPMID, err = project.AddProjectMember(models.Member{ + if projLimitedGuestPMID, err = member.Mgr.AddProjectMember(ctx, memberModels.Member{ ProjectID: 1, Role: common.RoleLimitedGuest, EntityID: int(projLimitedGuestID), @@ -340,7 +341,7 @@ func clean() { pmids := []int{projAdminPMID, projDeveloperPMID, projGuestPMID} for _, id := range pmids { - if err := project.DeleteProjectMemberByID(id); err != nil { + if err := member.Mgr.Delete(orm.Context(), 1, id); err != nil { fmt.Printf("failed to clean up member %d from project library: %v", id, err) } } diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index cc71eeac2..bf5c35c13 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -97,7 +97,6 @@ func init() { beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &MetadataAPI{}, "get:Get") beego.Router("/api/projects/:id([0-9]+)/metadatas/", &MetadataAPI{}, "post:Post") beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &MetadataAPI{}, "put:Put;delete:Delete") - beego.Router("/api/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &ProjectMemberAPI{}) beego.Router("/api/statistics", &StatisticAPI{}) beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping") beego.Router("/api/labels", &LabelAPI{}, "post:Post;get:List") diff --git a/src/core/api/projectmember.go b/src/core/api/projectmember.go deleted file mode 100644 index 4e65e8e83..000000000 --- a/src/core/api/projectmember.go +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright 2018 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 api - -import ( - "fmt" - "net/http" - "strconv" - "strings" - - "github.com/goharbor/harbor/src/controller/config" - "github.com/goharbor/harbor/src/lib/orm" - "github.com/goharbor/harbor/src/pkg/usergroup" - "github.com/goharbor/harbor/src/pkg/usergroup/model" - - "github.com/goharbor/harbor/src/common" - "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/dao/project" - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/rbac" - "github.com/goharbor/harbor/src/core/auth" - "github.com/goharbor/harbor/src/lib/errors" -) - -// ProjectMemberAPI handles request to /api/projects/{}/members/{} -type ProjectMemberAPI struct { - BaseController - id int - project *models.Project - member *models.Member - groupType int -} - -// ErrDuplicateProjectMember ... -var ErrDuplicateProjectMember = errors.New("The project member specified already exist") - -// ErrInvalidRole ... -var ErrInvalidRole = errors.New("Failed to update project member, role is not in 1,2,3") - -// Prepare validates the URL and parms -func (pma *ProjectMemberAPI) Prepare() { - pma.BaseController.Prepare() - - if !pma.SecurityCtx.IsAuthenticated() { - pma.SendUnAuthorizedError(errors.New("Unauthorized")) - return - } - pid, err := pma.GetInt64FromPath(":pid") - if err != nil || pid <= 0 { - text := "invalid project ID: " - if err != nil { - text += err.Error() - } else { - text += fmt.Sprintf("%d", pid) - } - pma.SendBadRequestError(errors.New(text)) - return - } - pro, err := pma.ProjectCtl.Get(pma.Context(), pid) - if err != nil { - if errors.IsNotFoundErr(err) { - pma.SendNotFoundError(fmt.Errorf("project %d not found", pid)) - } else { - pma.ParseAndHandleError(fmt.Sprintf("failed to get project %d", pid), err) - } - return - } - - pma.project = pro - - if pma.ParamExistsInPath(":pmid") { - pmid, err := pma.GetInt64FromPath(":pmid") - if err != nil || pmid <= 0 { - pma.SendBadRequestError(fmt.Errorf("The project member id is invalid, pmid:%s", pma.GetStringFromPath(":pmid"))) - return - } - pma.id = int(pmid) - - members, err := project.GetProjectMember(models.Member{ProjectID: pid, ID: pma.id}) - if err != nil { - pma.SendInternalServerError(err) - return - } - if len(members) == 0 { - pma.SendNotFoundError(fmt.Errorf("project member %d not found in project %d", pmid, pid)) - return - } - - pma.member = members[0] - } - - authMode, err := config.AuthMode(orm.Context()) - if err != nil { - pma.SendInternalServerError(fmt.Errorf("failed to get authentication mode")) - } - if authMode == common.LDAPAuth { - pma.groupType = common.LDAPGroupType - } else if authMode == common.HTTPAuth { - pma.groupType = common.HTTPGroupType - } -} - -func (pma *ProjectMemberAPI) requireAccess(action rbac.Action) bool { - return pma.RequireProjectAccess(pma.project.ProjectID, action, rbac.ResourceMember) -} - -// Get ... -func (pma *ProjectMemberAPI) Get() { - projectID := pma.project.ProjectID - queryMember := models.Member{} - queryMember.ProjectID = projectID - pma.Data["json"] = make([]models.Member, 0) - if pma.id == 0 { - if !pma.requireAccess(rbac.ActionList) { - return - } - entityname := pma.GetString("entityname") - memberList, err := project.SearchMemberByName(projectID, entityname) - if err != nil { - pma.SendInternalServerError(fmt.Errorf("Failed to query database for member list, error: %v", err)) - return - } - if len(memberList) > 0 { - pma.Data["json"] = memberList - } - - } else { - if !pma.requireAccess(rbac.ActionRead) { - return - } - pma.Data["json"] = pma.member - } - pma.ServeJSON() -} - -// Post ... Add a project member -func (pma *ProjectMemberAPI) Post() { - if !pma.requireAccess(rbac.ActionCreate) { - return - } - projectID := pma.project.ProjectID - var request models.MemberReq - if err := pma.DecodeJSONReq(&request); err != nil { - pma.SendBadRequestError(err) - return - } - request.MemberGroup.LdapGroupDN = strings.TrimSpace(request.MemberGroup.LdapGroupDN) - - pmid, err := AddProjectMember(projectID, request) - if err == auth.ErrorGroupNotExist || err == auth.ErrorUserNotExist { - pma.SendBadRequestError(fmt.Errorf("Failed to add project member, error: %v", err)) - return - } else if err == auth.ErrDuplicateLDAPGroup { - pma.SendConflictError(fmt.Errorf("Failed to add project member, already exist group or project member, groupDN:%v", request.MemberGroup.LdapGroupDN)) - return - } else if err == ErrDuplicateProjectMember { - pma.SendConflictError(fmt.Errorf("Failed to add project member, already exist group or project member, groupMemberID:%v", request.MemberGroup.ID)) - return - } else if err == ErrInvalidRole { - pma.SendBadRequestError(fmt.Errorf("Invalid role ID, role ID %v", request.Role)) - return - } else if err == auth.ErrInvalidLDAPGroupDN { - pma.SendBadRequestError(fmt.Errorf("Invalid LDAP DN: %v", request.MemberGroup.LdapGroupDN)) - return - } else if err != nil { - pma.SendInternalServerError(fmt.Errorf("Failed to add project member, error: %v", err)) - return - } - pma.Redirect(http.StatusCreated, strconv.FormatInt(int64(pmid), 10)) -} - -// Put ... Update an exist project member -func (pma *ProjectMemberAPI) Put() { - if !pma.requireAccess(rbac.ActionUpdate) { - return - } - pid := pma.project.ProjectID - pmID := pma.id - var req models.Member - if err := pma.DecodeJSONReq(&req); err != nil { - pma.SendBadRequestError(err) - return - } - if !isValidRole(req.Role) { - pma.SendBadRequestError(fmt.Errorf("Invalid role id %v", req.Role)) - return - } - err := project.UpdateProjectMemberRole(pmID, req.Role) - if err != nil { - pma.SendInternalServerError(fmt.Errorf("Failed to update DB to add project user role, project id: %d, pmid : %d, role id: %d", pid, pmID, req.Role)) - return - } -} - -// Delete ... -func (pma *ProjectMemberAPI) Delete() { - if !pma.requireAccess(rbac.ActionDelete) { - return - } - pmid := pma.id - err := project.DeleteProjectMemberByID(pmid) - if err != nil { - pma.SendInternalServerError(fmt.Errorf("Failed to delete project roles for user, project member id: %d, error: %v", pmid, err)) - return - } -} - -// AddProjectMember ... -func AddProjectMember(projectID int64, request models.MemberReq) (int, error) { - var member models.Member - member.ProjectID = projectID - member.Role = request.Role - member.EntityType = common.GroupMember - - if request.MemberUser.UserID > 0 { - member.EntityID = request.MemberUser.UserID - member.EntityType = common.UserMember - } else if request.MemberGroup.ID > 0 { - member.EntityID = request.MemberGroup.ID - } else if len(request.MemberUser.Username) > 0 { - var userID int - member.EntityType = common.UserMember - u, err := dao.GetUser(models.User{Username: request.MemberUser.Username}) - if err != nil { - return 0, err - } - if u != nil { - userID = u.UserID - } else { - userID, err = auth.SearchAndOnBoardUser(request.MemberUser.Username) - if err != nil { - return 0, err - } - } - member.EntityID = userID - } else if len(request.MemberGroup.LdapGroupDN) > 0 { - request.MemberGroup.GroupType = common.LDAPGroupType - // If groupname provided, use the provided groupname to name this group - groupID, err := auth.SearchAndOnBoardGroup(request.MemberGroup.LdapGroupDN, request.MemberGroup.GroupName) - if err != nil { - return 0, err - } - member.EntityID = groupID - } else if len(request.MemberGroup.GroupName) > 0 && request.MemberGroup.GroupType == common.HTTPGroupType || request.MemberGroup.GroupType == common.OIDCGroupType { - ugs, err := usergroup.Mgr.List(orm.Context(), model.UserGroup{GroupName: request.MemberGroup.GroupName, GroupType: request.MemberGroup.GroupType}) - if err != nil { - return 0, err - } - if len(ugs) == 0 { - groupID, err := auth.SearchAndOnBoardGroup(request.MemberGroup.GroupName, "") - if err != nil { - return 0, err - } - member.EntityID = groupID - } else { - member.EntityID = ugs[0].ID - } - - } - if member.EntityID <= 0 { - return 0, fmt.Errorf("Can not get valid member entity, request: %+v", request) - } - - // Check if member already exist in current project - memberList, err := project.GetProjectMember(models.Member{ - ProjectID: member.ProjectID, - EntityID: member.EntityID, - EntityType: member.EntityType, - }) - if err != nil { - return 0, err - } - if len(memberList) > 0 { - return 0, ErrDuplicateProjectMember - } - - 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.RoleMaintainer, - 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 deleted file mode 100644 index 859fd60c2..000000000 --- a/src/core/api/projectmember_test.go +++ /dev/null @@ -1,414 +0,0 @@ -// Copyright 2018 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 api - -import ( - "fmt" - "github.com/goharbor/harbor/src/lib/orm" - "github.com/goharbor/harbor/src/pkg/usergroup" - "github.com/goharbor/harbor/src/pkg/usergroup/model" - "net/http" - "testing" - - "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/dao/project" - "github.com/goharbor/harbor/src/common/models" -) - -func TestProjectMemberAPI_Get(t *testing.T) { - cases := []*codeCheckingCase{ - // 401 - { - request: &testingRequest{ - method: http.MethodGet, - url: "/api/projects/1/members", - }, - code: http.StatusUnauthorized, - }, - // 200 - { - request: &testingRequest{ - method: http.MethodGet, - url: "/api/projects/1/members", - credential: admin, - }, - code: http.StatusOK, - }, - // 400 - { - request: &testingRequest{ - method: http.MethodGet, - url: "/api/projects/0/members", - credential: admin, - }, - code: http.StatusBadRequest, - }, - // 200 - { - request: &testingRequest{ - method: http.MethodGet, - url: fmt.Sprintf("/api/projects/1/members/%d", projAdminPMID), - credential: admin, - }, - code: http.StatusOK, - }, - // 404 - { - request: &testingRequest{ - method: http.MethodGet, - url: "/api/projects/1/members/121", - credential: admin, - }, - code: http.StatusNotFound, - }, - // 404 - { - request: &testingRequest{ - method: http.MethodGet, - url: "/api/projects/99999/members/121", - credential: admin, - }, - code: http.StatusNotFound, - }, - } - runCodeCheckingCases(t, cases...) -} - -func TestProjectMemberAPI_Post(t *testing.T) { - userID, err := dao.Register(models.User{ - Username: "restuser", - Password: "Harbor12345", - Email: "restuser@example.com", - }) - defer dao.DeleteUser(int(userID)) - if err != nil { - t.Errorf("Error occurred when create user: %v", err) - } - - ugList, err := usergroup.Mgr.List(orm.Context(), model.UserGroup{GroupType: 1, LdapGroupDN: "cn=harbor_users,ou=sample,ou=vmware,dc=harbor,dc=com"}) - if err != nil { - t.Errorf("Failed to query the user group") - } - if len(ugList) <= 0 { - t.Errorf("Failed to query the user group") - } - httpUgList, err := usergroup.Mgr.List(orm.Context(), model.UserGroup{GroupType: 2, GroupName: "vsphere.local\\administrators"}) - if err != nil { - t.Errorf("Failed to query the user group") - } - if len(httpUgList) <= 0 { - t.Errorf("Failed to query the user group") - } - - cases := []*codeCheckingCase{ - // 401 - { - request: &testingRequest{ - method: http.MethodPost, - url: "/api/projects/1/members", - bodyJSON: &models.MemberReq{ - Role: 1, - MemberUser: models.User{ - UserID: int(userID), - }, - }, - }, - code: http.StatusUnauthorized, - }, - { - request: &testingRequest{ - method: http.MethodPost, - url: "/api/projects/1/members", - bodyJSON: &models.MemberReq{ - Role: 1, - MemberUser: models.User{ - UserID: int(userID), - }, - }, - credential: admin, - }, - code: http.StatusCreated, - }, - { - request: &testingRequest{ - method: http.MethodPost, - url: "/api/projects/1/members", - bodyJSON: &models.MemberReq{ - Role: 1, - MemberUser: models.User{ - Username: "notexistuser", - }, - }, - credential: admin, - }, - code: http.StatusBadRequest, - }, - { - request: &testingRequest{ - method: http.MethodPost, - url: "/api/projects/1/members", - bodyJSON: &models.MemberReq{ - Role: 1, - MemberUser: models.User{ - UserID: 0, - }, - }, - credential: admin, - }, - code: http.StatusInternalServerError, - }, - { - request: &testingRequest{ - method: http.MethodGet, - url: "/api/projects/1/members?entityname=restuser", - credential: admin, - }, - code: http.StatusOK, - }, - { - request: &testingRequest{ - method: http.MethodGet, - url: "/api/projects/1/members", - credential: admin, - }, - code: http.StatusOK, - }, - { - request: &testingRequest{ - method: http.MethodPost, - url: "/api/projects/1/members", - credential: admin, - bodyJSON: &models.MemberReq{ - Role: 1, - MemberGroup: model.UserGroup{ - GroupType: 1, - LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com", - }, - }, - }, - code: http.StatusInternalServerError, - }, - { - request: &testingRequest{ - method: http.MethodPost, - url: "/api/projects/1/members", - credential: admin, - bodyJSON: &models.MemberReq{ - Role: 1, - MemberGroup: model.UserGroup{ - GroupType: 2, - ID: httpUgList[0].ID, - }, - }, - }, - code: http.StatusCreated, - }, - { - request: &testingRequest{ - method: http.MethodPost, - url: "/api/projects/1/members", - credential: admin, - bodyJSON: &models.MemberReq{ - Role: 1, - MemberGroup: model.UserGroup{ - GroupType: 1, - ID: ugList[0].ID, - }, - }, - }, - code: http.StatusCreated, - }, - { - request: &testingRequest{ - method: http.MethodPost, - url: "/api/projects/1/members", - credential: admin, - bodyJSON: &models.MemberReq{ - Role: 1, - MemberGroup: model.UserGroup{ - GroupType: 2, - GroupName: "vsphere.local/users", - }, - }, - }, - code: http.StatusInternalServerError, - }, - } - runCodeCheckingCases(t, cases...) -} - -func TestProjectMemberAPI_PutAndDelete(t *testing.T) { - - userID, err := dao.Register(models.User{ - Username: "restuser", - Password: "Harbor12345", - Email: "restuser@example.com", - }) - defer dao.DeleteUser(int(userID)) - if err != nil { - t.Errorf("Error occurred when create user: %v", err) - } - - ID, err := project.AddProjectMember(models.Member{ - ProjectID: 1, - Role: 1, - EntityID: int(userID), - EntityType: "u", - }) - if err != nil { - t.Errorf("Error occurred when add project member: %v", err) - } - - projectID, err := dao.AddProject(models.Project{Name: "memberputanddelete", OwnerID: 1}) - if err != nil { - t.Errorf("Error occurred when add project: %v", err) - } - defer dao.DeleteProject(projectID) - - memberID, err := project.AddProjectMember(models.Member{ - ProjectID: projectID, - Role: 1, - EntityID: int(userID), - EntityType: "u", - }) - if err != nil { - t.Errorf("Error occurred when add project member: %v", err) - } - - URL := fmt.Sprintf("/api/projects/1/members/%v", ID) - badURL := fmt.Sprintf("/api/projects/1/members/%v", 0) - cases := []*codeCheckingCase{ - // 401 - { - request: &testingRequest{ - method: http.MethodPut, - url: URL, - bodyJSON: &models.Member{ - Role: 2, - }, - }, - code: http.StatusUnauthorized, - }, - // 200 - { - request: &testingRequest{ - method: http.MethodPut, - url: URL, - bodyJSON: &models.Member{ - Role: 2, - }, - credential: admin, - }, - code: http.StatusOK, - }, - // 200 - { - request: &testingRequest{ - method: http.MethodPut, - url: URL, - bodyJSON: &models.Member{ - Role: 4, - }, - credential: admin, - }, - code: http.StatusOK, - }, - // 400 - { - request: &testingRequest{ - method: http.MethodPut, - url: badURL, - bodyJSON: &models.Member{ - Role: 2, - }, - credential: admin, - }, - code: http.StatusBadRequest, - }, - // 400 - { - request: &testingRequest{ - method: http.MethodPut, - url: URL, - bodyJSON: &models.Member{ - Role: -2, - }, - credential: admin, - }, - code: http.StatusBadRequest, - }, - // 404 - { - request: &testingRequest{ - method: http.MethodPut, - url: fmt.Sprintf("/api/projects/1/members/%v", memberID), - bodyJSON: &models.Member{ - Role: 2, - }, - credential: admin, - }, - code: http.StatusNotFound, - }, - // 200 - { - request: &testingRequest{ - method: http.MethodDelete, - url: URL, - credential: admin, - }, - code: http.StatusOK, - }, - // 404 - { - request: &testingRequest{ - method: http.MethodDelete, - url: fmt.Sprintf("/api/projects/1/members/%v", memberID), - bodyJSON: &models.Member{ - Role: 2, - }, - credential: admin, - }, - code: http.StatusNotFound, - }, - } - - 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}, - {"maintainer", 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/core/auth/ldap/ldap_test.go b/src/core/auth/ldap/ldap_test.go index b8ef37759..851924392 100644 --- a/src/core/auth/ldap/ldap_test.go +++ b/src/core/auth/ldap/ldap_test.go @@ -14,22 +14,22 @@ package ldap import ( + "github.com/goharbor/harbor/src/pkg/usergroup" "os" "testing" "github.com/goharbor/harbor/src/controller/config" "github.com/goharbor/harbor/src/lib/orm" - "github.com/goharbor/harbor/src/pkg/usergroup" - "github.com/goharbor/harbor/src/pkg/usergroup/model" + ugModel "github.com/goharbor/harbor/src/pkg/usergroup/model" "github.com/stretchr/testify/assert" "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/dao/project" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/utils/test" - "github.com/goharbor/harbor/src/core/api" "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/pkg/member" + memberModels "github.com/goharbor/harbor/src/pkg/member/models" "github.com/goharbor/harbor/src/core/auth" ) @@ -267,7 +267,7 @@ func TestAuthenticateHelperOnBoardUser(t *testing.T) { } func TestOnBoardGroup(t *testing.T) { - group := model.UserGroup{ + group := ugModel.UserGroup{ GroupName: "harbor_group2", LdapGroupDN: "cn=harbor_group2,ou=groups,dc=example,dc=com", } @@ -360,18 +360,21 @@ func TestSearchAndOnBoardUser(t *testing.T) { } } func TestAddProjectMemberWithLdapUser(t *testing.T) { + memberMgr := member.Mgr + ctx := orm.Context() currentProject, err := dao.GetProjectByName("member_test_01") if err != nil { t.Errorf("Error occurred when GetProjectByName: %v", err) } - member := models.MemberReq{ - ProjectID: currentProject.ProjectID, - MemberUser: models.User{ - Username: "mike", - }, - Role: common.RoleProjectAdmin, + userID, err := auth.SearchAndOnBoardUser("mike") + member := memberModels.Member{ + ProjectID: currentProject.ProjectID, + EntityType: common.UserMember, + Entityname: "mike", + EntityID: userID, + Role: common.RoleProjectAdmin, } - pmid, err := api.AddProjectMember(currentProject.ProjectID, member) + pmid, err := memberMgr.AddProjectMember(ctx, member) if err != nil { t.Errorf("Error occurred in AddOrUpdateProjectMember: %v", err) } @@ -383,14 +386,14 @@ func TestAddProjectMemberWithLdapUser(t *testing.T) { if err != nil { t.Errorf("Error occurred when GetProjectByName: %v", err) } - member2 := models.MemberReq{ - ProjectID: currentProject.ProjectID, - MemberUser: models.User{ - Username: "mike", - }, - Role: common.RoleProjectAdmin, + member2 := memberModels.Member{ + ProjectID: currentProject.ProjectID, + EntityType: common.UserMember, + Entityname: "mike", + EntityID: userID, + Role: common.RoleProjectAdmin, } - pmid, err = api.AddProjectMember(currentProject.ProjectID, member2) + pmid, err = memberMgr.AddProjectMember(ctx, member2) if err != nil { t.Errorf("Error occurred in AddOrUpdateProjectMember: %v", err) } @@ -399,30 +402,31 @@ func TestAddProjectMemberWithLdapUser(t *testing.T) { } } func TestAddProjectMemberWithLdapGroup(t *testing.T) { + memberMgr := member.Mgr + ctx := orm.Context() currentProject, err := dao.GetProjectByName("member_test_01") if err != nil { t.Errorf("Error occurred when GetProjectByName: %v", err) } - userGroups := []model.UserGroup{{GroupName: "cn=harbor_users,ou=groups,dc=example,dc=com", LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com", GroupType: common.LDAPGroupType}} - groupIds, err := usergroup.Mgr.Populate(orm.Context(), userGroups) - member := models.MemberReq{ - ProjectID: currentProject.ProjectID, - MemberGroup: model.UserGroup{ - ID: groupIds[0], - }, - Role: common.RoleProjectAdmin, + userGroups := []ugModel.UserGroup{{GroupName: "cn=harbor_users,ou=groups,dc=example,dc=com", LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com", GroupType: common.LDAPGroupType}} + groupIds, err := usergroup.Mgr.Populate(ctx, userGroups) + m := memberModels.Member{ + ProjectID: currentProject.ProjectID, + EntityType: common.GroupMember, + EntityID: groupIds[0], + Role: common.RoleProjectAdmin, } - pmid, err := api.AddProjectMember(currentProject.ProjectID, member) + pmid, err := memberMgr.AddProjectMember(ctx, m) if err != nil { t.Errorf("Error occurred in AddOrUpdateProjectMember: %v", err) } if pmid == 0 { t.Errorf("Error occurred in AddOrUpdateProjectMember: pmid: %v", pmid) } - queryMember := models.Member{ + queryMember := memberModels.Member{ ProjectID: currentProject.ProjectID, } - memberList, err := project.GetProjectMember(queryMember) + memberList, err := member.Mgr.List(ctx, queryMember, nil) if err != nil { t.Errorf("Failed to query project member, %v, error: %v", queryMember, err) } diff --git a/src/lib/orm/query.go b/src/lib/orm/query.go index 00545fd82..a5ed30c2b 100644 --- a/src/lib/orm/query.go +++ b/src/lib/orm/query.go @@ -70,6 +70,25 @@ func QuerySetter(ctx context.Context, model interface{}, query *q.Query) (orm.Qu return qs, nil } +// PaginationOnRawSQL append page information to the raw sql +// It should be called after the order by +// e.g. +// select a, b, c from mytable order by a limit ? offset ? +// it appends the " limit ? offset ? " to sql, +// and appends the limit value and offset value to the params of this query +func PaginationOnRawSQL(query *q.Query, sql string, params []interface{}) (string, []interface{}) { + if query != nil && query.PageSize > 0 { + sql += ` limit ?` + params = append(params, query.PageSize) + + if query.PageNumber > 0 { + sql += ` offset ?` + params = append(params, (query.PageNumber-1)*query.PageSize) + } + } + return sql, params +} + // QuerySetterForCount creates the query setter used for count with the sort and pagination information ignored func QuerySetterForCount(ctx context.Context, model interface{}, query *q.Query, ignoredCols ...string) (orm.QuerySeter, error) { query = q.MustClone(query) diff --git a/src/lib/orm/test/orm_test.go b/src/lib/orm/test/orm_test.go index d7a52c742..1524f8a84 100644 --- a/src/lib/orm/test/orm_test.go +++ b/src/lib/orm/test/orm_test.go @@ -18,6 +18,7 @@ import ( "context" "errors" . "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" "sync" "testing" @@ -410,6 +411,19 @@ func (suite *OrmSuite) TestReadOrCreateParallel() { suite.Equal(1, sum) } +func (suite *OrmSuite) TestPaginationOnRawSQL() { + query := &q.Query{ + PageNumber: 1, + PageSize: 10, + } + sql := "select * from harbor_user where user_id > ? order by user_name " + params := []interface{}{2} + sql, params = PaginationOnRawSQL(query, sql, params) + suite.Equal("select * from harbor_user where user_id > ? order by user_name limit ? offset ?", sql) + suite.Equal(int64(10), params[1]) + suite.Equal(int64(0), params[2]) +} + func TestRunOrmSuite(t *testing.T) { suite.Run(t, new(OrmSuite)) } diff --git a/src/pkg/exporter/project_collector_test.go b/src/pkg/exporter/project_collector_test.go index f2c7e41e0..33d7a20ab 100644 --- a/src/pkg/exporter/project_collector_test.go +++ b/src/pkg/exporter/project_collector_test.go @@ -10,13 +10,14 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/dao/project" "github.com/goharbor/harbor/src/common/models" "github.com/goharbor/harbor/src/common/utils/test" proctl "github.com/goharbor/harbor/src/controller/project" quotactl "github.com/goharbor/harbor/src/controller/quota" "github.com/goharbor/harbor/src/lib/orm" "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/pkg/member" + memberModels "github.com/goharbor/harbor/src/pkg/member/models" qtypes "github.com/goharbor/harbor/src/pkg/quota/types" "github.com/goharbor/harbor/src/pkg/repository" ) @@ -105,15 +106,17 @@ func setupTest(t *testing.T) { // Add member to project pmIDs = make([]int, 0) alice.UserID, bob.UserID, eve.UserID = int(aliceID), int(bobID), int(eveID) - p1m1ID, err := project.AddProjectMember(models.Member{ProjectID: proID1, Role: common.RoleDeveloper, EntityID: int(aliceID), EntityType: common.UserMember}) + + p1m1ID, err := member.Mgr.AddProjectMember(ctx, memberModels.Member{ProjectID: proID1, Role: common.RoleDeveloper, EntityID: int(aliceID), EntityType: common.UserMember}) if err != nil { t.Errorf("add project member error %v", err) } - p2m1ID, err := project.AddProjectMember(models.Member{ProjectID: proID2, Role: common.RoleMaintainer, EntityID: int(bobID), EntityType: common.UserMember}) + p2m1ID, err := member.Mgr.AddProjectMember(ctx, memberModels.Member{ProjectID: proID2, Role: common.RoleMaintainer, EntityID: int(bobID), EntityType: common.UserMember}) if err != nil { t.Errorf("add project member error %v", err) } - p2m2ID, err := project.AddProjectMember(models.Member{ProjectID: proID2, Role: common.RoleMaintainer, EntityID: int(eveID), EntityType: common.UserMember}) + p2m2ID, err := member.Mgr.AddProjectMember(ctx, memberModels.Member{ProjectID: proID2, Role: common.RoleMaintainer, EntityID: int(eveID), EntityType: common.UserMember}) + if err != nil { t.Errorf("add project member error %v", err) } diff --git a/src/common/dao/project/projectmember.go b/src/pkg/member/dao/dao.go similarity index 54% rename from src/common/dao/project/projectmember.go rename to src/pkg/member/dao/dao.go index 5e31bc0b8..ebc81d8e0 100644 --- a/src/common/dao/project/projectmember.go +++ b/src/pkg/member/dao/dao.go @@ -1,36 +1,71 @@ -// Copyright Project Harbor Authors +// 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 +// 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 +// 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. +// 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 +package dao import ( + "context" "fmt" - "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/utils" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/pkg/member/models" ) -// GetProjectMember gets all members of the project. -func GetProjectMember(queryMember models.Member) ([]*models.Member, error) { +func init() { + orm.RegisterModel( + new(models.Member), + ) +} + +// DAO the dao for project member +type DAO interface { + // GetProjectMember gets all members of the project. + GetProjectMember(ctx context.Context, queryMember models.Member, query *q.Query) ([]*models.Member, error) + // GetTotalOfProjectMembers returns total of project members + GetTotalOfProjectMembers(ctx context.Context, projectID int64, query *q.Query, roles ...int) (int, error) + // AddProjectMember inserts a record to table project_member + AddProjectMember(ctx context.Context, member models.Member) (int, error) + // UpdateProjectMemberRole updates the record in table project_member, only role can be changed + UpdateProjectMemberRole(ctx context.Context, projectID int64, pmID int, role int) error + // DeleteProjectMemberByID - Delete Project Member by ID + DeleteProjectMemberByID(ctx context.Context, projectID int64, pmid int) error + // SearchMemberByName search members of the project by entity_name + SearchMemberByName(ctx context.Context, projectID int64, entityName string) ([]*models.Member, error) + // ListRoles lists the roles of user for the specific project + ListRoles(ctx context.Context, user *models.User, projectID int64) ([]int, error) +} + +type dao struct { +} + +// New ... +func New() DAO { + return &dao{} +} + +func (d *dao) GetProjectMember(ctx context.Context, queryMember models.Member, query *q.Query) ([]*models.Member, error) { log.Debugf("Query condition %+v", queryMember) if queryMember.ProjectID == 0 { return nil, fmt.Errorf("Failed to query project member, query condition %v", queryMember) } + o, err := orm.FromContext(ctx) + if err != nil { + return nil, err + } - o := dao.GetOrmer() sql := ` select a.* from (select pm.id as id, pm.project_id as project_id, ug.id as entity_id, ug.group_name as entity_name, ug.creation_time, ug.update_time, r.name as rolename, r.role_id as role, pm.entity_type as entity_type from user_group ug join project_member pm on pm.project_id = ? and ug.id = pm.entity_id join role r on pm.role = r.role_id where pm.entity_type = 'g' @@ -65,14 +100,14 @@ func GetProjectMember(queryMember models.Member) ([]*models.Member, error) { queryParam = append(queryParam, queryMember.ID) } sql += ` order by entity_name ` + sql, queryParam = orm.PaginationOnRawSQL(query, sql, queryParam) members := []*models.Member{} - _, err := o.Raw(sql, queryParam).QueryRows(&members) + _, err = o.Raw(sql, queryParam).QueryRows(&members) return members, err } -// GetTotalOfProjectMembers returns total of project members -func GetTotalOfProjectMembers(projectID int64, roles ...int) (int64, error) { +func (d *dao) GetTotalOfProjectMembers(ctx context.Context, projectID int64, query *q.Query, roles ...int) (int, error) { log.Debugf("Query condition %+v", projectID) if projectID == 0 { return 0, fmt.Errorf("failed to get total of project members, project id required %v", projectID) @@ -87,16 +122,21 @@ func GetTotalOfProjectMembers(projectID int64, roles ...int) (int64, error) { queryParam = append(queryParam, roles[0]) } - var count int64 - err := dao.GetOrmer().Raw(sql, queryParam).QueryRow(&count) + var count int + o, err := orm.FromContext(ctx) + if err != nil { + return 0, err + } + o.Raw(sql, queryParam).QueryRow(&count) return count, err } -// AddProjectMember inserts a record to table project_member -func AddProjectMember(member models.Member) (int, error) { - +func (d *dao) AddProjectMember(ctx context.Context, member models.Member) (int, error) { log.Debugf("Adding project member %+v", member) - o := dao.GetOrmer() + o, err := orm.FromContext(ctx) + if err != nil { + return 0, err + } if member.EntityID <= 0 { return 0, fmt.Errorf("Invalid entity_id, member: %+v", member) @@ -107,7 +147,7 @@ func AddProjectMember(member models.Member) (int, error) { } delSQL := "delete from project_member where project_id = ? and entity_id = ? and entity_type = ? " - _, err := o.Raw(delSQL, member.ProjectID, member.EntityID, member.EntityType).Exec() + _, err = o.Raw(delSQL, member.ProjectID, member.EntityID, member.EntityType).Exec() if err != nil { return 0, err } @@ -121,27 +161,33 @@ func AddProjectMember(member models.Member) (int, error) { return pmid, err } -// UpdateProjectMemberRole updates the record in table project_member, only role can be changed -func UpdateProjectMemberRole(pmID int, role int) error { - o := dao.GetOrmer() - sql := "update project_member set role = ? where id = ? " - _, err := o.Raw(sql, role, pmID).Exec() +func (d *dao) UpdateProjectMemberRole(ctx context.Context, projectID int64, pmID int, role int) error { + o, err := orm.FromContext(ctx) + if err != nil { + return err + } + sql := "update project_member set role = ? where project_id = ? and id = ? " + _, err = o.Raw(sql, role, projectID, pmID).Exec() return err } -// DeleteProjectMemberByID - Delete Project Member by ID -func DeleteProjectMemberByID(pmid int) error { - o := dao.GetOrmer() - sql := "delete from project_member where id = ?" - if _, err := o.Raw(sql, pmid).Exec(); err != nil { +func (d *dao) DeleteProjectMemberByID(ctx context.Context, projectID int64, pmid int) error { + o, err := orm.FromContext(ctx) + if err != nil { + return err + } + sql := "delete from project_member where project_id = ? and id = ?" + if _, err := o.Raw(sql, projectID, pmid).Exec(); err != nil { return err } return nil } -// SearchMemberByName search members of the project by entity_name -func SearchMemberByName(projectID int64, entityName string) ([]*models.Member, error) { - o := dao.GetOrmer() +func (d *dao) SearchMemberByName(ctx context.Context, projectID int64, entityName string) ([]*models.Member, error) { + o, err := orm.FromContext(ctx) + if err != nil { + return nil, err + } sql := `select pm.id, pm.project_id, u.username as entity_name, r.name as rolename, @@ -167,12 +213,11 @@ func SearchMemberByName(projectID int64, entityName string) ([]*models.Member, e queryParam = append(queryParam, "%"+orm.Escape(entityName)+"%") members := []*models.Member{} log.Debugf("Query sql: %v", sql) - _, err := o.Raw(sql, queryParam).QueryRows(&members) + _, err = o.Raw(sql, queryParam).QueryRows(&members) return members, err } -// ListRoles lists the roles of user for the specific project -func ListRoles(user *models.User, projectID int64) ([]int, error) { +func (d *dao) ListRoles(ctx context.Context, user *models.User, projectID int64) ([]int, error) { if user == nil { return nil, nil } @@ -186,12 +231,16 @@ func ListRoles(user *models.User, projectID int64) ([]int, error) { sql += fmt.Sprintf(`union select role from project_member - where entity_type = 'g' and entity_id in ( %s ) and project_id = ? `, utils.ParamPlaceholderForIn(len(user.GroupIDs))) + where entity_type = 'g' and entity_id in ( %s ) and project_id = ? `, orm.ParamPlaceholderForIn(len(user.GroupIDs))) params = append(params, user.GroupIDs) params = append(params, projectID) } roles := []int{} - _, err := dao.GetOrmer().Raw(sql, params).QueryRows(&roles) + o, err := orm.FromContext(ctx) + if err != nil { + return nil, err + } + _, err = o.Raw(sql, params).QueryRows(&roles) if err != nil { return nil, err } diff --git a/src/pkg/member/dao/dao_test.go b/src/pkg/member/dao/dao_test.go new file mode 100644 index 000000000..13b2f5739 --- /dev/null +++ b/src/pkg/member/dao/dao_test.go @@ -0,0 +1,271 @@ +// 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 dao + +import ( + "github.com/goharbor/harbor/src/common" + _ "github.com/goharbor/harbor/src/common/dao" + testDao "github.com/goharbor/harbor/src/common/dao" + comModels "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/pkg/member/models" + "github.com/goharbor/harbor/src/pkg/project" + "github.com/goharbor/harbor/src/pkg/user" + "github.com/goharbor/harbor/src/pkg/usergroup" + ugModel "github.com/goharbor/harbor/src/pkg/usergroup/model" + htesting "github.com/goharbor/harbor/src/testing" + "github.com/stretchr/testify/suite" + "testing" +) + +type DaoTestSuite struct { + htesting.Suite + dao DAO + projectMgr project.Manager + projectID int64 + userMgr user.Manager +} + +func (s *DaoTestSuite) SetupSuite() { + s.Suite.SetupSuite() + s.Suite.ClearTables = []string{"project_member"} + s.dao = New() + // Extract to test utils + initSqls := []string{ + "insert into harbor_user (username, email, password, realname) values ('member_test_01', 'member_test_01@example.com', '123456', 'member_test_01')", + "insert into project (name, owner_id) values ('member_test_01', 1)", + "insert into user_group (group_name, group_type, ldap_group_dn) values ('test_group_01', 1, 'CN=harbor_users,OU=sample,OU=vmware,DC=harbor,DC=com')", + "update project set owner_id = (select user_id from harbor_user where username = 'member_test_01') where name = 'member_test_01'", + "insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_01') , (select user_id from harbor_user where username = 'member_test_01'), 'u', 1)", + "insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_01') , (select id from user_group where group_name = 'test_group_01'), 'g', 1)", + + "insert into harbor_user (username, email, password, realname) values ('member_test_02', 'member_test_02@example.com', '123456', 'member_test_02')", + "insert into project (name, owner_id) values ('member_test_02', 1)", + "insert into user_group (group_name, group_type, ldap_group_dn) values ('test_group_02', 1, 'CN=harbor_users,OU=sample,OU=vmware,DC=harbor,DC=com')", + "update project set owner_id = (select user_id from harbor_user where username = 'member_test_02') where name = 'member_test_02'", + "insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_02') , (select user_id from harbor_user where username = 'member_test_02'), 'u', 1)", + "insert into project_member (project_id, entity_id, entity_type, role) values ( (select project_id from project where name = 'member_test_02') , (select id from user_group where group_name = 'test_group_02'), 'g', 1)", + } + + clearSqls := []string{ + "delete from project where name='member_test_01' or name='member_test_02'", + "delete from harbor_user where username='member_test_01' or username='member_test_02' or username='pm_sample'", + "delete from user_group", + "delete from project_member where id > 1", + } + testDao.PrepareTestData(clearSqls, initSqls) + s.projectMgr = project.Mgr + s.userMgr = user.Mgr + ctx := s.Context() + proj, err := s.projectMgr.Get(ctx, "member_test_01") + s.Nil(err) + s.NotNil(proj) + s.projectID = proj.ProjectID +} +func (s *DaoTestSuite) TearDownSuite() { +} + +func (s *DaoTestSuite) TestAddProjectMember() { + ctx := s.Context() + proj, err := s.projectMgr.Get(ctx, "member_test_01") + s.Nil(err) + s.NotNil(proj) + + member := models.Member{ + ProjectID: proj.ProjectID, + EntityID: 1, + EntityType: common.UserMember, + Role: common.RoleProjectAdmin, + } + pmid, err := s.dao.AddProjectMember(ctx, member) + s.Nil(err) + s.True(pmid > 0) + + queryMember := models.Member{ + ProjectID: proj.ProjectID, + ID: pmid, + } + memberList, err := s.dao.GetProjectMember(ctx, queryMember, nil) + s.Nil(err) + s.False(len(memberList) == 0) + + _, err = s.dao.AddProjectMember(ctx, models.Member{ + ProjectID: -1, + EntityID: 1, + EntityType: common.UserMember, + Role: common.RoleProjectAdmin, + }) + + s.NotNil(err) + + _, err = s.dao.AddProjectMember(ctx, models.Member{ + ProjectID: 1, + EntityID: -1, + EntityType: common.UserMember, + Role: common.RoleProjectAdmin, + }) + + s.NotNil(err) +} + +func (s *DaoTestSuite) TestUpdateProjectMemberRole() { + ctx := s.Context() + proj, err := s.projectMgr.Get(ctx, "member_test_01") + s.Nil(err) + s.NotNil(proj) + user := comModels.User{ + Username: "pm_sample", + Email: "pm_sample@example.com", + Realname: "pm_sample", + Password: "1234567d", + } + o, err := orm.FromContext(ctx) + s.Nil(err) + userID, err := o.Insert(&user) + s.Nil(err) + member := models.Member{ + ProjectID: proj.ProjectID, + EntityID: int(userID), + EntityType: common.UserMember, + Role: common.RoleProjectAdmin, + } + + pmid, err := s.dao.AddProjectMember(ctx, member) + s.Nil(err) + s.dao.UpdateProjectMemberRole(ctx, proj.ProjectID, pmid, common.RoleDeveloper) + + queryMember := models.Member{ + ProjectID: proj.ProjectID, + EntityID: int(userID), + EntityType: common.UserMember, + } + + memberList, err := s.dao.GetProjectMember(ctx, queryMember, nil) + s.Nil(err) + s.True(len(memberList) == 1, "project member should exist") + memberItem := memberList[0] + s.Equal(common.RoleDeveloper, memberItem.Role, "should be developer role") + s.Equal(user.Username, memberItem.Entityname) + + memberList2, err := s.dao.SearchMemberByName(ctx, proj.ProjectID, "pm_sample") + s.Nil(err) + s.True(len(memberList2) > 0) + + memberList3, err := s.dao.SearchMemberByName(ctx, proj.ProjectID, "") + s.Nil(err) + s.True(len(memberList3) > 0, "failed to search project member") +} + +func (s *DaoTestSuite) TestGetProjectMembers() { + ctx := s.Context() + + query1 := models.Member{ProjectID: s.projectID, Entityname: "member_test_01", EntityType: common.UserMember} + member1, err := s.dao.GetProjectMember(ctx, query1, nil) + s.Nil(err) + s.True(len(member1) > 0) + s.Equal(member1[0].Entityname, "member_test_01") + + query2 := models.Member{ProjectID: s.projectID, Entityname: "test_group_01", EntityType: common.GroupMember} + member2, err := s.dao.GetProjectMember(ctx, query2, nil) + s.Nil(err) + s.True(len(member2) > 0) + s.Equal(member2[0].Entityname, "test_group_01") +} + +func (s *DaoTestSuite) TestGetTotalOfProjectMembers() { + ctx := s.Context() + tot, err := s.dao.GetTotalOfProjectMembers(ctx, s.projectID, nil) + s.Nil(err) + s.Equal(2, int(tot)) +} + +func (s *DaoTestSuite) TestListRoles() { + ctx := s.Context() + + // nil user + roles, err := s.dao.ListRoles(ctx, nil, 1) + s.Nil(err) + s.Len(roles, 0) + + // user with empty groups + u, err := s.userMgr.GetByName(ctx, "member_test_01") + s.Nil(err) + s.NotNil(u) + user := &models.User{ + UserID: u.UserID, + Username: u.Username, + } + roles, err = s.dao.ListRoles(ctx, user, s.projectID) + s.Nil(err) + s.Len(roles, 1) + + // user with a group whose ID doesn't exist + user.GroupIDs = []int{9999} + roles, err = s.dao.ListRoles(ctx, user, s.projectID) + s.Nil(err) + s.Len(roles, 1) + s.Equal(common.RoleProjectAdmin, roles[0]) + + // user with a valid group + groupID, err := usergroup.Mgr.Create(ctx, ugModel.UserGroup{ + GroupName: "group_for_list_role", + GroupType: 1, + LdapGroupDN: "CN=list_role_users,OU=sample,OU=vmware,DC=harbor,DC=com", + }) + + s.Nil(err) + defer usergroup.Mgr.Delete(ctx, groupID) + + memberID, err := s.dao.AddProjectMember(ctx, models.Member{ + ProjectID: s.projectID, + Role: common.RoleDeveloper, + EntityID: groupID, + EntityType: "g", + }) + s.Nil(err) + defer s.dao.DeleteProjectMemberByID(ctx, s.projectID, memberID) + + user.GroupIDs = []int{groupID} + roles, err = s.dao.ListRoles(ctx, user, s.projectID) + s.Nil(err) + s.Len(roles, 2) + s.Equal(common.RoleProjectAdmin, roles[0]) + s.Equal(common.RoleDeveloper, roles[1]) +} + +func (s *DaoTestSuite) TestDeleteProjectMember() { + ctx := s.Context() + var addMember = models.Member{ + ProjectID: s.projectID, + EntityID: 1, + EntityType: common.UserMember, + Role: common.RoleDeveloper, + } + pmid, err := s.dao.AddProjectMember(ctx, addMember) + s.Nil(err) + s.True(pmid > 0) + + err = s.dao.DeleteProjectMemberByID(ctx, s.projectID, pmid) + s.Nil(err) + + // not exist + err = s.dao.DeleteProjectMemberByID(ctx, s.projectID, -1) + s.Nil(err) + +} + +func TestDaoTestSuite(t *testing.T) { + suite.Run(t, &DaoTestSuite{}) +} diff --git a/src/pkg/member/manager.go b/src/pkg/member/manager.go new file mode 100644 index 000000000..51156d644 --- /dev/null +++ b/src/pkg/member/manager.go @@ -0,0 +1,101 @@ +// 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 member + +import ( + "context" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/member/dao" + "github.com/goharbor/harbor/src/pkg/member/models" +) + +var ( + // Mgr default project member manager + Mgr = NewManager() +) + +// Manager is used to manage the project member +type Manager interface { + // AddProjectMember add project member + AddProjectMember(ctx context.Context, member models.Member) (int, error) + // Delete delete project member + Delete(ctx context.Context, projectID int64, memberID int) error + // Get get project member by ID + Get(ctx context.Context, projectID int64, memberID int) (*models.Member, error) + // List list the project member by conditions + List(ctx context.Context, queryMember models.Member, query *q.Query) ([]*models.Member, error) + // UpdateRole update project member's role + UpdateRole(ctx context.Context, projectID int64, pmID int, role int) error + // SearchMemberByName search project member by name + SearchMemberByName(ctx context.Context, projectID int64, entityName string) ([]*models.Member, error) + // GetTotalOfProjectMembers get the total amount of project members + GetTotalOfProjectMembers(ctx context.Context, projectID int64, query *q.Query, roles ...int) (int, error) + // ListRoles list project roles + ListRoles(ctx context.Context, user *models.User, projectID int64) ([]int, error) +} + +type manager struct { + dao dao.DAO +} + +func (m *manager) Get(ctx context.Context, projectID int64, memberID int) (*models.Member, error) { + query := models.Member{ + ID: memberID, + ProjectID: projectID, + } + pm, err := m.dao.GetProjectMember(ctx, query, nil) + if err != nil { + return nil, err + } + if len(pm) == 0 { + return nil, errors.NotFoundError(nil). + WithMessage("the project member is not found, project id %v, member id %v", projectID, memberID) + } + return pm[0], nil +} + +func (m *manager) AddProjectMember(ctx context.Context, member models.Member) (int, error) { + return m.dao.AddProjectMember(ctx, member) +} + +func (m *manager) UpdateRole(ctx context.Context, projectID int64, pmID int, role int) error { + return m.dao.UpdateProjectMemberRole(ctx, projectID, pmID, role) +} + +func (m *manager) SearchMemberByName(ctx context.Context, projectID int64, entityName string) ([]*models.Member, error) { + return m.dao.SearchMemberByName(ctx, projectID, entityName) +} + +func (m *manager) GetTotalOfProjectMembers(ctx context.Context, projectID int64, query *q.Query, roles ...int) (int, error) { + return m.dao.GetTotalOfProjectMembers(ctx, projectID, query, roles...) +} + +func (m *manager) ListRoles(ctx context.Context, user *models.User, projectID int64) ([]int, error) { + return m.dao.ListRoles(ctx, user, projectID) +} + +func (m *manager) List(ctx context.Context, queryMember models.Member, query *q.Query) ([]*models.Member, error) { + return m.dao.GetProjectMember(ctx, queryMember, query) +} + +func (m *manager) Delete(ctx context.Context, projectID int64, memberID int) error { + return m.dao.DeleteProjectMemberByID(ctx, projectID, memberID) +} + +// NewManager ... +func NewManager() Manager { + return &manager{dao: dao.New()} +} diff --git a/src/pkg/member/models/member.go b/src/pkg/member/models/member.go new file mode 100644 index 000000000..2d1b53a5d --- /dev/null +++ b/src/pkg/member/models/member.go @@ -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 models + +// Member holds the details of a member. +type Member struct { + ID int `orm:"pk;column(id)" json:"id"` + ProjectID int64 `orm:"column(project_id)" json:"project_id"` + Entityname string `orm:"column(entity_name)" json:"entity_name"` + Rolename string `json:"role_name"` + Role int `json:"role_id"` + EntityID int `orm:"column(entity_id)" json:"entity_id"` + EntityType string `orm:"column(entity_type)" json:"entity_type"` +} + +// User ... +type User struct { + UserID int + Username string + GroupIDs []int +} diff --git a/src/server/v2.0/handler/handler.go b/src/server/v2.0/handler/handler.go index 5d828ca25..7668abca4 100644 --- a/src/server/v2.0/handler/handler.go +++ b/src/server/v2.0/handler/handler.go @@ -39,6 +39,7 @@ func New() http.Handler { ScanAllAPI: newScanAllAPI(), SearchAPI: newSearchAPI(), ProjectAPI: newProjectAPI(), + MemberAPI: newMemberAPI(), PreheatAPI: newPreheatAPI(), IconAPI: newIconAPI(), RobotAPI: newRobotAPI(), diff --git a/src/server/v2.0/handler/member.go b/src/server/v2.0/handler/member.go new file mode 100644 index 000000000..6b7f28d44 --- /dev/null +++ b/src/server/v2.0/handler/member.go @@ -0,0 +1,171 @@ +// 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 handler + +import ( + "context" + "encoding/json" + "fmt" + "github.com/go-openapi/runtime/middleware" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/controller/member" + "github.com/goharbor/harbor/src/lib" + "github.com/goharbor/harbor/src/lib/errors" + memberModels "github.com/goharbor/harbor/src/pkg/member/models" + "github.com/goharbor/harbor/src/server/v2.0/models" + operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/member" +) + +type memberAPI struct { + BaseAPI + ctl member.Controller +} + +func newMemberAPI() *memberAPI { + return &memberAPI{ctl: member.NewController()} +} + +func (m *memberAPI) CreateProjectMember(ctx context.Context, params operation.CreateProjectMemberParams) middleware.Responder { + projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName) + if err := m.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionCreate, rbac.ResourceMember); err != nil { + return m.SendError(ctx, err) + } + if params.ProjectMember == nil { + return m.SendError(ctx, errors.BadRequestError(nil).WithMessage("the project member should provide")) + } + req, err := toMemberReq(params.ProjectMember) + if err != nil { + return m.SendError(ctx, err) + } + id, err := m.ctl.Create(ctx, projectNameOrID, *req) + if err != nil { + return m.SendError(ctx, err) + } + return operation.NewCreateProjectMemberCreated(). + WithLocation(fmt.Sprintf("/api/v2.0/projects/%v/members/%d", projectNameOrID, id)) +} + +func toMemberReq(memberReq *models.ProjectMember) (*member.Request, error) { + data, err := json.Marshal(memberReq) + if err != nil { + return nil, err + } + var result member.Request + err = json.Unmarshal(data, &result) + if err != nil { + return nil, err + } + return &result, nil +} + +func (m *memberAPI) DeleteProjectMember(ctx context.Context, params operation.DeleteProjectMemberParams) middleware.Responder { + projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName) + if err := m.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionDelete, rbac.ResourceMember); err != nil { + return m.SendError(ctx, err) + } + if params.Mid == 0 { + return m.SendError(ctx, errors.BadRequestError(nil).WithMessage("the project member id is required.")) + } + err := m.ctl.Delete(ctx, projectNameOrID, int(params.Mid)) + if err != nil { + return m.SendError(ctx, err) + } + return operation.NewDeleteProjectMemberOK() +} + +func (m *memberAPI) GetProjectMember(ctx context.Context, params operation.GetProjectMemberParams) middleware.Responder { + projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName) + if err := m.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionRead, rbac.ResourceMember); err != nil { + return m.SendError(ctx, err) + } + + if params.Mid == 0 { + return m.SendError(ctx, errors.BadRequestError(nil).WithMessage("the member id can not be empty!")) + } + + member, err := m.ctl.Get(ctx, projectNameOrID, int(params.Mid)) + if err != nil { + return m.SendError(ctx, err) + } + return operation.NewGetProjectMemberOK().WithPayload(toProjectMemberResp(member)) +} + +func (m *memberAPI) ListProjectMembers(ctx context.Context, params operation.ListProjectMembersParams) middleware.Responder { + projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName) + if err := m.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionList, rbac.ResourceMember); err != nil { + return m.SendError(ctx, err) + } + entityName := lib.StringValue(params.Entityname) + query, err := m.BuildQuery(ctx, nil, nil, params.Page, params.PageSize) + if err != nil { + return m.SendError(ctx, err) + } + total, err := m.ctl.Count(ctx, projectNameOrID, query) + if err != nil { + return m.SendError(ctx, err) + } + if total == 0 { + return operation.NewListProjectMembersOK(). + WithXTotalCount(0). + WithPayload([]*models.ProjectMemberEntity{}) + } + members, err := m.ctl.List(ctx, projectNameOrID, entityName, query) + if err != nil { + return m.SendError(ctx, err) + } + return operation.NewListProjectMembersOK(). + WithXTotalCount(int64(total)). + WithLink(m.Links(ctx, params.HTTPRequest.URL, int64(total), query.PageNumber, query.PageSize).String()). + WithPayload(toProjectMemberRespList(members)) +} + +func toProjectMemberRespList(members []*memberModels.Member) []*models.ProjectMemberEntity { + result := make([]*models.ProjectMemberEntity, 0) + for _, mem := range members { + result = append(result, toProjectMemberResp(mem)) + } + return result +} + +func toProjectMemberResp(member *memberModels.Member) *models.ProjectMemberEntity { + return &models.ProjectMemberEntity{ + ProjectID: member.ProjectID, + ID: int64(member.ID), + EntityName: member.Entityname, + EntityID: int64(member.EntityID), + EntityType: member.EntityType, + RoleID: int64(member.Role), + RoleName: member.Rolename, + } +} + +func (m *memberAPI) UpdateProjectMember(ctx context.Context, params operation.UpdateProjectMemberParams) middleware.Responder { + projectNameOrID := parseProjectNameOrID(params.ProjectNameOrID, params.XIsResourceName) + if err := m.RequireProjectAccess(ctx, projectNameOrID, rbac.ActionUpdate, rbac.ResourceMember); err != nil { + return m.SendError(ctx, err) + } + if params.Role == nil { + return m.SendError(ctx, errors.BadRequestError(nil).WithMessage("role can not be empty!")) + } + if params.Mid == 0 { + return m.SendError(ctx, errors.BadRequestError(nil).WithMessage("member id can not be empty!")) + } + + err := m.ctl.UpdateRole(ctx, projectNameOrID, int(params.Mid), int(params.Role.RoleID)) + if err != nil { + return m.SendError(ctx, err) + } + return operation.NewUpdateProjectMemberOK() +} diff --git a/src/server/v2.0/handler/project.go b/src/server/v2.0/handler/project.go index d19f6358e..7ff8db799 100644 --- a/src/server/v2.0/handler/project.go +++ b/src/server/v2.0/handler/project.go @@ -17,15 +17,16 @@ package handler import ( "context" "fmt" - "github.com/goharbor/harbor/src/controller/config" "strconv" "strings" "sync" + "github.com/goharbor/harbor/src/controller/config" + "github.com/goharbor/harbor/src/pkg/member" + "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" "github.com/goharbor/harbor/src/common" - pro "github.com/goharbor/harbor/src/common/dao/project" "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/common/security/local" @@ -63,6 +64,7 @@ func newProjectAPI() *projectAPI { userMgr: user.Mgr, repositoryCtl: repository.Ctl, projectCtl: project.Ctl, + memberMgr: member.Mgr, quotaCtl: quota.Ctl, robotMgr: robot.Mgr, preheatCtl: preheat.Ctl, @@ -78,6 +80,7 @@ type projectAPI struct { userMgr user.Manager repositoryCtl repository.Controller projectCtl project.Controller + memberMgr member.Manager quotaCtl quota.Controller robotMgr robot.Manager preheatCtl preheat.Controller @@ -343,7 +346,7 @@ func (a *projectAPI) GetProjectSummary(ctx context.Context, params operation.Get } if hasPerm := a.HasProjectPermission(ctx, p.ProjectID, rbac.ActionList, rbac.ResourceMember); hasPerm { - fetchSummaries = append(fetchSummaries, getProjectMemberSummary) + fetchSummaries = append(fetchSummaries, a.getProjectMemberSummary) } if p.IsProxy() { @@ -696,7 +699,7 @@ func getProjectQuotaSummary(ctx context.Context, p *project.Project, summary *mo } } -func getProjectMemberSummary(ctx context.Context, p *project.Project, summary *models.ProjectSummary) { +func (a *projectAPI) getProjectMemberSummary(ctx context.Context, p *project.Project, summary *models.ProjectSummary) { var wg sync.WaitGroup for _, e := range []struct { @@ -712,14 +715,13 @@ func getProjectMemberSummary(ctx context.Context, p *project.Project, summary *m wg.Add(1) go func(role int, count *int64) { defer wg.Done() - - total, err := pro.GetTotalOfProjectMembers(p.ProjectID, role) + total, err := a.memberMgr.GetTotalOfProjectMembers(orm.Clone(ctx), p.ProjectID, nil, role) if err != nil { log.Warningf("failed to get total of project members of role %d", role) return } - *count = total + *count = int64(total) }(e.role, e.count) } diff --git a/src/server/v2.0/route/legacy.go b/src/server/v2.0/route/legacy.go index b5e2569d4..cb22218a7 100755 --- a/src/server/v2.0/route/legacy.go +++ b/src/server/v2.0/route/legacy.go @@ -23,7 +23,6 @@ import ( // RegisterRoutes for Harbor legacy APIs func registerLegacyRoutes() { version := APIVersion - beego.Router("/api/"+version+"/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &api.ProjectMemberAPI{}) beego.Router("/api/"+version+"/email/ping", &api.EmailAPI{}, "post:Ping") beego.Router("/api/"+version+"/health", &api.HealthAPI{}, "get:CheckHealth") beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get") diff --git a/tests/apitests/python/library/base.py b/tests/apitests/python/library/base.py index fbab5481b..d63414269 100644 --- a/tests/apitests/python/library/base.py +++ b/tests/apitests/python/library/base.py @@ -30,8 +30,8 @@ def get_endpoint(): def _create_client(server, credential, debug, api_type="products"): cfg = None if api_type in ('projectv2', 'artifact', 'repository', 'scanner', 'scan', 'scanall', 'preheat', 'quota', - 'replication', 'registry', 'robot', 'gc', 'retention', "immutable", "system_cve_allowlist", - "configure", "users"): + 'replication', 'registry', 'robot', 'gc', 'retention', 'immutable', 'system_cve_allowlist', + 'configure', 'users', 'member'): cfg = v2_swagger_client.Configuration() else: cfg = swagger_client.Configuration() @@ -73,6 +73,7 @@ def _create_client(server, credential, debug, api_type="products"): "system_cve_allowlist": v2_swagger_client.SystemCVEAllowlistApi(v2_swagger_client.ApiClient(cfg)), "configure": v2_swagger_client.ConfigureApi(v2_swagger_client.ApiClient(cfg)), "users": v2_swagger_client.UsersApi(v2_swagger_client.ApiClient(cfg)), + "member": v2_swagger_client.MemberApi(v2_swagger_client.ApiClient(cfg)), }.get(api_type,'Error: Wrong API type') def _assert_status_code(expect_code, return_code, err_msg = r"HTTPS status code s not as we expected. Expected {}, while actual HTTPS status code is {}."): diff --git a/tests/apitests/python/library/project.py b/tests/apitests/python/library/project.py index 4bdb2e41c..79d968d6e 100644 --- a/tests/apitests/python/library/project.py +++ b/tests/apitests/python/library/project.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- -import base import swagger_client import v2_swagger_client -from v2_swagger_client.rest import ApiException from library.base import _assert_status_code +from v2_swagger_client.rest import ApiException + +import base + def is_member_exist_in_project(members, member_user_name, expected_member_role_id = None): result = False @@ -125,15 +127,15 @@ class Project(base.Base): return count def get_project_members(self, project_id, **kwargs): - kwargs['api_type'] = 'products' - return self._get_client(**kwargs).projects_project_id_members_get(project_id) + kwargs['api_type'] = 'member' + return self._get_client(**kwargs).list_project_members(project_id) def get_project_member(self, project_id, member_id, expect_status_code = 200, expect_response_body = None, **kwargs): from swagger_client.rest import ApiException - kwargs['api_type'] = 'products' + kwargs['api_type'] = 'member' data = [] try: - data, status_code, _ = self._get_client(**kwargs).projects_project_id_members_mid_get_with_http_info(project_id, member_id,) + data, status_code, _ = self._get_client(**kwargs).get_project_member_with_http_info(project_id, member_id,) except ApiException as e: base._assert_status_code(expect_status_code, e.status) if expect_response_body is not None: @@ -145,7 +147,7 @@ class Project(base.Base): return data def get_project_member_id(self, project_id, member_user_name, **kwargs): - kwargs['api_type'] = 'products' + kwargs['api_type'] = 'member' members = self.get_project_members(project_id, **kwargs) result = get_member_id_by_name(list(members), member_user_name) if result == None: @@ -154,36 +156,36 @@ class Project(base.Base): return result def check_project_member_not_exist(self, project_id, member_user_name, **kwargs): - kwargs['api_type'] = 'products' + kwargs['api_type'] = 'member' members = self.get_project_members(project_id, **kwargs) result = is_member_exist_in_project(list(members), member_user_name) if result == True: raise Exception(r"User {} should not be a member of project with ID {}.".format(member_user_name, project_id)) def check_project_members_exist(self, project_id, member_user_name, expected_member_role_id = None, **kwargs): - kwargs['api_type'] = 'products' + kwargs['api_type'] = 'member' members = self.get_project_members(project_id, **kwargs) result = is_member_exist_in_project(members, member_user_name, expected_member_role_id = expected_member_role_id) if result == False: raise Exception(r"User {} should be a member of project with ID {}.".format(member_user_name, project_id)) def update_project_member_role(self, project_id, member_id, member_role_id, expect_status_code = 200, **kwargs): - kwargs['api_type'] = 'products' + kwargs['api_type'] = 'member' role = swagger_client.Role(role_id = member_role_id) - data, status_code, _ = self._get_client(**kwargs).projects_project_id_members_mid_put_with_http_info(project_id, member_id, role = role) + data, status_code, _ = self._get_client(**kwargs).update_project_member_with_http_info(project_id, member_id, role = role) base._assert_status_code(expect_status_code, status_code) base._assert_status_code(200, status_code) return data def delete_project_member(self, project_id, member_id, expect_status_code = 200, **kwargs): - kwargs['api_type'] = 'products' - _, status_code, _ = self._get_client(**kwargs).projects_project_id_members_mid_delete_with_http_info(project_id, member_id) + kwargs['api_type'] = 'member' + _, status_code, _ = self._get_client(**kwargs).delete_project_member_with_http_info(project_id, member_id) base._assert_status_code(expect_status_code, status_code) base._assert_status_code(200, status_code) def add_project_members(self, project_id, user_id = None, member_role_id = None, _ldap_group_dn=None, expect_status_code = 201, **kwargs): - kwargs['api_type'] = 'products' - projectMember = swagger_client.ProjectMember() + kwargs['api_type'] = 'member' + projectMember = v2_swagger_client.ProjectMember() if user_id is not None: projectMember.member_user = {"user_id": int(user_id)} if member_role_id is None: @@ -191,12 +193,12 @@ class Project(base.Base): else: projectMember.role_id = member_role_id if _ldap_group_dn is not None: - projectMember.member_group = swagger_client.UserGroup(ldap_group_dn=_ldap_group_dn) + projectMember.member_group = v2_swagger_client.UserGroup(ldap_group_dn=_ldap_group_dn) data = [] try: - data, status_code, header = self._get_client(**kwargs).projects_project_id_members_post_with_http_info(project_id, project_member = projectMember) - except swagger_client.rest.ApiException as e: + data, status_code, header = self._get_client(**kwargs).create_project_member_with_http_info(project_id, project_member = projectMember) + except ApiException as e: base._assert_status_code(expect_status_code, e.status) else: base._assert_status_code(expect_status_code, status_code)