From b8a48d03262e59dbdcad7ee51432d093e61d2c33 Mon Sep 17 00:00:00 2001 From: stonezdj Date: Thu, 19 Apr 2018 13:52:18 +0800 Subject: [PATCH] Update security context for assign role to project group member The project list will contain all public projects, user is a member of this project, or user is in the group which is a member of this projects. Change the behaviour of user roles, if the user is not a member of this project, then return the user's groups role of current project --- src/common/dao/dao_test.go | 2 +- src/common/dao/group/usergroup.go | 20 +++ src/common/dao/group/usergroup_test.go | 176 +++++++++++++++++++ src/common/dao/project.go | 98 +++++++++-- src/common/dao/project/projectmember.go | 33 +++- src/common/dao/project/projectmember_test.go | 47 +++++ src/common/dao/project_test.go | 122 ++++++++++++- src/common/dao/testutils.go | 14 ++ src/common/models/project.go | 5 +- src/common/models/user.go | 11 +- src/common/security/admiral/context_test.go | 2 - src/common/security/context.go | 2 + src/common/security/local/context.go | 58 +++--- src/common/security/local/context_test.go | 112 ++++++++++++ src/ui/api/project.go | 11 +- src/ui/auth/ldap/ldap.go | 19 +- src/ui/auth/ldap/ldap_test.go | 2 +- src/ui/promgr/pmsdriver/local/local.go | 25 ++- tests/testprepare.sh | 6 +- 19 files changed, 688 insertions(+), 77 deletions(-) diff --git a/src/common/dao/dao_test.go b/src/common/dao/dao_test.go index 1bec71541..504cbd3cb 100644 --- a/src/common/dao/dao_test.go +++ b/src/common/dao/dao_test.go @@ -662,7 +662,7 @@ func TestGetTotalOfProjects(t *testing.T) { func TestGetProjects(t *testing.T) { projects, err := GetProjects(nil) if err != nil { - t.Errorf("Error occurred in GetAllProjects: %v", err) + t.Errorf("Error occurred in GetProjects: %v", err) } if len(projects) != 2 { t.Errorf("Expected length of projects is 2, but actual: %d, the projects: %+v", len(projects), projects) diff --git a/src/common/dao/group/usergroup.go b/src/common/dao/group/usergroup.go index f1349f86f..f39f7d7d9 100644 --- a/src/common/dao/group/usergroup.go +++ b/src/common/dao/group/usergroup.go @@ -15,8 +15,11 @@ package group import ( + "strings" "time" + "github.com/vmware/harbor/src/common" + "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" @@ -133,3 +136,20 @@ func OnBoardUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttri return nil } + +// GetGroupDNQueryCondition get the part of IN ('XXX', 'XXX') condition +func GetGroupDNQueryCondition(userGroupList []*models.UserGroup) string { + result := make([]string, 0) + count := 0 + for _, userGroup := range userGroupList { + if userGroup.GroupType == common.LdapGroupType { + result = append(result, "'"+userGroup.LdapGroupDN+"'") + count++ + } + } + //No LDAP Group found + if count == 0 { + return "" + } + return strings.Join(result, ",") +} diff --git a/src/common/dao/group/usergroup_test.go b/src/common/dao/group/usergroup_test.go index c48fad3e1..62f9db262 100644 --- a/src/common/dao/group/usergroup_test.go +++ b/src/common/dao/group/usergroup_test.go @@ -21,6 +21,7 @@ import ( "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/dao/project" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" ) @@ -247,3 +248,178 @@ func TestOnBoardUserGroup(t *testing.T) { }) } } + +func TestGetGroupDNQueryCondition(t *testing.T) { + userGroupList := []*models.UserGroup{ + &models.UserGroup{ + GroupName: "sample1", + GroupType: 1, + LdapGroupDN: "cn=sample1_users,ou=groups,dc=example,dc=com", + }, + &models.UserGroup{ + GroupName: "sample2", + GroupType: 1, + LdapGroupDN: "cn=sample2_users,ou=groups,dc=example,dc=com", + }, + &models.UserGroup{ + GroupName: "sample3", + GroupType: 0, + LdapGroupDN: "cn=sample3_users,ou=groups,dc=example,dc=com", + }, + } + + groupQueryConditions := GetGroupDNQueryCondition(userGroupList) + expectedConditions := `'cn=sample1_users,ou=groups,dc=example,dc=com','cn=sample2_users,ou=groups,dc=example,dc=com'` + if groupQueryConditions != expectedConditions { + t.Errorf("Failed to GetGroupDNQueryCondition, expected %v, actual %v", expectedConditions, groupQueryConditions) + } + var userGroupList2 []*models.UserGroup + groupQueryCondition2 := GetGroupDNQueryCondition(userGroupList2) + if len(groupQueryCondition2) > 0 { + t.Errorf("Failed to GetGroupDNQueryCondition, expected %v, actual %v", "", groupQueryCondition2) + } + groupQueryCondition3 := GetGroupDNQueryCondition(nil) + if len(groupQueryCondition3) > 0 { + t.Errorf("Failed to GetGroupDNQueryCondition, expected %v, actual %v", "", groupQueryCondition3) + } +} +func TestGetGroupProjects(t *testing.T) { + userID, err := dao.Register(models.User{ + Username: "grouptestu09", + Email: "grouptest09@example.com", + Password: "Harbor123456", + }) + defer dao.DeleteUser(int(userID)) + projectID1, err := dao.AddProject(models.Project{ + Name: "grouptest01", + OwnerID: 1, + }) + if err != nil { + t.Errorf("Error occurred when AddProject: %v", err) + } + defer dao.DeleteProject(projectID1) + projectID2, err := dao.AddProject(models.Project{ + Name: "grouptest02", + OwnerID: 1, + }) + if err != nil { + t.Errorf("Error occurred when AddProject: %v", err) + } + defer dao.DeleteProject(projectID2) + groupID, err := AddUserGroup(models.UserGroup{ + GroupName: "test_group_01", + GroupType: 1, + LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com", + }) + if err != nil { + t.Errorf("Error occurred when AddUserGroup: %v", err) + } + defer DeleteUserGroup(groupID) + pmid, err := project.AddProjectMember(models.Member{ + ProjectID: projectID1, + EntityID: groupID, + EntityType: "g", + }) + defer project.DeleteProjectMemberByID(pmid) + type args struct { + groupDNCondition string + query *models.ProjectQueryParam + } + member := &models.MemberQuery{ + Name: "grouptestu09", + } + tests := []struct { + name string + args args + wantSize int + wantErr bool + }{ + {"Query with group DN", + args{"'cn=harbor_users,ou=groups,dc=example,dc=com'", + &models.ProjectQueryParam{ + Member: member, + }}, + 1, false}, + {"Query without group DN", + args{"", + &models.ProjectQueryParam{}}, + 1, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := dao.GetGroupProjects(tt.args.groupDNCondition, tt.args.query) + if (err != nil) != tt.wantErr { + t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(got) < tt.wantSize { + t.Errorf("GetGroupProjects() size: %v, want %v", len(got), tt.wantSize) + } + }) + } +} + +func TestGetTotalGroupProjects(t *testing.T) { + projectID1, err := dao.AddProject(models.Project{ + Name: "grouptest01", + OwnerID: 1, + }) + if err != nil { + t.Errorf("Error occurred when AddProject: %v", err) + } + defer dao.DeleteProject(projectID1) + projectID2, err := dao.AddProject(models.Project{ + Name: "grouptest02", + OwnerID: 1, + }) + if err != nil { + t.Errorf("Error occurred when AddProject: %v", err) + } + defer dao.DeleteProject(projectID2) + groupID, err := AddUserGroup(models.UserGroup{ + GroupName: "test_group_01", + GroupType: 1, + LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com", + }) + if err != nil { + t.Errorf("Error occurred when AddUserGroup: %v", err) + } + defer DeleteUserGroup(groupID) + pmid, err := project.AddProjectMember(models.Member{ + ProjectID: projectID1, + EntityID: groupID, + EntityType: "g", + }) + defer project.DeleteProjectMemberByID(pmid) + type args struct { + groupDNCondition string + query *models.ProjectQueryParam + } + tests := []struct { + name string + args args + wantSize int + wantErr bool + }{ + {"Query with group DN", + args{"'cn=harbor_users,ou=groups,dc=example,dc=com'", + &models.ProjectQueryParam{}}, + 1, false}, + {"Query without group DN", + args{"", + &models.ProjectQueryParam{}}, + 1, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := dao.GetTotalGroupProjects(tt.args.groupDNCondition, tt.args.query) + if (err != nil) != tt.wantErr { + t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got < tt.wantSize { + t.Errorf("GetGroupProjects() size: %v, want %v", got, tt.wantSize) + } + }) + } +} diff --git a/src/common/dao/project.go b/src/common/dao/project.go index 0737a7727..5e3165ff2 100644 --- a/src/common/dao/project.go +++ b/src/common/dao/project.go @@ -135,42 +135,85 @@ func GetTotalOfProjects(query *models.ProjectQueryParam) (int64, error) { // GetProjects returns a project list according to the query conditions func GetProjects(query *models.ProjectQueryParam) ([]*models.Project, error) { - sql, params := projectQueryConditions(query) - if query == nil { - sql += ` order by p.name` - } + sqlStr, queryParam := projectQueryConditions(query) + sqlStr = `select distinct p.project_id, p.name, p.owner_id, + p.creation_time, p.update_time ` + sqlStr + ` order by p.name` + sqlStr, queryParam = CreatePagination(query, sqlStr, queryParam) + log.Debugf("sql:=%+v, param= %+v", sqlStr, queryParam) + var projects []*models.Project + _, err := GetOrmer().Raw(sqlStr, queryParam).QueryRows(&projects) + + return projects, err + +} + +// GetGroupProjects - Get user's all projects, including user is the user member of this project +// and the user is in the group which is a group member of this project. +func GetGroupProjects(groupDNCondition string, query *models.ProjectQueryParam) ([]*models.Project, error) { + sql, params := projectQueryConditions(query) sql = `select distinct p.project_id, p.name, p.owner_id, p.creation_time, p.update_time ` + sql + if len(groupDNCondition) > 0 { + sql = fmt.Sprintf( + `%s union select distinct p.project_id, p.name, p.owner_id, p.creation_time, p.update_time + from project p + left join project_member pm on p.project_id = pm.project_id + left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g' and ug.group_type = 1 + where ug.ldap_group_dn in ( %s ) order by name`, + sql, groupDNCondition) + } + sqlStr, queryParams := CreatePagination(query, sql, params) + log.Debugf("query sql:%v", sql) var projects []*models.Project - _, err := GetOrmer().Raw(sql, params).QueryRows(&projects) + _, err := GetOrmer().Raw(sqlStr, queryParams).QueryRows(&projects) return projects, err } +// GetTotalGroupProjects - Get the total count of projects, including user is the member of this project and the +// user is in the group, which is the group member of this project. +func GetTotalGroupProjects(groupDNCondition string, query *models.ProjectQueryParam) (int, error) { + var sql string + sqlCondition, params := projectQueryConditions(query) + if len(groupDNCondition) == 0 { + sql = `select count(1) ` + sqlCondition + } else { + sql = fmt.Sprintf( + `select count(1) + from ( select p.project_id %s union select p.project_id + from project p + left join project_member pm on p.project_id = pm.project_id + left join user_group ug on ug.id = pm.entity_id and pm.entity_type = 'g' and ug.group_type = 1 + where ug.ldap_group_dn in ( %s )) t`, + sqlCondition, groupDNCondition) + } + log.Debugf("query sql:%v", sql) + var count int + if err := GetOrmer().Raw(sql, params).QueryRow(&count); err != nil { + return 0, err + } + return count, nil +} + func projectQueryConditions(query *models.ProjectQueryParam) (string, []interface{}) { params := []interface{}{} - sql := ` from project as p` - if query == nil { sql += ` where p.deleted=false` return sql, params } - // if query.ProjectIDs is not nil but has no element, the query will returns no rows if query.ProjectIDs != nil && len(query.ProjectIDs) == 0 { sql += ` where 1 = 0` return sql, params } - if len(query.Owner) != 0 { sql += ` join harbor_user u1 on p.owner_id = u1.user_id` } - if query.Member != nil && len(query.Member.Name) != 0 { sql += ` join project_member pm - on p.project_id = pm.project_id + on p.project_id = pm.project_id and pm.entity_type = 'u' join harbor_user u2 on pm.entity_id=u2.user_id` } @@ -205,15 +248,18 @@ func projectQueryConditions(query *models.ProjectQueryParam) (string, []interfac params = append(params, roleID) } } - if len(query.ProjectIDs) > 0 { sql += fmt.Sprintf(` and p.project_id in ( %s )`, paramPlaceholder(len(query.ProjectIDs))) params = append(params, query.ProjectIDs) } + return sql, params +} - if query.Pagination != nil && query.Pagination.Size > 0 { - sql += ` order by p.name limit ?` +// CreatePagination ... +func CreatePagination(query *models.ProjectQueryParam, sql string, params []interface{}) (string, []interface{}) { + if query != nil && query.Pagination != nil && query.Pagination.Size > 0 { + sql += ` limit ?` params = append(params, query.Pagination.Size) if query.Pagination.Page > 0 { @@ -221,7 +267,6 @@ func projectQueryConditions(query *models.ProjectQueryParam) (string, []interfac params = append(params, (query.Pagination.Page-1)*query.Pagination.Size) } } - return sql, params } @@ -231,12 +276,31 @@ func DeleteProject(id int64) error { if err != nil { return err } - name := fmt.Sprintf("%s#%d", project.Name, project.ProjectID) - sql := `update project set deleted = true, name = ? where project_id = ?` _, err = GetOrmer().Raw(sql, name, id).Exec() return err } + +// GetRolesByLDAPGroup - Get Project roles of the +// specified group DN is a member of current project +func GetRolesByLDAPGroup(projectID int64, groupDNCondition string) ([]int, error) { + var roles []int + if len(groupDNCondition) == 0 { + return roles, nil + } + o := GetOrmer() + sql := fmt.Sprintf( + `select pm.role from project_member pm + left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id + where ug.ldap_group_dn in ( %s ) and pm.project_id = ? `, + groupDNCondition) + log.Debugf("sql:%v", sql) + if _, err := o.Raw(sql, projectID).QueryRows(&roles); err != nil { + log.Warningf("Error in GetRolesByLDAPGroup, error: %v", err) + return nil, err + } + return roles, nil +} diff --git a/src/common/dao/project/projectmember.go b/src/common/dao/project/projectmember.go index 7a12d9616..263cbe737 100644 --- a/src/common/dao/project/projectmember.go +++ b/src/common/dao/project/projectmember.go @@ -30,14 +30,14 @@ func GetProjectMember(queryMember models.Member) ([]*models.Member, error) { } 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 - 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' union - (select pm.id as id, pm.project_id as project_id, u.user_id as entity_id, u.username as entity_name, u.creation_time, u.update_time, r.name as rolename, + select pm.id as id, pm.project_id as project_id, u.user_id as entity_id, u.username as entity_name, u.creation_time, u.update_time, r.name as rolename, r.role_id as role, pm.entity_type as entity_type from harbor_user u join project_member pm on pm.project_id = ? and u.user_id = pm.entity_id - join role r on pm.role = r.role_id where u.deleted = false and pm.entity_type = 'u')) as a where a.project_id = ? ` + join role r on pm.role = r.role_id where u.deleted = false and pm.entity_type = 'u') as a where a.project_id = ? ` queryParam := make([]interface{}, 1) // used ProjectID already @@ -63,7 +63,7 @@ func GetProjectMember(queryMember models.Member) ([]*models.Member, error) { sql += " and a.id = ? " queryParam = append(queryParam, queryMember.ID) } - sql += ` order by a.entity_name ` + sql += ` order by entity_name ` members := []*models.Member{} _, err := o.Raw(sql, queryParam).QueryRows(&members) @@ -120,29 +120,44 @@ func DeleteProjectMemberByID(pmid int) error { // SearchMemberByName search members of the project by entity_name func SearchMemberByName(projectID int64, entityName string) ([]*models.Member, error) { o := dao.GetOrmer() - sql := `(select pm.id, pm.project_id, + sql := `select pm.id, pm.project_id, u.username as entity_name, r.name as rolename, pm.role, pm.entity_id, pm.entity_type from project_member pm left join harbor_user u on pm.entity_id = u.user_id and pm.entity_type = 'u' left join role r on pm.role = r.role_id - where u.deleted = false and pm.project_id = ? and u.username like ? order by entity_name ) + where u.deleted = false and pm.project_id = ? and u.username like ? union - (select pm.id, pm.project_id, + select pm.id, pm.project_id, ug.group_name as entity_name, r.name as rolename, pm.role, pm.entity_id, pm.entity_type from project_member pm left join user_group ug on pm.entity_id = ug.id and pm.entity_type = 'g' left join role r on pm.role = r.role_id - where pm.project_id = ? and ug.group_name like ? order by entity_name ) ` + where pm.project_id = ? and ug.group_name like ? + order by entity_name ` queryParam := make([]interface{}, 4) queryParam = append(queryParam, projectID) queryParam = append(queryParam, "%"+dao.Escape(entityName)+"%") queryParam = append(queryParam, projectID) queryParam = append(queryParam, "%"+dao.Escape(entityName)+"%") members := []*models.Member{} + log.Debugf("Query sql: %v", sql) _, err := o.Raw(sql, queryParam).QueryRows(&members) return members, err } + +// GetRolesByGroup -- Query group roles +func GetRolesByGroup(projectID int64, groupDNCondition string) []int { + var roles []int + o := dao.GetOrmer() + sql := `select role from project_member pm + left join user_group ug on pm.project_id = ? + where ug.group_type = 1 and ug.ldap_group_dn in (` + groupDNCondition + `)` + if _, err := o.Raw(sql, projectID).QueryRows(&roles); err != nil { + return roles + } + return roles +} diff --git a/src/common/dao/project/projectmember_test.go b/src/common/dao/project/projectmember_test.go index 68b6fefae..4abe20d7d 100644 --- a/src/common/dao/project/projectmember_test.go +++ b/src/common/dao/project/projectmember_test.go @@ -285,3 +285,50 @@ func TestGetProjectMember(t *testing.T) { } } +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) +} +func TestGetRolesByGroup(t *testing.T) { + PrepareGroupTest() + + project, err := dao.GetProjectByName("group_project") + if err != nil { + t.Errorf("Error occurred when GetProjectByName : %v", err) + } + type args struct { + projectID int64 + groupDNCondition string + } + tests := []struct { + name string + args args + want []int + }{ + {"Query group with role", args{project.ProjectID, "'cn=harbor_user,dc=example,dc=com'"}, []int{2}}, + {"Query group no role", args{project.ProjectID, "'cn=another_user,dc=example,dc=com'"}, []int{}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetRolesByGroup(tt.args.projectID, tt.args.groupDNCondition); !dao.ArrayEqual(got, tt.want) { + t.Errorf("GetRolesByGroup() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/src/common/dao/project_test.go b/src/common/dao/project_test.go index 2d1b9d8b0..9e88b8928 100644 --- a/src/common/dao/project_test.go +++ b/src/common/dao/project_test.go @@ -103,9 +103,9 @@ func Test_projectQueryConditions(t *testing.T) { args{query: &models.ProjectQueryParam{ProjectIDs: []int64{2, 3}, Owner: "admin", Name: "sample", Member: &models.MemberQuery{Name: "name", Role: 1}, Pagination: &models.Pagination{Page: 1, Size: 20}}}, ` from project as p join harbor_user u1 on p.owner_id = u1.user_id join project_member pm - on p.project_id = pm.project_id + on p.project_id = pm.project_id and pm.entity_type = 'u' join harbor_user u2 - on pm.entity_id=u2.user_id where p.deleted=false and u1.username=? and p.name like ? and u2.username=? and pm.role = ? and p.project_id in ( ?,? ) order by p.name limit ? offset ?`, + on pm.entity_id=u2.user_id where p.deleted=false and u1.username=? and p.name like ? and u2.username=? and pm.role = ? and p.project_id in ( ?,? )`, []interface{}{1, []int64{2, 3}, 20, 0}}, } for _, tt := range tests { @@ -117,3 +117,121 @@ func Test_projectQueryConditions(t *testing.T) { }) } } + +func TestGetGroupProjects(t *testing.T) { + prepareGroupTest() + query := &models.ProjectQueryParam{Member: &models.MemberQuery{Name: "sample_group"}} + type args struct { + groupDNCondition string + query *models.ProjectQueryParam + } + tests := []struct { + name string + args args + wantSize int + wantErr bool + }{ + {"Verify correct sql", args{groupDNCondition: "'cn=harbor_user,dc=example,dc=com'", query: query}, 1, false}, + {"Verify missed sql", args{groupDNCondition: "'cn=another_user,dc=example,dc=com'", query: query}, 0, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetGroupProjects(tt.args.groupDNCondition, tt.args.query) + if (err != nil) != tt.wantErr { + t.Errorf("GetGroupProjects() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(got) != tt.wantSize { + t.Errorf("GetGroupProjects() = %v, want %v", got, tt.wantSize) + } + }) + } +} + +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'`, + } + PrepareTestData(clearSqls, initSqls) +} + +func TestGetTotalGroupProjects(t *testing.T) { + prepareGroupTest() + query := &models.ProjectQueryParam{Member: &models.MemberQuery{Name: "sample_group"}} + type args struct { + groupDNCondition string + query *models.ProjectQueryParam + } + tests := []struct { + name string + args args + want int + wantErr bool + }{ + {"Verify correct sql", args{groupDNCondition: "'cn=harbor_user,dc=example,dc=com'", query: query}, 1, false}, + {"Verify missed sql", args{groupDNCondition: "'cn=another_user,dc=example,dc=com'", query: query}, 0, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetTotalGroupProjects(tt.args.groupDNCondition, tt.args.query) + if (err != nil) != tt.wantErr { + t.Errorf("GetTotalGroupProjects() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetTotalGroupProjects() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetRolesByLDAPGroup(t *testing.T) { + prepareGroupTest() + project, err := GetProjectByName("group_project") + if err != nil { + t.Errorf("Error occurred when Get project by name: %v", err) + } + privateProject, err := GetProjectByName("group_project_private") + if err != nil { + t.Errorf("Error occurred when Get project by name: %v", err) + } + type args struct { + projectID int64 + groupDNCondition string + } + tests := []struct { + name string + args args + wantSize int + wantErr bool + }{ + {"Check normal", args{project.ProjectID, "'cn=harbor_user,dc=example,dc=com'"}, 1, false}, + {"Check non exist", args{privateProject.ProjectID, "'cn=not_harbor_user,dc=example,dc=com'"}, 0, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetRolesByLDAPGroup(tt.args.projectID, tt.args.groupDNCondition) + if (err != nil) != tt.wantErr { + t.Errorf("TestGetRolesByLDAPGroup() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(got) != tt.wantSize { + t.Errorf("TestGetRolesByLDAPGroup() = %v, want %v", len(got), tt.wantSize) + } + }) + } +} diff --git a/src/common/dao/testutils.go b/src/common/dao/testutils.go index 3f27e80f2..b067498a2 100644 --- a/src/common/dao/testutils.go +++ b/src/common/dao/testutils.go @@ -116,3 +116,17 @@ func PrepareTestData(clearSqls []string, initSqls []string) { } } } + +// ArrayEqual ... +func ArrayEqual(arrayA, arrayB []int) bool { + if len(arrayA) != len(arrayB) { + return false + } + size := len(arrayA) + for i := 0; i < size; i++ { + if arrayA[i] != arrayB[i] { + return false + } + } + return true +} diff --git a/src/common/models/project.go b/src/common/models/project.go index ef701891c..f184f5726 100644 --- a/src/common/models/project.go +++ b/src/common/models/project.go @@ -147,8 +147,9 @@ type ProjectQueryParam struct { // MemberQuery fitler by member's username and role type MemberQuery struct { - Name string // the username of member - Role int // the role of the member has to the project + Name string // the username of member + Role int // the role of the member has to the project + GroupList []*UserGroup // the group list of current user } // Pagination ... diff --git a/src/common/models/user.go b/src/common/models/user.go index 57c010f6e..2a3095cb1 100644 --- a/src/common/models/user.go +++ b/src/common/models/user.go @@ -35,11 +35,12 @@ type User struct { //to it. Role int `orm:"-" json:"role_id"` // RoleList []Role `json:"role_list"` - HasAdminRole bool `orm:"column(sysadmin_flag)" json:"has_admin_role"` - ResetUUID string `orm:"column(reset_uuid)" json:"reset_uuid"` - Salt string `orm:"column(salt)" json:"-"` - CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"` - UpdateTime time.Time `orm:"column(update_time)" json:"update_time"` + HasAdminRole bool `orm:"column(sysadmin_flag)" json:"has_admin_role"` + ResetUUID string `orm:"column(reset_uuid)" json:"reset_uuid"` + Salt string `orm:"column(salt)" json:"-"` + CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"` + UpdateTime time.Time `orm:"column(update_time)" json:"update_time"` + GroupList []*UserGroup `orm:"-" json:"-"` } // UserQuery ... diff --git a/src/common/security/admiral/context_test.go b/src/common/security/admiral/context_test.go index 842cac2db..dab5dccb0 100644 --- a/src/common/security/admiral/context_test.go +++ b/src/common/security/admiral/context_test.go @@ -13,5 +13,3 @@ // limitations under the License. package admiral - -// TODO add test cases diff --git a/src/common/security/context.go b/src/common/security/context.go index e349521b3..9969721eb 100644 --- a/src/common/security/context.go +++ b/src/common/security/context.go @@ -34,6 +34,8 @@ type Context interface { HasWritePerm(projectIDOrName interface{}) bool // HasAllPerm returns whether the user has all permissions to the project HasAllPerm(projectIDOrName interface{}) bool + //Get current user's all project GetMyProjects() ([]*models.Project, error) + //Get user's role in provided project GetProjectRoles(projectIDOrName interface{}) []int } diff --git a/src/common/security/local/context.go b/src/common/security/local/context.go index 31c7e342d..7c2f713ab 100644 --- a/src/common/security/local/context.go +++ b/src/common/security/local/context.go @@ -17,6 +17,7 @@ package local import ( "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/dao/group" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/ui/promgr" @@ -88,7 +89,6 @@ func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool { } roles := s.GetProjectRoles(projectIDOrName) - return len(roles) > 0 } @@ -97,12 +97,10 @@ func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { if !s.IsAuthenticated() { return false } - // system admin if s.IsSysAdmin() { return true } - roles := s.GetProjectRoles(projectIDOrName) for _, role := range roles { switch role { @@ -111,7 +109,6 @@ func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool { return true } } - return false } @@ -120,12 +117,10 @@ func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { if !s.IsAuthenticated() { return false } - // system admin if s.IsSysAdmin() { return true } - roles := s.GetProjectRoles(projectIDOrName) for _, role := range roles { switch role { @@ -133,19 +128,9 @@ func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool { return true } } - return false } -// GetMyProjects ... -func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) { - return dao.GetProjects(&models.ProjectQueryParam{ - Member: &models.MemberQuery{ - Name: s.GetUsername(), - }, - }) -} - // GetProjectRoles ... func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int { if !s.IsAuthenticated() || projectIDOrName == nil { @@ -164,7 +149,6 @@ func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int { log.Debugf("user %s not found", s.GetUsername()) return roles } - project, err := s.pm.Get(projectIDOrName) if err != nil { log.Errorf("failed to get project %v: %v", projectIDOrName, err) @@ -174,13 +158,11 @@ func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int { log.Errorf("project %v not found", projectIDOrName) return roles } - roleList, err := dao.GetUserProjectRoles(user.UserID, project.ProjectID, common.UserMember) if err != nil { log.Errorf("failed to get roles of user %d to project %d: %v", user.UserID, project.ProjectID, err) return roles } - for _, role := range roleList { switch role.RoleCode { case "MDRWS": @@ -191,8 +173,42 @@ func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int { roles = append(roles, common.RoleGuest) } } + if len(roles) != 0 { + return roles + } + return s.GetRolesByGroup(projectIDOrName) +} - //If len(roles)==0, Get Group Roles - +// GetRolesByGroup - Get the group role of current user to the project +func (s *SecurityContext) GetRolesByGroup(projectIDOrName interface{}) []int { + var roles []int + user := s.user + project, err := s.pm.Get(projectIDOrName) + //No user, group or project info + if err != nil || project == nil || user == nil || len(user.GroupList) == 0 { + return roles + } + //Get role by LDAP group + groupDNConditions := group.GetGroupDNQueryCondition(user.GroupList) + roles, err = dao.GetRolesByLDAPGroup(project.ProjectID, groupDNConditions) + if err != nil { + return nil + } return roles } + +// GetMyProjects ... +func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) { + result, err := s.pm.List( + &models.ProjectQueryParam{ + Member: &models.MemberQuery{ + Name: s.GetUsername(), + GroupList: s.user.GroupList, + }, + }) + if err != nil { + return nil, err + } + + return result.Projects, nil +} diff --git a/src/common/security/local/context_test.go b/src/common/security/local/context_test.go index 9d3d0fd76..2683ee0e9 100644 --- a/src/common/security/local/context_test.go +++ b/src/common/security/local/context_test.go @@ -272,6 +272,25 @@ func TestHasAllPerm(t *testing.T) { assert.True(t, ctx.HasAllPerm(private.Name)) } +func TestHasAllPermWithGroup(t *testing.T) { + PrepareGroupTest() + project, err := dao.GetProjectByName("group_project") + if err != nil { + t.Errorf("Error occurred when GetProjectByName: %v", err) + } + developer, err := dao.GetUser(models.User{Username: "sample01"}) + if err != nil { + t.Errorf("Error occurred when GetUser: %v", err) + } + developer.GroupList = []*models.UserGroup{ + &models.UserGroup{GroupName: "test_group", GroupType: 1, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"}, + } + ctx := NewSecurityContext(developer, pm) + assert.False(t, ctx.HasAllPerm(project.Name)) + assert.True(t, ctx.HasWritePerm(project.Name)) + assert.True(t, ctx.HasReadPerm(project.Name)) +} + func TestGetMyProjects(t *testing.T) { ctx := NewSecurityContext(guestUser, pm) projects, err := ctx.GetMyProjects() @@ -309,3 +328,96 @@ func TestGetProjectRoles(t *testing.T) { assert.Equal(t, 1, len(roles)) assert.Equal(t, common.RoleProjectAdmin, roles[0]) } +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) +} + +func TestSecurityContext_GetRolesByGroup(t *testing.T) { + PrepareGroupTest() + project, err := dao.GetProjectByName("group_project") + if err != nil { + t.Errorf("Error occurred when GetProjectByName: %v", err) + } + developer, err := dao.GetUser(models.User{Username: "sample01"}) + if err != nil { + t.Errorf("Error occurred when GetUser: %v", err) + } + developer.GroupList = []*models.UserGroup{ + &models.UserGroup{GroupName: "test_group", GroupType: 1, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"}, + } + type fields struct { + user *models.User + pm promgr.ProjectManager + } + type args struct { + projectIDOrName interface{} + } + tests := []struct { + name string + fields fields + args args + want []int + }{ + {"Developer", fields{user: developer, pm: pm}, args{project.ProjectID}, []int{2}}, + {"Guest", fields{user: guestUser, pm: pm}, args{project.ProjectID}, []int{}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &SecurityContext{ + user: tt.fields.user, + pm: tt.fields.pm, + } + if got := s.GetRolesByGroup(tt.args.projectIDOrName); !dao.ArrayEqual(got, tt.want) { + t.Errorf("SecurityContext.GetRolesByGroup() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSecurityContext_GetMyProjects(t *testing.T) { + type fields struct { + user *models.User + pm promgr.ProjectManager + } + tests := []struct { + name string + fields fields + wantSize int + wantErr bool + }{ + {"Admin", fields{user: projectAdminUser, pm: pm}, 1, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &SecurityContext{ + user: tt.fields.user, + pm: tt.fields.pm, + } + got, err := s.GetMyProjects() + if (err != nil) != tt.wantErr { + t.Errorf("SecurityContext.GetMyProjects() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(got) != tt.wantSize { + t.Errorf("SecurityContext.GetMyProjects() = %v, want %v", len(got), tt.wantSize) + } + }) + } +} diff --git a/src/ui/api/project.go b/src/ui/api/project.go index a292ca9fe..e75a6e72d 100644 --- a/src/ui/api/project.go +++ b/src/ui/api/project.go @@ -347,19 +347,16 @@ func (p *ProjectAPI) List() { return } projects = append(projects, pros...) - - mps, err := p.ProjectMgr.List(&models.ProjectQueryParam{ - Member: &models.MemberQuery{ - Name: p.SecurityCtx.GetUsername(), - }, - }) + mps, err := p.SecurityCtx.GetMyProjects() if err != nil { p.HandleInternalServerError(fmt.Sprintf("failed to list projects: %v", err)) return } - projects = append(projects, mps.Projects...) + projects = append(projects, mps...) } } + //Query projects by user group + if projects != nil { projectIDs := []int64{} for _, project := range projects { diff --git a/src/ui/auth/ldap/ldap.go b/src/ui/auth/ldap/ldap.go index 6d114a66b..26a28fdcb 100644 --- a/src/ui/auth/ldap/ldap.go +++ b/src/ui/auth/ldap/ldap.go @@ -76,14 +76,31 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) { u.Username = ldapUsers[0].Username u.Email = strings.TrimSpace(ldapUsers[0].Email) u.Realname = ldapUsers[0].Realname + userGroups := make([]*models.UserGroup, 0) dn := ldapUsers[0].DN - if err = ldapSession.Bind(dn, m.Password); err != nil { log.Warningf("Failed to bind user, username: %s, dn: %s, error: %v", u.Username, dn, err) return nil, auth.NewErrAuth(err.Error()) } + //Attach user group + for _, groupDN := range ldapUsers[0].GroupDNList { + userGroupQuery := models.UserGroup{ + GroupType: 1, + LdapGroupDN: groupDN, + } + userGroupList, err := group.QueryUserGroup(userGroupQuery) + if err != nil { + continue + } + if len(userGroupList) == 0 { + continue + } + userGroups = append(userGroups, userGroupList[0]) + } + u.GroupList = userGroups + return &u, nil } diff --git a/src/ui/auth/ldap/ldap_test.go b/src/ui/auth/ldap/ldap_test.go index ed97ee94e..d09b9a2b3 100644 --- a/src/ui/auth/ldap/ldap_test.go +++ b/src/ui/auth/ldap/ldap_test.go @@ -109,7 +109,7 @@ func TestMain(m *testing.M) { //Extract to test utils initSqls := []string{ - "insert into user (username, email, password, realname) values ('member_test_01', 'member_test_01@example.com', '123456', 'member_test_01')", + "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, group_property) 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'", diff --git a/src/ui/promgr/pmsdriver/local/local.go b/src/ui/promgr/pmsdriver/local/local.go index 386c0579e..826b62b4b 100644 --- a/src/ui/promgr/pmsdriver/local/local.go +++ b/src/ui/promgr/pmsdriver/local/local.go @@ -20,6 +20,7 @@ import ( "time" "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/dao/group" "github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/utils" errutil "github.com/vmware/harbor/src/common/utils/error" @@ -117,7 +118,6 @@ func (d *driver) Delete(projectIDOrName interface{}) error { } id = project.ProjectID } - return dao.DeleteProject(id) } @@ -129,17 +129,26 @@ func (d *driver) Update(projectIDOrName interface{}, } // List returns a project list according to the query parameters -func (d *driver) List(query *models.ProjectQueryParam) ( - *models.ProjectQueryResult, error) { - total, err := dao.GetTotalOfProjects(query) +func (d *driver) List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error) { + var total int64 + var projects []*models.Project + var groupDNCondition string + + //List with LDAP group projects + if query != nil && query.Member != nil { + groupDNCondition = group.GetGroupDNQueryCondition(query.Member.GroupList) + } + + count, err := dao.GetTotalGroupProjects(groupDNCondition, query) + if err != nil { + return nil, err + } + total = int64(count) + projects, err = dao.GetGroupProjects(groupDNCondition, query) if err != nil { return nil, err } - projects, err := dao.GetProjects(query) - if err != nil { - return nil, err - } return &models.ProjectQueryResult{ Total: total, Projects: projects, diff --git a/tests/testprepare.sh b/tests/testprepare.sh index 334c33f4f..96841ca59 100755 --- a/tests/testprepare.sh +++ b/tests/testprepare.sh @@ -7,7 +7,11 @@ cp make/common/config/ui/private_key.pem /etc/ui/ mkdir src/ui/conf cp make/common/config/ui/app.conf src/ui/conf/ -IP=`ip addr s eth0 |grep "inet "|awk '{print $2}' |awk -F "/" '{print $1}'` +if [ "$(uname)" == "Darwin" ]; then + IP=`ifconfig en0 | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}'` +else + IP=`ip addr s eth0 |grep "inet "|awk '{print $2}' |awk -F "/" '{print $1}'` +fi echo "server ip is "$IP sed -i -r "s/POSTGRESQL_HOST=postgresql/POSTGRESQL_HOST=$IP/" make/common/config/adminserver/env sed -i -r "s|REGISTRY_URL=http://registry:5000|REGISTRY_URL=http://$IP:5000|" make/common/config/adminserver/env