Merge pull request #14577 from stonezdj/21mar30_projectmember

Refactor project member api to new programming model
This commit is contained in:
stonezdj(Daojun Zhang) 2021-04-13 22:24:07 +08:00 committed by GitHub
commit 81a72c0435
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1250 additions and 1405 deletions

View File

@ -176,170 +176,6 @@ paths:
description: Project or metadata does not exist. description: Project or metadata does not exist.
'500': '500':
description: Internal server errors. 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: /statistics:
get: get:
summary: Get projects number and repositories number relevant to the user summary: Get projects number and repositories number relevant to the user

View File

@ -447,6 +447,170 @@ paths:
$ref: '#/responses/404' $ref: '#/responses/404'
'500': '500':
$ref: '#/responses/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: /repositories:
get: get:
summary: List all authorized repositories summary: List all authorized repositories
@ -4783,6 +4947,7 @@ parameters:
required: true required: true
type: integer type: integer
format: int64 format: int64
responses: responses:
'200': '200':
description: Success description: Success
@ -7392,6 +7557,55 @@ definitions:
editable: editable:
type: boolean type: boolean
description: The configure item can be updated or not 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: UserProfile:
type: object type: object
properties: properties:

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/goharbor/harbor/src/lib/orm"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -30,9 +31,10 @@ import (
"github.com/dghubble/sling" "github.com/dghubble/sling"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/dao/project"
common_http "github.com/goharbor/harbor/src/common/http" common_http "github.com/goharbor/harbor/src/common/http"
"github.com/goharbor/harbor/src/common/models" "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" htesting "github.com/goharbor/harbor/src/testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -249,8 +251,8 @@ func prepare() error {
if err != nil { if err != nil {
return err return err
} }
ctx := orm.Context()
if projAdminPMID, err = project.AddProjectMember(models.Member{ if projAdminPMID, err = member.Mgr.AddProjectMember(ctx, memberModels.Member{
ProjectID: 1, ProjectID: 1,
Role: common.RoleProjectAdmin, Role: common.RoleProjectAdmin,
EntityID: int(projAdminID), EntityID: int(projAdminID),
@ -268,8 +270,7 @@ func prepare() error {
if err != nil { if err != nil {
return err return err
} }
if projAdminRobotPMID, err = member.Mgr.AddProjectMember(ctx, memberModels.Member{
if projAdminRobotPMID, err = project.AddProjectMember(models.Member{
ProjectID: 1, ProjectID: 1,
Role: common.RoleProjectAdmin, Role: common.RoleProjectAdmin,
EntityID: int(projAdminRobotID), EntityID: int(projAdminRobotID),
@ -288,7 +289,7 @@ func prepare() error {
return err return err
} }
if projDeveloperPMID, err = project.AddProjectMember(models.Member{ if projDeveloperPMID, err = member.Mgr.AddProjectMember(ctx, memberModels.Member{
ProjectID: 1, ProjectID: 1,
Role: common.RoleDeveloper, Role: common.RoleDeveloper,
EntityID: int(projDeveloperID), EntityID: int(projDeveloperID),
@ -307,7 +308,7 @@ func prepare() error {
return err return err
} }
if projGuestPMID, err = project.AddProjectMember(models.Member{ if projGuestPMID, err = member.Mgr.AddProjectMember(ctx, memberModels.Member{
ProjectID: 1, ProjectID: 1,
Role: common.RoleGuest, Role: common.RoleGuest,
EntityID: int(projGuestID), EntityID: int(projGuestID),
@ -325,7 +326,7 @@ func prepare() error {
if err != nil { if err != nil {
return err return err
} }
if projLimitedGuestPMID, err = project.AddProjectMember(models.Member{ if projLimitedGuestPMID, err = member.Mgr.AddProjectMember(ctx, memberModels.Member{
ProjectID: 1, ProjectID: 1,
Role: common.RoleLimitedGuest, Role: common.RoleLimitedGuest,
EntityID: int(projLimitedGuestID), EntityID: int(projLimitedGuestID),
@ -340,7 +341,7 @@ func clean() {
pmids := []int{projAdminPMID, projDeveloperPMID, projGuestPMID} pmids := []int{projAdminPMID, projDeveloperPMID, projGuestPMID}
for _, id := range pmids { 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) fmt.Printf("failed to clean up member %d from project library: %v", id, err)
} }
} }

View File

@ -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/?:name", &MetadataAPI{}, "get:Get")
beego.Router("/api/projects/:id([0-9]+)/metadatas/", &MetadataAPI{}, "post:Post") 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/: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/statistics", &StatisticAPI{})
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping") beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
beego.Router("/api/labels", &LabelAPI{}, "post:Post;get:List") beego.Router("/api/labels", &LabelAPI{}, "post:Post;get:List")

View File

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

View File

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

View File

@ -14,22 +14,22 @@
package ldap package ldap
import ( import (
"github.com/goharbor/harbor/src/pkg/usergroup"
"os" "os"
"testing" "testing"
"github.com/goharbor/harbor/src/controller/config" "github.com/goharbor/harbor/src/controller/config"
"github.com/goharbor/harbor/src/lib/orm" "github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/usergroup" ugModel "github.com/goharbor/harbor/src/pkg/usergroup/model"
"github.com/goharbor/harbor/src/pkg/usergroup/model"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao" "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/models"
"github.com/goharbor/harbor/src/common/utils/test" "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/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" "github.com/goharbor/harbor/src/core/auth"
) )
@ -267,7 +267,7 @@ func TestAuthenticateHelperOnBoardUser(t *testing.T) {
} }
func TestOnBoardGroup(t *testing.T) { func TestOnBoardGroup(t *testing.T) {
group := model.UserGroup{ group := ugModel.UserGroup{
GroupName: "harbor_group2", GroupName: "harbor_group2",
LdapGroupDN: "cn=harbor_group2,ou=groups,dc=example,dc=com", LdapGroupDN: "cn=harbor_group2,ou=groups,dc=example,dc=com",
} }
@ -360,18 +360,21 @@ func TestSearchAndOnBoardUser(t *testing.T) {
} }
} }
func TestAddProjectMemberWithLdapUser(t *testing.T) { func TestAddProjectMemberWithLdapUser(t *testing.T) {
memberMgr := member.Mgr
ctx := orm.Context()
currentProject, err := dao.GetProjectByName("member_test_01") currentProject, err := dao.GetProjectByName("member_test_01")
if err != nil { if err != nil {
t.Errorf("Error occurred when GetProjectByName: %v", err) t.Errorf("Error occurred when GetProjectByName: %v", err)
} }
member := models.MemberReq{ userID, err := auth.SearchAndOnBoardUser("mike")
member := memberModels.Member{
ProjectID: currentProject.ProjectID, ProjectID: currentProject.ProjectID,
MemberUser: models.User{ EntityType: common.UserMember,
Username: "mike", Entityname: "mike",
}, EntityID: userID,
Role: common.RoleProjectAdmin, Role: common.RoleProjectAdmin,
} }
pmid, err := api.AddProjectMember(currentProject.ProjectID, member) pmid, err := memberMgr.AddProjectMember(ctx, member)
if err != nil { if err != nil {
t.Errorf("Error occurred in AddOrUpdateProjectMember: %v", err) t.Errorf("Error occurred in AddOrUpdateProjectMember: %v", err)
} }
@ -383,14 +386,14 @@ func TestAddProjectMemberWithLdapUser(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Error occurred when GetProjectByName: %v", err) t.Errorf("Error occurred when GetProjectByName: %v", err)
} }
member2 := models.MemberReq{ member2 := memberModels.Member{
ProjectID: currentProject.ProjectID, ProjectID: currentProject.ProjectID,
MemberUser: models.User{ EntityType: common.UserMember,
Username: "mike", Entityname: "mike",
}, EntityID: userID,
Role: common.RoleProjectAdmin, Role: common.RoleProjectAdmin,
} }
pmid, err = api.AddProjectMember(currentProject.ProjectID, member2) pmid, err = memberMgr.AddProjectMember(ctx, member2)
if err != nil { if err != nil {
t.Errorf("Error occurred in AddOrUpdateProjectMember: %v", err) t.Errorf("Error occurred in AddOrUpdateProjectMember: %v", err)
} }
@ -399,30 +402,31 @@ func TestAddProjectMemberWithLdapUser(t *testing.T) {
} }
} }
func TestAddProjectMemberWithLdapGroup(t *testing.T) { func TestAddProjectMemberWithLdapGroup(t *testing.T) {
memberMgr := member.Mgr
ctx := orm.Context()
currentProject, err := dao.GetProjectByName("member_test_01") currentProject, err := dao.GetProjectByName("member_test_01")
if err != nil { if err != nil {
t.Errorf("Error occurred when GetProjectByName: %v", err) 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}} 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(orm.Context(), userGroups) groupIds, err := usergroup.Mgr.Populate(ctx, userGroups)
member := models.MemberReq{ m := memberModels.Member{
ProjectID: currentProject.ProjectID, ProjectID: currentProject.ProjectID,
MemberGroup: model.UserGroup{ EntityType: common.GroupMember,
ID: groupIds[0], EntityID: groupIds[0],
},
Role: common.RoleProjectAdmin, Role: common.RoleProjectAdmin,
} }
pmid, err := api.AddProjectMember(currentProject.ProjectID, member) pmid, err := memberMgr.AddProjectMember(ctx, m)
if err != nil { if err != nil {
t.Errorf("Error occurred in AddOrUpdateProjectMember: %v", err) t.Errorf("Error occurred in AddOrUpdateProjectMember: %v", err)
} }
if pmid == 0 { if pmid == 0 {
t.Errorf("Error occurred in AddOrUpdateProjectMember: pmid: %v", pmid) t.Errorf("Error occurred in AddOrUpdateProjectMember: pmid: %v", pmid)
} }
queryMember := models.Member{ queryMember := memberModels.Member{
ProjectID: currentProject.ProjectID, ProjectID: currentProject.ProjectID,
} }
memberList, err := project.GetProjectMember(queryMember) memberList, err := member.Mgr.List(ctx, queryMember, nil)
if err != nil { if err != nil {
t.Errorf("Failed to query project member, %v, error: %v", queryMember, err) t.Errorf("Failed to query project member, %v, error: %v", queryMember, err)
} }

View File

@ -70,6 +70,25 @@ func QuerySetter(ctx context.Context, model interface{}, query *q.Query) (orm.Qu
return qs, nil 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 // 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) { func QuerySetterForCount(ctx context.Context, model interface{}, query *q.Query, ignoredCols ...string) (orm.QuerySeter, error) {
query = q.MustClone(query) query = q.MustClone(query)

View File

@ -18,6 +18,7 @@ import (
"context" "context"
"errors" "errors"
. "github.com/goharbor/harbor/src/lib/orm" . "github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"sync" "sync"
"testing" "testing"
@ -410,6 +411,19 @@ func (suite *OrmSuite) TestReadOrCreateParallel() {
suite.Equal(1, sum) 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) { func TestRunOrmSuite(t *testing.T) {
suite.Run(t, new(OrmSuite)) suite.Run(t, new(OrmSuite))
} }

View File

@ -10,13 +10,14 @@ import (
"github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/dao" "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/models"
"github.com/goharbor/harbor/src/common/utils/test" "github.com/goharbor/harbor/src/common/utils/test"
proctl "github.com/goharbor/harbor/src/controller/project" proctl "github.com/goharbor/harbor/src/controller/project"
quotactl "github.com/goharbor/harbor/src/controller/quota" quotactl "github.com/goharbor/harbor/src/controller/quota"
"github.com/goharbor/harbor/src/lib/orm" "github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/artifact" "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" qtypes "github.com/goharbor/harbor/src/pkg/quota/types"
"github.com/goharbor/harbor/src/pkg/repository" "github.com/goharbor/harbor/src/pkg/repository"
) )
@ -105,15 +106,17 @@ func setupTest(t *testing.T) {
// Add member to project // Add member to project
pmIDs = make([]int, 0) pmIDs = make([]int, 0)
alice.UserID, bob.UserID, eve.UserID = int(aliceID), int(bobID), int(eveID) 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 { if err != nil {
t.Errorf("add project member error %v", err) 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 { if err != nil {
t.Errorf("add project member error %v", err) 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 { if err != nil {
t.Errorf("add project member error %v", err) t.Errorf("add project member error %v", err)
} }

View File

@ -12,25 +12,60 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package project package dao
import ( import (
"context"
"fmt" "fmt"
"github.com/goharbor/harbor/src/common/dao" "github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm" "github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/pkg/member/models"
) )
// GetProjectMember gets all members of the project. func init() {
func GetProjectMember(queryMember models.Member) ([]*models.Member, error) { 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) log.Debugf("Query condition %+v", queryMember)
if queryMember.ProjectID == 0 { if queryMember.ProjectID == 0 {
return nil, fmt.Errorf("Failed to query project member, query condition %v", queryMember) 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, 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 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' 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) queryParam = append(queryParam, queryMember.ID)
} }
sql += ` order by entity_name ` sql += ` order by entity_name `
sql, queryParam = orm.PaginationOnRawSQL(query, sql, queryParam)
members := []*models.Member{} members := []*models.Member{}
_, err := o.Raw(sql, queryParam).QueryRows(&members) _, err = o.Raw(sql, queryParam).QueryRows(&members)
return members, err return members, err
} }
// GetTotalOfProjectMembers returns total of project members func (d *dao) GetTotalOfProjectMembers(ctx context.Context, projectID int64, query *q.Query, roles ...int) (int, error) {
func GetTotalOfProjectMembers(projectID int64, roles ...int) (int64, error) {
log.Debugf("Query condition %+v", projectID) log.Debugf("Query condition %+v", projectID)
if projectID == 0 { if projectID == 0 {
return 0, fmt.Errorf("failed to get total of project members, project id required %v", projectID) 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]) queryParam = append(queryParam, roles[0])
} }
var count int64 var count int
err := dao.GetOrmer().Raw(sql, queryParam).QueryRow(&count) o, err := orm.FromContext(ctx)
if err != nil {
return 0, err
}
o.Raw(sql, queryParam).QueryRow(&count)
return count, err return count, err
} }
// AddProjectMember inserts a record to table project_member func (d *dao) AddProjectMember(ctx context.Context, member models.Member) (int, error) {
func AddProjectMember(member models.Member) (int, error) {
log.Debugf("Adding project member %+v", member) 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 { if member.EntityID <= 0 {
return 0, fmt.Errorf("Invalid entity_id, member: %+v", member) 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 = ? " 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 { if err != nil {
return 0, err return 0, err
} }
@ -121,27 +161,33 @@ func AddProjectMember(member models.Member) (int, error) {
return pmid, err return pmid, err
} }
// UpdateProjectMemberRole updates the record in table project_member, only role can be changed func (d *dao) UpdateProjectMemberRole(ctx context.Context, projectID int64, pmID int, role int) error {
func UpdateProjectMemberRole(pmID int, role int) error { o, err := orm.FromContext(ctx)
o := dao.GetOrmer() if err != nil {
sql := "update project_member set role = ? where id = ? " return err
_, err := o.Raw(sql, role, pmID).Exec() }
sql := "update project_member set role = ? where project_id = ? and id = ? "
_, err = o.Raw(sql, role, projectID, pmID).Exec()
return err return err
} }
// DeleteProjectMemberByID - Delete Project Member by ID func (d *dao) DeleteProjectMemberByID(ctx context.Context, projectID int64, pmid int) error {
func DeleteProjectMemberByID(pmid int) error { o, err := orm.FromContext(ctx)
o := dao.GetOrmer() if err != nil {
sql := "delete from project_member where id = ?" return err
if _, err := o.Raw(sql, pmid).Exec(); err != nil { }
sql := "delete from project_member where project_id = ? and id = ?"
if _, err := o.Raw(sql, projectID, pmid).Exec(); err != nil {
return err return err
} }
return nil return nil
} }
// SearchMemberByName search members of the project by entity_name func (d *dao) SearchMemberByName(ctx context.Context, projectID int64, entityName string) ([]*models.Member, error) {
func SearchMemberByName(projectID int64, entityName string) ([]*models.Member, error) { o, err := orm.FromContext(ctx)
o := dao.GetOrmer() if err != nil {
return nil, err
}
sql := `select pm.id, pm.project_id, sql := `select pm.id, pm.project_id,
u.username as entity_name, u.username as entity_name,
r.name as rolename, r.name as rolename,
@ -167,12 +213,11 @@ func SearchMemberByName(projectID int64, entityName string) ([]*models.Member, e
queryParam = append(queryParam, "%"+orm.Escape(entityName)+"%") queryParam = append(queryParam, "%"+orm.Escape(entityName)+"%")
members := []*models.Member{} members := []*models.Member{}
log.Debugf("Query sql: %v", sql) log.Debugf("Query sql: %v", sql)
_, err := o.Raw(sql, queryParam).QueryRows(&members) _, err = o.Raw(sql, queryParam).QueryRows(&members)
return members, err return members, err
} }
// ListRoles lists the roles of user for the specific project func (d *dao) ListRoles(ctx context.Context, user *models.User, projectID int64) ([]int, error) {
func ListRoles(user *models.User, projectID int64) ([]int, error) {
if user == nil { if user == nil {
return nil, nil return nil, nil
} }
@ -186,12 +231,16 @@ func ListRoles(user *models.User, projectID int64) ([]int, error) {
sql += fmt.Sprintf(`union sql += fmt.Sprintf(`union
select role select role
from project_member 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, user.GroupIDs)
params = append(params, projectID) params = append(params, projectID)
} }
roles := []int{} 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 { if err != nil {
return nil, err return nil, err
} }

View File

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

101
src/pkg/member/manager.go Normal file
View File

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

View File

@ -0,0 +1,33 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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
}

View File

@ -39,6 +39,7 @@ func New() http.Handler {
ScanAllAPI: newScanAllAPI(), ScanAllAPI: newScanAllAPI(),
SearchAPI: newSearchAPI(), SearchAPI: newSearchAPI(),
ProjectAPI: newProjectAPI(), ProjectAPI: newProjectAPI(),
MemberAPI: newMemberAPI(),
PreheatAPI: newPreheatAPI(), PreheatAPI: newPreheatAPI(),
IconAPI: newIconAPI(), IconAPI: newIconAPI(),
RobotAPI: newRobotAPI(), RobotAPI: newRobotAPI(),

View File

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

View File

@ -17,15 +17,16 @@ package handler
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/goharbor/harbor/src/controller/config"
"strconv" "strconv"
"strings" "strings"
"sync" "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/runtime/middleware"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/common" "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/rbac"
"github.com/goharbor/harbor/src/common/security" "github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/security/local" "github.com/goharbor/harbor/src/common/security/local"
@ -63,6 +64,7 @@ func newProjectAPI() *projectAPI {
userMgr: user.Mgr, userMgr: user.Mgr,
repositoryCtl: repository.Ctl, repositoryCtl: repository.Ctl,
projectCtl: project.Ctl, projectCtl: project.Ctl,
memberMgr: member.Mgr,
quotaCtl: quota.Ctl, quotaCtl: quota.Ctl,
robotMgr: robot.Mgr, robotMgr: robot.Mgr,
preheatCtl: preheat.Ctl, preheatCtl: preheat.Ctl,
@ -78,6 +80,7 @@ type projectAPI struct {
userMgr user.Manager userMgr user.Manager
repositoryCtl repository.Controller repositoryCtl repository.Controller
projectCtl project.Controller projectCtl project.Controller
memberMgr member.Manager
quotaCtl quota.Controller quotaCtl quota.Controller
robotMgr robot.Manager robotMgr robot.Manager
preheatCtl preheat.Controller 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 { if hasPerm := a.HasProjectPermission(ctx, p.ProjectID, rbac.ActionList, rbac.ResourceMember); hasPerm {
fetchSummaries = append(fetchSummaries, getProjectMemberSummary) fetchSummaries = append(fetchSummaries, a.getProjectMemberSummary)
} }
if p.IsProxy() { 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 var wg sync.WaitGroup
for _, e := range []struct { for _, e := range []struct {
@ -712,14 +715,13 @@ func getProjectMemberSummary(ctx context.Context, p *project.Project, summary *m
wg.Add(1) wg.Add(1)
go func(role int, count *int64) { go func(role int, count *int64) {
defer wg.Done() defer wg.Done()
total, err := a.memberMgr.GetTotalOfProjectMembers(orm.Clone(ctx), p.ProjectID, nil, role)
total, err := pro.GetTotalOfProjectMembers(p.ProjectID, role)
if err != nil { if err != nil {
log.Warningf("failed to get total of project members of role %d", role) log.Warningf("failed to get total of project members of role %d", role)
return return
} }
*count = total *count = int64(total)
}(e.role, e.count) }(e.role, e.count)
} }

View File

@ -23,7 +23,6 @@ import (
// RegisterRoutes for Harbor legacy APIs // RegisterRoutes for Harbor legacy APIs
func registerLegacyRoutes() { func registerLegacyRoutes() {
version := APIVersion 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+"/email/ping", &api.EmailAPI{}, "post:Ping")
beego.Router("/api/"+version+"/health", &api.HealthAPI{}, "get:CheckHealth") beego.Router("/api/"+version+"/health", &api.HealthAPI{}, "get:CheckHealth")
beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get") beego.Router("/api/"+version+"/projects/:id([0-9]+)/metadatas/?:name", &api.MetadataAPI{}, "get:Get")

View File

@ -30,8 +30,8 @@ def get_endpoint():
def _create_client(server, credential, debug, api_type="products"): def _create_client(server, credential, debug, api_type="products"):
cfg = None cfg = None
if api_type in ('projectv2', 'artifact', 'repository', 'scanner', 'scan', 'scanall', 'preheat', 'quota', if api_type in ('projectv2', 'artifact', 'repository', 'scanner', 'scan', 'scanall', 'preheat', 'quota',
'replication', 'registry', 'robot', 'gc', 'retention', "immutable", "system_cve_allowlist", 'replication', 'registry', 'robot', 'gc', 'retention', 'immutable', 'system_cve_allowlist',
"configure", "users"): 'configure', 'users', 'member'):
cfg = v2_swagger_client.Configuration() cfg = v2_swagger_client.Configuration()
else: else:
cfg = swagger_client.Configuration() 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)), "system_cve_allowlist": v2_swagger_client.SystemCVEAllowlistApi(v2_swagger_client.ApiClient(cfg)),
"configure": v2_swagger_client.ConfigureApi(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)), "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') }.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 {}."): 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 {}."):

View File

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import base
import swagger_client import swagger_client
import v2_swagger_client import v2_swagger_client
from v2_swagger_client.rest import ApiException
from library.base import _assert_status_code 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): def is_member_exist_in_project(members, member_user_name, expected_member_role_id = None):
result = False result = False
@ -125,15 +127,15 @@ class Project(base.Base):
return count return count
def get_project_members(self, project_id, **kwargs): def get_project_members(self, project_id, **kwargs):
kwargs['api_type'] = 'products' kwargs['api_type'] = 'member'
return self._get_client(**kwargs).projects_project_id_members_get(project_id) 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): def get_project_member(self, project_id, member_id, expect_status_code = 200, expect_response_body = None, **kwargs):
from swagger_client.rest import ApiException from swagger_client.rest import ApiException
kwargs['api_type'] = 'products' kwargs['api_type'] = 'member'
data = [] data = []
try: 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: except ApiException as e:
base._assert_status_code(expect_status_code, e.status) base._assert_status_code(expect_status_code, e.status)
if expect_response_body is not None: if expect_response_body is not None:
@ -145,7 +147,7 @@ class Project(base.Base):
return data return data
def get_project_member_id(self, project_id, member_user_name, **kwargs): 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) members = self.get_project_members(project_id, **kwargs)
result = get_member_id_by_name(list(members), member_user_name) result = get_member_id_by_name(list(members), member_user_name)
if result == None: if result == None:
@ -154,36 +156,36 @@ class Project(base.Base):
return result return result
def check_project_member_not_exist(self, project_id, member_user_name, **kwargs): 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) members = self.get_project_members(project_id, **kwargs)
result = is_member_exist_in_project(list(members), member_user_name) result = is_member_exist_in_project(list(members), member_user_name)
if result == True: if result == True:
raise Exception(r"User {} should not be a member of project with ID {}.".format(member_user_name, project_id)) 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): 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) 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) result = is_member_exist_in_project(members, member_user_name, expected_member_role_id = expected_member_role_id)
if result == False: if result == False:
raise Exception(r"User {} should be a member of project with ID {}.".format(member_user_name, project_id)) 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): 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) 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(expect_status_code, status_code)
base._assert_status_code(200, status_code) base._assert_status_code(200, status_code)
return data return data
def delete_project_member(self, project_id, member_id, expect_status_code = 200, **kwargs): def delete_project_member(self, project_id, member_id, expect_status_code = 200, **kwargs):
kwargs['api_type'] = 'products' kwargs['api_type'] = 'member'
_, status_code, _ = self._get_client(**kwargs).projects_project_id_members_mid_delete_with_http_info(project_id, member_id) _, 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(expect_status_code, status_code)
base._assert_status_code(200, 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): 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' kwargs['api_type'] = 'member'
projectMember = swagger_client.ProjectMember() projectMember = v2_swagger_client.ProjectMember()
if user_id is not None: if user_id is not None:
projectMember.member_user = {"user_id": int(user_id)} projectMember.member_user = {"user_id": int(user_id)}
if member_role_id is None: if member_role_id is None:
@ -191,12 +193,12 @@ class Project(base.Base):
else: else:
projectMember.role_id = member_role_id projectMember.role_id = member_role_id
if _ldap_group_dn is not None: 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 = [] data = []
try: try:
data, status_code, header = self._get_client(**kwargs).projects_project_id_members_post_with_http_info(project_id, project_member = projectMember) data, status_code, header = self._get_client(**kwargs).create_project_member_with_http_info(project_id, project_member = projectMember)
except swagger_client.rest.ApiException as e: except ApiException as e:
base._assert_status_code(expect_status_code, e.status) base._assert_status_code(expect_status_code, e.status)
else: else:
base._assert_status_code(expect_status_code, status_code) base._assert_status_code(expect_status_code, status_code)