diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e44d3223c4..b7e6e4a62c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -627,6 +627,167 @@ paths: description: Project ID does not exist. '500': description: Unexpected internal errors. + '/projects/{project_id}/projectmembers': + 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. + 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. + '400': + description: Illegal format of project member or 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 does not exist. + '500': + description: Unexpected internal errors. + '/projects/{project_id}/projectmembers/{mid}': + get: + summary: Get the project member information + description: Get the project member information + tags: + - Products + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + - name: mid + in: path + type: integer + format: int64 + required: true + description: The member ID + responses: + '200': + description: Project member retrieved successfully. + schema: + $ref: '#/definitions/ProjectMemberEntity' + '400': + description: Illegal format of project member or invalid project id, member id. + '401': + description: User need to log in first. + '403': + description: User in session does not have permission to the project. + '404': + description: Project or projet member does not exist. + '500': + description: Unexpected internal errors. + put: + summary: Update project member + description: Update project member relationship + tags: + - Products + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + - name: mid + in: path + type: integer + format: int64 + required: true + description: Member ID. + - name: role + in: body + schema: + $ref: '#/definitions/RoleRequest' + responses: + '200': + description: Project member updated successfully. + '400': + description: Invalid role id, it should be 1,2 or 3, or invalid project id, or invalid member id. + '401': + description: User need to log in first. + '403': + description: User in session does not have permission to the project. + '404': + description: project or project member does not exist. + '500': + description: Unexpected internal errors. + delete: + summary: Delete project member + tags: + - Products + parameters: + - name: project_id + in: path + type: integer + format: int64 + required: true + description: Relevant project ID. + - name: mid + in: path + type: integer + format: int64 + required: true + description: Member ID. + responses: + '200': + description: Project member deleted successfully. + '400': + description: The project id or project member id is invalid. + '401': + description: User need to log in first. + '403': + description: User in session does not have permission to the project. + '500': + description: Unexpected internal errors. /statistics: get: summary: Get projects number and repositories number relevant to the user @@ -2307,8 +2468,153 @@ paths: $ref: '#/responses/UnsupportedMediaType' '500': description: Unexpected internal errors. + /ldap/groups/search: + get: + summary: Search available ldap groups. + description: > + This endpoint searches the available ldap groups based on related + configuration parameters. Support searched by input ladp configuration, + load configuration from the system and specific filter. + parameters: + - name: groupname + in: query + type: string + required: false + description: Ldap group name + tags: + - Products + responses: + '200': + description: Search ldap group successfully. + schema: + type: array + items: + $ref: '#/definitions/UserGroup' + '500': + description: Unexpected internal errors. + /usergroups: + get: + summary: Get all user groups information + description: Get all user groups information + tags: + - Products + responses: + '200': + description: Get user group successfully. + schema: + type: array + items: + $ref: '#/definitions/UserGroup' + '401': + description: User need to log in first. + '403': + description: User in session does not have permission to the user group. + '500': + description: Unexpected internal errors. + post: + summary: Create user group + description: Create user group information + tags: + - Products + parameters: + - name: usergroup + in: body + schema: + $ref: '#/definitions/UserGroup' + responses: + '201': + description: User group created successfully. + '401': + description: User need to log in first. + '403': + description: User in session does not have permission to the user group. + '404': + description: The LDAP group is not found. + '409': + description: An LDAP user group with same DN already exist. + '500': + description: Unexpected internal errors. + /usergroups/{group_id}: + get: + summary: Get user group information + description: Get user group information + tags: + - Products + parameters: + - name: group_id + in: path + type: integer + format: int64 + required: true + description: Group ID + responses: + '200': + description: User group get successfully. + schema: + $ref: '#/definitions/UserGroup' + '400': + description: The user group id is invalid. + '401': + description: User need to log in first. + '403': + description: User in session does not have permission to the user group. + '404': + description: User group does not exist. + '500': + description: Unexpected internal errors. + put: + summary: Update group information + description: Update user group information + tags: + - Products + parameters: + - name: group_id + in: path + type: integer + format: int64 + required: true + description: Group ID + - name: usergroup + in: body + required: false + schema: + $ref: '#/definitions/UserGroup' + responses: + '200': + description: User group updated successfully. + '400': + description: The user group id is invalid. + '401': + description: User need to log in first. + '403': + description: Only admin has this authority. + '404': + description: User group does not exist. + '500': + description: Unexpected internal errors. + delete: + summary: Delete user group + description: Delete user group + tags: + - Products + parameters: + - name: group_id + type: integer + in: path + required: true + responses: + '200': + description: User group deleted successfully. + '400': + description: The user group id is invalid. + '401': + description: User need to log in first. + '403': + description: Only admin has this authority. + '500': + description: Unexpected internal errors. /ldap/users/search: - post: + get: summary: Search available ldap users. description: > This endpoint searches the available ldap users based on related @@ -2320,15 +2626,6 @@ paths: type: string required: false description: Registered user ID - - name: ldap_conf - in: body - description: >- - ldap search configuration. ldapconf field can input ldap service - configuration. If this item are blank, will load default - configuration will load current configuration from the system. - required: false - schema: - $ref: '#/definitions/LdapConf' tags: - Products responses: @@ -2338,14 +2635,10 @@ paths: type: array items: $ref: '#/definitions/LdapUsers' - '400': - description: Inviald ldap configuration parameters. '401': description: User need to login first. '403': description: Only admin has this authority. - '415': - $ref: '#/responses/UnsupportedMediaType' '500': description: Unexpected internal errors. /ldap/users/import: @@ -2379,7 +2672,7 @@ paths: description: Only admin has this authority. '415': $ref: '#/responses/UnsupportedMediaType' - '500': + '404': description: Failed import some users. schema: type: array @@ -3440,3 +3733,70 @@ definitions: update_time: type: string description: The update time of label. + 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 an user, it is user_id in user table. if the member is an 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 + 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 + UserEntity: + type: object + properties: + user_id: + type: integer + description: The ID of the user. + username: + type: string + description: The name of the user. + UserGroup: + type: object + properties: + id: + type: integer + description: The ID of the user group + group_name: + type: string + description: The name of the user group + group_type: + type: integer + description: The group type, 1 for LDAP group. + ldap_group_dn: + type: string + description: The DN of the LDAP group if group type is 1 (LDAP group). + + + diff --git a/src/common/const.go b/src/common/const.go index 1eba447db5..1d95cb9795 100644 --- a/src/common/const.go +++ b/src/common/const.go @@ -106,4 +106,5 @@ const ( DefaultJobserviceEndpoint = "http://jobservice:8080" DefaultUIEndpoint = "http://ui:8080" DefaultNotaryEndpoint = "http://notary-server:4443" + LdapGroupType = 1 ) diff --git a/src/common/dao/group/usergroup.go b/src/common/dao/group/usergroup.go index 5218a8a970..156db76a39 100644 --- a/src/common/dao/group/usergroup.go +++ b/src/common/dao/group/usergroup.go @@ -50,6 +50,10 @@ func QueryUserGroup(query models.UserGroup) ([]*models.UserGroup, error) { sql += ` and ldap_group_dn = ? ` sqlParam = append(sqlParam, query.LdapGroupDN) } + if query.ID != 0 { + sql += ` and id = ? ` + sqlParam = append(sqlParam, query.ID) + } _, err := o.Raw(sql, sqlParam).QueryRows(&groups) if err != nil { return nil, err @@ -60,12 +64,14 @@ func QueryUserGroup(query models.UserGroup) ([]*models.UserGroup, error) { // GetUserGroup ... func GetUserGroup(id int) (*models.UserGroup, error) { userGroup := models.UserGroup{ID: id} - o := dao.GetOrmer() - err := o.Read(&userGroup) + userGroupList, err := QueryUserGroup(userGroup) if err != nil { return nil, err } - return &userGroup, nil + if len(userGroupList) > 0 { + return userGroupList[0], nil + } + return nil, nil } // DeleteUserGroup ... @@ -92,3 +98,30 @@ func UpdateUserGroupName(id int, groupName string) error { _, err := o.Raw(sql, groupName, id).Exec() return err } + +// OnBoardUserGroup will check if a usergroup exists in usergroup table, if not insert the usergroup and +// put the id in the pointer of usergroup model, if it does exist, return the usergroup's profile. +// This is used for ldap and uaa authentication, such the usergroup can have an ID in Harbor. +// the keyAttribute and combinedKeyAttribute are key columns used to check duplicate usergroup in harbor +func OnBoardUserGroup(g *models.UserGroup, keyAttribute string, combinedKeyAttributes ...string) error { + o := dao.GetOrmer() + created, ID, err := o.ReadOrCreate(g, keyAttribute, combinedKeyAttributes...) + if err != nil { + return err + } + + if created { + g.ID = int(ID) + } else { + prevGroup, err := GetUserGroup(int(ID)) + if err != nil { + return err + } + g.ID = prevGroup.ID + g.GroupName = prevGroup.GroupName + g.GroupType = prevGroup.GroupType + g.LdapGroupDN = prevGroup.LdapGroupDN + } + + return nil +} diff --git a/src/common/dao/group/usergroup_test.go b/src/common/dao/group/usergroup_test.go index 7e65a09d58..c1ad6e4dc4 100644 --- a/src/common/dao/group/usergroup_test.go +++ b/src/common/dao/group/usergroup_test.go @@ -19,6 +19,7 @@ import ( "os" "testing" + "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" @@ -80,7 +81,7 @@ func TestAddUserGroup(t *testing.T) { want int wantErr bool }{ - {"Insert an ldap user group", args{userGroup: models.UserGroup{GroupName: "sample_group", GroupType: 1, LdapGroupDN: "sample_ldap_dn_string"}}, 0, false}, + {"Insert an ldap user group", args{userGroup: models.UserGroup{GroupName: "sample_group", GroupType: common.LdapGroupType, LdapGroupDN: "sample_ldap_dn_string"}}, 0, false}, {"Insert other user group", args{userGroup: models.UserGroup{GroupName: "other_group", GroupType: 3, LdapGroupDN: "other information"}}, 0, false}, } for _, tt := range tests { @@ -108,8 +109,8 @@ func TestQueryUserGroup(t *testing.T) { wantErr bool }{ {"Query all user group", args{query: models.UserGroup{GroupName: "test_group_01"}}, 1, false}, - {"Query all ldap group", args{query: models.UserGroup{GroupType: 1}}, 2, false}, - {"Query ldap group with group property", args{query: models.UserGroup{GroupType: 1, LdapGroupDN: "CN=harbor_users,OU=sample,OU=vmware,DC=harbor,DC=com"}}, 1, false}, + {"Query all ldap group", args{query: models.UserGroup{GroupType: common.LdapGroupType}}, 2, false}, + {"Query ldap group with group property", args{query: models.UserGroup{GroupType: common.LdapGroupType, LdapGroupDN: "CN=harbor_users,OU=sample,OU=vmware,DC=harbor,DC=com"}}, 1, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -126,7 +127,7 @@ func TestQueryUserGroup(t *testing.T) { } func TestGetUserGroup(t *testing.T) { - userGroup := models.UserGroup{GroupName: "insert_group", GroupType: 1, LdapGroupDN: "ldap_dn_string"} + userGroup := models.UserGroup{GroupName: "insert_group", GroupType: common.LdapGroupType, LdapGroupDN: "ldap_dn_string"} result, err := AddUserGroup(userGroup) if err != nil { t.Errorf("Error occurred when AddUserGroup: %v", err) @@ -142,6 +143,7 @@ func TestGetUserGroup(t *testing.T) { wantErr bool }{ {"Get User Group", args{id: result}, "insert_group", false}, + {"Get User Group does not exist", args{id: 9999}, "insert_group", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -150,7 +152,7 @@ func TestGetUserGroup(t *testing.T) { t.Errorf("GetUserGroup() error = %v, wantErr %v", err, tt.wantErr) return } - if got.GroupName != tt.want { + if got != nil && got.GroupName != tt.want { t.Errorf("GetUserGroup() = %v, want %v", got.GroupName, tt.want) } }) @@ -216,3 +218,34 @@ func TestDeleteUserGroup(t *testing.T) { }) } } + +func TestOnBoardUserGroup(t *testing.T) { + type args struct { + g *models.UserGroup + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"OnBoardUserGroup", + args{g: &models.UserGroup{ + GroupName: "harbor_example", + LdapGroupDN: "cn=harbor_example,ou=groups,dc=example,dc=com", + GroupType: common.LdapGroupType}}, + false}, + {"OnBoardUserGroup second time", + args{g: &models.UserGroup{ + GroupName: "harbor_example", + LdapGroupDN: "cn=harbor_example,ou=groups,dc=example,dc=com", + GroupType: common.LdapGroupType}}, + false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := OnBoardUserGroup(tt.args.g, "LdapGroupDN", "GroupType"); (err != nil) != tt.wantErr { + t.Errorf("OnBoardUserGroup() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/src/common/dao/project/projectmember.go b/src/common/dao/project/projectmember.go index 963eb62d68..ba59a0cfde 100644 --- a/src/common/dao/project/projectmember.go +++ b/src/common/dao/project/projectmember.go @@ -74,6 +74,7 @@ func GetProjectMember(queryMember models.Member) ([]*models.Member, error) { func AddProjectMember(member models.Member) (int, error) { log.Debugf("Adding project member %+v", member) + o := dao.GetOrmer() if member.EntityID <= 0 { @@ -98,9 +99,7 @@ func AddProjectMember(member models.Member) (int, error) { // UpdateProjectMemberRole updates the record in table project_member, only role can be changed func UpdateProjectMemberRole(pmID int, role int) error { - if role <= 0 || role >= 3 { - return fmt.Errorf("Failed to update project member, role is not in 0,1,2, role:%v", role) - } + o := dao.GetOrmer() sql := "update project_member set role = ? where id = ? " _, err := o.Raw(sql, role, pmID).Exec() diff --git a/src/common/models/ldap.go b/src/common/models/ldap.go index 28e09d026e..7d666b3a65 100644 --- a/src/common/models/ldap.go +++ b/src/common/models/ldap.go @@ -58,5 +58,5 @@ type LdapFailedImportUser struct { // LdapGroup ... type LdapGroup struct { GroupName string `json:"group_name,omitempty"` - GroupDN string `json:"group_dn,omitempty"` + GroupDN string `json:"ldap_group_dn,omitempty"` } diff --git a/src/ui/api/harborapi_test.go b/src/ui/api/harborapi_test.go index df8538b86f..bbbf664d4e 100644 --- a/src/ui/api/harborapi_test.go +++ b/src/ui/api/harborapi_test.go @@ -106,9 +106,11 @@ func init() { beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &MetadataAPI{}, "get:Get") beego.Router("/api/projects/:id([0-9]+)/metadatas/", &MetadataAPI{}, "post:Post") beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &MetadataAPI{}, "put:Put;delete:Delete") + beego.Router("/api/projects/:pid([0-9]+)/projectmembers/?:pmid([0-9]+)", &ProjectMemberAPI{}) beego.Router("/api/repositories", &RepositoryAPI{}) beego.Router("/api/statistics", &StatisticAPI{}) beego.Router("/api/users/?:id", &UserAPI{}) + beego.Router("/api/usergroups/?:ugid([0-9]+)", &UserGroupAPI{}) beego.Router("/api/logs", &LogAPI{}) beego.Router("/api/repositories/*", &RepositoryAPI{}, "put:Put") beego.Router("/api/repositories/*/labels", &RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository") diff --git a/src/ui/api/ldap.go b/src/ui/api/ldap.go index 43e1922880..bc4c572cd5 100644 --- a/src/ui/api/ldap.go +++ b/src/ui/api/ldap.go @@ -15,7 +15,7 @@ package api import ( - "net/http" + "fmt" "github.com/vmware/harbor/src/common/models" ldapUtils "github.com/vmware/harbor/src/common/utils/ldap" @@ -62,8 +62,7 @@ func (l *LdapAPI) Ping() { if string(l.Ctx.Input.RequestBody) == "" { ldapSession, err = ldapUtils.LoadSystemLdapConfig() if err != nil { - log.Errorf("Can't load system configuration, error: %v", err) - l.RenderError(http.StatusInternalServerError, pingErrorMessage) + l.HandleInternalServerError(fmt.Sprintf("Can't load system configuration, error: %v", err)) return } err = ldapSession.ConnectionTest() @@ -73,9 +72,7 @@ func (l *LdapAPI) Ping() { } if err != nil { - log.Errorf("ldap connect fail, error: %v", err) - // do not return any detail information of the error, or may cause SSRF security issue #3755 - l.RenderError(http.StatusBadRequest, pingErrorMessage) + l.HandleInternalServerError(fmt.Sprintf("LDAP connect fail, error: %v", err)) return } } @@ -84,24 +81,9 @@ func (l *LdapAPI) Ping() { func (l *LdapAPI) Search() { var err error var ldapUsers []models.LdapUser - var ldapConfs models.LdapConf - var ldapSession *ldapUtils.Session - l.Ctx.Input.CopyBody(1 << 32) - if string(l.Ctx.Input.RequestBody) == "" { - ldapSession, err = ldapUtils.LoadSystemLdapConfig() - if err != nil { - log.Errorf("can't load system configuration, error: %v", err) - l.RenderError(http.StatusInternalServerError, loadSystemErrorMessage) - return - } - } else { - l.DecodeJSONReqAndValidate(&ldapConfs) - ldapSession, err = ldapUtils.CreateWithConfig(ldapConfs) - } - + ldapSession, err := ldapUtils.LoadSystemLdapConfig() if err = ldapSession.Open(); err != nil { - log.Errorf("can't Open ldap session, error: %v", err) - l.RenderError(http.StatusInternalServerError, canNotOpenLdapSession) + l.HandleInternalServerError(fmt.Sprintf("Can't Open LDAP session, error: %v", err)) return } defer ldapSession.Close() @@ -111,8 +93,7 @@ func (l *LdapAPI) Search() { ldapUsers, err = ldapSession.SearchUser(searchName) if err != nil { - log.Errorf("Ldap search fail, error: %v", err) - l.RenderError(http.StatusBadRequest, searchLdapFailMessage) + l.HandleInternalServerError(fmt.Sprintf("LDAP search fail, error: %v", err)) return } @@ -132,14 +113,12 @@ func (l *LdapAPI) ImportUser() { ldapFailedImportUsers, err := importUsers(ldapConfs, ldapImportUsers.LdapUIDList) if err != nil { - log.Errorf("Ldap import user fail, error: %v", err) - l.RenderError(http.StatusBadRequest, importUserError) + l.HandleInternalServerError(fmt.Sprintf("LDAP import user fail, error: %v", err)) return } if len(ldapFailedImportUsers) > 0 { - log.Errorf("Import ldap user have internal error") - l.RenderError(http.StatusInternalServerError, importUserError) + l.HandleNotFound("Import LDAP user have internal error") l.Data["json"] = ldapFailedImportUsers l.ServeJSON() return @@ -153,12 +132,12 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models. ldapSession, err := ldapUtils.LoadSystemLdapConfig() if err != nil { - log.Errorf("can't load system configuration, error: %v", err) + log.Errorf("Can't load system configuration, error: %v", err) return nil, err } if err = ldapSession.Open(); err != nil { - log.Errorf("Can't connect to ldap, error: %v", err) + log.Errorf("Can't connect to LDAP, error: %v", err) } defer ldapSession.Close() @@ -182,7 +161,7 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models. u.UID = tempUID u.Error = "failed_search_user" failedImportUser = append(failedImportUser, u) - log.Errorf("Invalid ldap search request for %s, error: %v", tempUID, err) + log.Errorf("Invalid LDAP search request for %s, error: %v", tempUID, err) continue } @@ -211,3 +190,22 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models. return failedImportUser, nil } + +// SearchGroup ... Search LDAP by groupname +func (l *LdapAPI) SearchGroup() { + searchName := l.GetString("groupname") + ldapSession, err := ldapUtils.LoadSystemLdapConfig() + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("Can't get LDAP system config, error: %v", err)) + return + } + ldapSession.Open() + defer ldapSession.Close() + ldapGroups, err := ldapSession.SearchGroupByName(searchName) + if err != nil { + l.HandleInternalServerError(fmt.Sprintf("Can't search LDAP group by name, error: %v", err)) + return + } + l.Data["json"] = ldapGroups + l.ServeJSON() +} diff --git a/src/ui/api/projectmember.go b/src/ui/api/projectmember.go new file mode 100644 index 0000000000..fd38136991 --- /dev/null +++ b/src/ui/api/projectmember.go @@ -0,0 +1,206 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// 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" + + "github.com/vmware/harbor/src/common" + "github.com/vmware/harbor/src/common/dao/project" + "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils/log" + "github.com/vmware/harbor/src/ui/auth" +) + +// ProjectMemberAPI handles request to /api/projects/{}/members/{} +type ProjectMemberAPI struct { + BaseController + id int + entityID int + entityType string + project *models.Project +} + +// Prepare validates the URL and parms +func (pma *ProjectMemberAPI) Prepare() { + pma.BaseController.Prepare() + + if !pma.SecurityCtx.IsAuthenticated() { + pma.HandleUnauthorized() + 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.HandleBadRequest(text) + return + } + project, err := pma.ProjectMgr.Get(pid) + if err != nil { + pma.ParseAndHandleError(fmt.Sprintf("failed to get project %d", pid), err) + return + } + if project == nil { + pma.HandleNotFound(fmt.Sprintf("project %d not found", pid)) + return + } + pma.project = project + + if !(pma.Ctx.Input.IsGet() && pma.SecurityCtx.HasReadPerm(pid) || + pma.SecurityCtx.HasAllPerm(pid)) { + pma.HandleForbidden(pma.SecurityCtx.GetUsername()) + return + } + + pmid, err := pma.GetInt64FromPath(":pmid") + if err != nil { + log.Errorf("Failed to get pmid from path, error %v", err) + } + if pmid <= 0 && (pma.Ctx.Input.IsPut() || pma.Ctx.Input.IsDelete()) { + pma.HandleBadRequest(fmt.Sprintf("The project member id is invalid, pmid:%s", pma.GetStringFromPath(":pmid"))) + return + } + pma.id = int(pmid) +} + +//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 { + //member id not set, return all member of current project + memberList, err := project.GetProjectMember(queryMember) + if err != nil { + pma.HandleInternalServerError(fmt.Sprintf("Failed to query database for member list, error: %v", err)) + return + } + if len(memberList) > 0 { + pma.Data["json"] = memberList + } + } else { + //return a specific member + queryMember.ID = pma.id + memberList, err := project.GetProjectMember(queryMember) + if err != nil { + pma.HandleInternalServerError(fmt.Sprintf("Failed to query database for member list, error: %v", err)) + return + } + if len(memberList) == 0 { + pma.HandleNotFound(fmt.Sprintf("The project member does not exit, pmid:%v", pma.id)) + return + } + pma.Data["json"] = memberList[0] + } + pma.ServeJSON() +} + +// Post ... Add a project member +func (pma *ProjectMemberAPI) Post() { + projectID := pma.project.ProjectID + var request models.MemberReq + pma.DecodeJSONReq(&request) + pmid, err := AddOrUpdateProjectMember(projectID, request) + if err != nil { + pma.HandleInternalServerError(fmt.Sprintf("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() { + pid := pma.project.ProjectID + pmID := pma.id + var req models.Member + pma.DecodeJSONReq(&req) + if req.Role < 1 || req.Role > 3 { + pma.HandleBadRequest(fmt.Sprintf("Invalid role id %v", req.Role)) + return + } + err := project.UpdateProjectMemberRole(pmID, req.Role) + if err != nil { + pma.HandleInternalServerError(fmt.Sprintf("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() { + pmid := pma.id + err := project.DeleteProjectMemberByID(pmid) + if err != nil { + pma.HandleInternalServerError(fmt.Sprintf("Failed to delete project roles for user, project member id: %d, error: %v", pmid, err)) + return + } +} + +// AddOrUpdateProjectMember ... If the project member relationship does not exist, create it. if exist, update it +func AddOrUpdateProjectMember(projectID int64, request models.MemberReq) (int, error) { + var member models.Member + member.ProjectID = projectID + member.Role = request.Role + 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 + member.EntityType = common.GroupMember + } else if len(request.MemberUser.Username) > 0 { + member.EntityType = common.UserMember + userID, err := auth.SearchAndOnBoardUser(request.MemberUser.Username) + if err != nil { + return 0, err + } + member.EntityID = userID + } else if len(request.MemberGroup.LdapGroupDN) > 0 { + member.EntityType = common.GroupMember + //If groupname provided, use the provided groupname + //If ldap group already exist in harbor, use the previous group name + groupID, err := auth.SearchAndOnBoardGroup(request.MemberGroup.LdapGroupDN, request.MemberGroup.GroupName) + if err != nil { + return 0, err + } + member.EntityID = groupID + } + if member.EntityID <= 0 { + return 0, fmt.Errorf("Can not get valid member entity, request: %+v", request) + } + 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 { + project.UpdateProjectMemberRole(memberList[0].ID, member.Role) + return 0, nil + } + + if member.Role < 1 || member.Role > 3 { + return 0, fmt.Errorf("Failed to update project member, role is not in 1,2,3 role:%v", member.Role) + } + return project.AddProjectMember(member) +} diff --git a/src/ui/api/projectmember_test.go b/src/ui/api/projectmember_test.go new file mode 100644 index 0000000000..3b72d5a24b --- /dev/null +++ b/src/ui/api/projectmember_test.go @@ -0,0 +1,210 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// 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" + "testing" + + "github.com/vmware/harbor/src/common/dao" + "github.com/vmware/harbor/src/common/dao/project" + "github.com/vmware/harbor/src/common/models" +) + +func TestProjectMemberAPI_Get(t *testing.T) { + cases := []*codeCheckingCase{ + // 401 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: "/api/projects/1/projectmembers", + }, + code: http.StatusUnauthorized, + }, + //200 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: "/api/projects/1/projectmembers", + credential: admin, + }, + code: http.StatusOK, + }, + //400 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: "/api/projects/0/projectmembers", + credential: admin, + }, + code: http.StatusBadRequest, + }, + // 404 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: "/api/projects/1/projectmembers/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) + } + + cases := []*codeCheckingCase{ + // 401 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: "/api/projects/1/projectmembers", + bodyJSON: &models.MemberReq{ + Role: 1, + MemberUser: models.User{ + UserID: int(userID), + }, + }, + }, + code: http.StatusUnauthorized, + }, + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: "/api/projects/1/projectmembers", + bodyJSON: &models.MemberReq{ + Role: 1, + MemberUser: models.User{ + UserID: int(userID), + }, + }, + credential: admin, + }, + code: http.StatusCreated, + }, + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: "/api/projects/1/projectmembers", + bodyJSON: &models.MemberReq{ + Role: 1, + MemberUser: models.User{ + UserID: 0, + }, + }, + credential: admin, + }, + 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) + } + URL := fmt.Sprintf("/api/projects/1/projectmembers/%v", ID) + badURL := fmt.Sprintf("/api/projects/1/projectmembers/%v", 0) + cases := []*codeCheckingCase{ + // 401 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: URL, + bodyJSON: &models.Member{ + Role: 2, + }, + }, + code: http.StatusUnauthorized, + }, + // 200 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: URL, + bodyJSON: &models.Member{ + Role: 2, + }, + credential: admin, + }, + code: http.StatusOK, + }, + // 400 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: badURL, + bodyJSON: &models.Member{ + Role: 2, + }, + credential: admin, + }, + code: http.StatusBadRequest, + }, + // 400 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: URL, + bodyJSON: &models.Member{ + Role: -2, + }, + credential: admin, + }, + code: http.StatusBadRequest, + }, + // 200 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodDelete, + url: URL, + credential: admin, + }, + code: http.StatusOK, + }, + } + + runCodeCheckingCases(t, cases...) + +} diff --git a/src/ui/api/usergroup.go b/src/ui/api/usergroup.go new file mode 100644 index 0000000000..4645b9b0f8 --- /dev/null +++ b/src/ui/api/usergroup.go @@ -0,0 +1,146 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// 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" + + "github.com/vmware/harbor/src/common" + "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/auth" +) + +// UserGroupAPI ... +type UserGroupAPI struct { + BaseController + id int +} + +// Prepare validates the URL and parms +func (uga *UserGroupAPI) Prepare() { + uga.BaseController.Prepare() + if !uga.SecurityCtx.IsAuthenticated() { + uga.HandleUnauthorized() + return + } + + ugid, err := uga.GetInt64FromPath(":ugid") + if err != nil { + log.Warningf("failed to parse user group id, error: %v", err) + } + if ugid <= 0 && (uga.Ctx.Input.IsPut() || uga.Ctx.Input.IsDelete()) { + uga.HandleBadRequest(fmt.Sprintf("invalid user group ID: %s", uga.GetStringFromPath(":ugid"))) + return + } + uga.id = int(ugid) + //Common user can create/update, only harbor admin can delete user group. + if uga.Ctx.Input.IsDelete() && !uga.SecurityCtx.IsSysAdmin() { + uga.HandleForbidden(uga.SecurityCtx.GetUsername()) + return + } +} + +// Get ... +func (uga *UserGroupAPI) Get() { + ID := uga.id + uga.Data["json"] = make([]models.UserGroup, 0) + if ID == 0 { + //user group id not set, return all user group + query := models.UserGroup{GroupType: common.LdapGroupType} //Current query LDAP group only + userGroupList, err := group.QueryUserGroup(query) + if err != nil { + uga.HandleInternalServerError(fmt.Sprintf("Failed to query database for user group list, error: %v", err)) + return + } + if len(userGroupList) > 0 { + uga.Data["json"] = userGroupList + } + } else { + //return a specific user group + userGroup, err := group.GetUserGroup(ID) + if userGroup == nil { + uga.HandleNotFound("The user group does not exist.") + return + } + if err != nil { + uga.HandleInternalServerError(fmt.Sprintf("Failed to query database for user group list, error: %v", err)) + return + } + uga.Data["json"] = userGroup + } + uga.ServeJSON() +} + +// Post ... Create User Group +func (uga *UserGroupAPI) Post() { + userGroup := models.UserGroup{} + uga.DecodeJSONReq(&userGroup) + userGroup.ID = 0 + userGroup.GroupType = common.LdapGroupType + query := models.UserGroup{GroupType: userGroup.GroupType, LdapGroupDN: userGroup.LdapGroupDN} + result, err := group.QueryUserGroup(query) + if err != nil { + uga.HandleInternalServerError(fmt.Sprintf("Error occurred in add user group, error: %v", err)) + return + } + if len(result) > 0 { + uga.HandleConflict("Error occurred in add user group, duplicate user group exist.") + } + // User can not add ldap group when the ldap server is offline + ldapGroup, err := auth.SearchGroup(userGroup.LdapGroupDN) + if err != nil { + uga.HandleInternalServerError(fmt.Sprintf("Error occurred in search user group. error: %v", err)) + return + } + if ldapGroup == nil { + uga.HandleNotFound("The LDAP group is not found") + return + } + groupID, err := group.AddUserGroup(userGroup) + if err != nil { + uga.HandleInternalServerError(fmt.Sprintf("Error occurred in add user group, error: %v", err)) + return + } + uga.Redirect(http.StatusCreated, strconv.FormatInt(int64(groupID), 10)) +} + +// Put ... Only support update name +func (uga *UserGroupAPI) Put() { + userGroup := models.UserGroup{} + uga.DecodeJSONReq(&userGroup) + ID := uga.id + userGroup.GroupType = common.LdapGroupType + log.Debugf("Updated user group %v", userGroup) + err := group.UpdateUserGroupName(ID, userGroup.GroupName) + if err != nil { + uga.HandleInternalServerError(fmt.Sprintf("Error occurred in update user group, error: %v", err)) + return + } + return +} + +// Delete ... +func (uga *UserGroupAPI) Delete() { + err := group.DeleteUserGroup(uga.id) + if err != nil { + uga.HandleInternalServerError(fmt.Sprintf("Error occurred in update user group, error: %v", err)) + return + } + return +} diff --git a/src/ui/api/usergroup_test.go b/src/ui/api/usergroup_test.go new file mode 100644 index 0000000000..d160a0977e --- /dev/null +++ b/src/ui/api/usergroup_test.go @@ -0,0 +1,154 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// 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" + "testing" + + "github.com/vmware/harbor/src/common" + + "github.com/vmware/harbor/src/common/dao/group" + + "github.com/vmware/harbor/src/common/models" +) + +const ( + URL = "/api/usergroups" +) + +func TestUserGroupAPI_GetAndDelete(t *testing.T) { + + groupID, err := group.AddUserGroup(models.UserGroup{ + GroupName: "harbor_users", + LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com", + GroupType: common.LdapGroupType, + }) + + if err != nil { + t.Errorf("Error occurred when AddUserGroup: %v", err) + } + defer group.DeleteUserGroup(groupID) + cases := []*codeCheckingCase{ + // 401 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: URL, + }, + code: http.StatusUnauthorized, + }, + + // 200 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: fmt.Sprintf("/api/usergroups/%d", groupID), + credential: admin, + }, + code: http.StatusOK, + }, + // 200 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodGet, + url: fmt.Sprintf("/api/usergroups"), + credential: admin, + }, + code: http.StatusOK, + }, + // 200 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodDelete, + url: fmt.Sprintf("/api/usergroups/%d", groupID), + credential: admin, + }, + code: http.StatusOK, + }, + } + + runCodeCheckingCases(t, cases...) +} + +func TestUserGroupAPI_Post(t *testing.T) { + groupID, err := group.AddUserGroup(models.UserGroup{ + GroupName: "harbor_group", + LdapGroupDN: "cn=harbor_group,ou=groups,dc=example,dc=com", + GroupType: common.LdapGroupType, + }) + if err != nil { + t.Errorf("Error occurred when AddUserGroup: %v", err) + } + defer group.DeleteUserGroup(groupID) + + cases := []*codeCheckingCase{ + //409 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPost, + url: "/api/usergroups", + bodyJSON: &models.UserGroup{ + GroupName: "harbor_group", + LdapGroupDN: "cn=harbor_group,ou=groups,dc=example,dc=com", + GroupType: common.LdapGroupType, + }, + credential: admin, + }, + code: http.StatusConflict, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestUserGroupAPI_Put(t *testing.T) { + groupID, err := group.AddUserGroup(models.UserGroup{ + GroupName: "harbor_group", + LdapGroupDN: "cn=harbor_groups,ou=groups,dc=example,dc=com", + GroupType: common.LdapGroupType, + }) + defer group.DeleteUserGroup(groupID) + + if err != nil { + t.Errorf("Error occurred when AddUserGroup: %v", err) + } + cases := []*codeCheckingCase{ + //401 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("/api/usergroups/%d", groupID), + bodyJSON: &models.UserGroup{ + GroupName: "my_group", + }, + }, + code: http.StatusUnauthorized, + }, + //200 + &codeCheckingCase{ + request: &testingRequest{ + method: http.MethodPut, + url: fmt.Sprintf("/api/usergroups/%d", groupID), + bodyJSON: &models.UserGroup{ + GroupName: "my_group", + }, + credential: admin, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} diff --git a/src/ui/auth/auth_test.go b/src/ui/auth/auth_test.go index fe93018c20..9998d552ea 100644 --- a/src/ui/auth/auth_test.go +++ b/src/ui/auth/auth_test.go @@ -68,7 +68,7 @@ func TestDefaultAuthenticate(t *testing.T) { authHelper := DefaultAuthenticateHelper{} m := models.AuthModel{} user, err := authHelper.Authenticate(m) - if user != nil || err != nil { + if user != nil || err == nil { t.Fatal("Default implementation should return nil") } } @@ -77,8 +77,26 @@ func TestDefaultOnBoardUser(t *testing.T) { user := &models.User{} authHelper := DefaultAuthenticateHelper{} err := authHelper.OnBoardUser(user) - if err != nil { - t.Fatal("Default implementation should return nil") + if err == nil { + t.Fatal("Default implementation should return error") + } +} + +func TestDefaultMethods(t *testing.T) { + authHelper := DefaultAuthenticateHelper{} + _, err := authHelper.SearchUser("sample") + if err == nil { + t.Fatal("Default implementation should return error") + } + + _, err = authHelper.SearchGroup("sample") + if err == nil { + t.Fatal("Default implementation should return error") + } + + err = authHelper.OnBoardGroup(&models.UserGroup{}, "sample") + if err == nil { + t.Fatal("Default implementation should return error") } } diff --git a/src/ui/auth/authenticator.go b/src/ui/auth/authenticator.go index f7f8230ccc..9ac7e88530 100644 --- a/src/ui/auth/authenticator.go +++ b/src/ui/auth/authenticator.go @@ -15,6 +15,7 @@ package auth import ( + "errors" "fmt" "time" @@ -55,9 +56,13 @@ type AuthenticateHelper interface { // put the id in the pointer of user model, if it does exist, fill in the user model based // on the data record of the user OnBoardUser(u *models.User) error + // Create a group in harbor DB, if altGroupName is not empty, take the altGroupName as groupName in harbor DB. + OnBoardGroup(g *models.UserGroup, altGroupName string) error // Get user information from account repository SearchUser(username string) (*models.User, error) - // Update user information after authenticate, such as Onboard or sync info etc + // Search a group based on specific authentication + SearchGroup(groupDN string) (*models.UserGroup, error) + // Update user information after authenticate, such as OnBoard or sync info etc PostAuthenticate(u *models.User) error } @@ -67,26 +72,36 @@ type DefaultAuthenticateHelper struct { // Authenticate ... func (d *DefaultAuthenticateHelper) Authenticate(m models.AuthModel) (*models.User, error) { - return nil, nil + return nil, errors.New("Not supported") } // OnBoardUser will check if a user exists in user table, if not insert the user and // put the id in the pointer of user model, if it does exist, fill in the user model based // on the data record of the user func (d *DefaultAuthenticateHelper) OnBoardUser(u *models.User) error { - return nil + return errors.New("Not supported") } //SearchUser - Get user information from account repository func (d *DefaultAuthenticateHelper) SearchUser(username string) (*models.User, error) { - return nil, nil + return nil, errors.New("Not supported") } -//PostAuthenticate - Update user information after authenticate, such as Onboard or sync info etc +//PostAuthenticate - Update user information after authenticate, such as OnBoard or sync info etc func (d *DefaultAuthenticateHelper) PostAuthenticate(u *models.User) error { return nil } +// OnBoardGroup - OnBoardGroup, it will set the ID of the user group, if altGroupName is not empty, take the altGroupName as groupName in harbor DB. +func (d *DefaultAuthenticateHelper) OnBoardGroup(u *models.UserGroup, altGroupName string) error { + return errors.New("Not supported") +} + +// SearchGroup - Search ldap group by group key, groupKey is the unique attribute of group in authenticator, for LDAP, the key is group DN +func (d *DefaultAuthenticateHelper) SearchGroup(groupKey string) (*models.UserGroup, error) { + return nil, errors.New("Not supported") +} + var registry = make(map[string]AuthenticateHelper) // Register add different authenticators to registry map. @@ -128,9 +143,7 @@ func Login(m models.AuthModel) (*models.User, error) { } return nil, err } - err = authenticator.PostAuthenticate(user) - return user, err } @@ -166,6 +179,51 @@ func SearchUser(username string) (*models.User, error) { return helper.SearchUser(username) } +// OnBoardGroup - Create a user group in harbor db, if altGroupName is not empty, take the altGroupName as groupName in harbor DB +func OnBoardGroup(userGroup *models.UserGroup, altGroupName string) error { + helper, err := getHelper() + if err != nil { + return err + } + return helper.OnBoardGroup(userGroup, altGroupName) +} + +// SearchGroup -- Search group in authenticator, groupKey is the unique attribute of group in authenticator, for LDAP, the key is group DN +func SearchGroup(groupKey string) (*models.UserGroup, error) { + helper, err := getHelper() + if err != nil { + return nil, err + } + return helper.SearchGroup(groupKey) +} + +// SearchAndOnBoardUser ... Search user and OnBoard user, if user exist, return the ID of current user. +func SearchAndOnBoardUser(username string) (int, error) { + user, err := SearchUser(username) + if err != nil { + return 0, err + } + if user != nil { + err = OnBoardUser(user) + if err != nil { + return 0, err + } + } + return user.UserID, nil +} + +// SearchAndOnBoardGroup ... if altGroupName is not empty, take the altGroupName as groupName in harbor DB +func SearchAndOnBoardGroup(groupKey, altGroupName string) (int, error) { + userGroup, err := SearchGroup(groupKey) + if err != nil { + return 0, err + } + if userGroup != nil { + err = OnBoardGroup(userGroup, altGroupName) + } + return userGroup.ID, err +} + // PostAuthenticate - func PostAuthenticate(u *models.User) error { helper, err := getHelper() diff --git a/src/ui/auth/db/db_test.go b/src/ui/auth/db/db_test.go index 246e1b650c..c49c4d5532 100644 --- a/src/ui/auth/db/db_test.go +++ b/src/ui/auth/db/db_test.go @@ -126,7 +126,7 @@ func TestSearchUser(t *testing.T) { } -func TestAuthenticateHelperOnboardUser(t *testing.T) { +func TestAuthenticateHelperOnBoardUser(t *testing.T) { user := models.User{ Username: "test01", Realname: "test01", diff --git a/src/ui/auth/ldap/ldap.go b/src/ui/auth/ldap/ldap.go index 5fd03e16b3..6d114a66bb 100644 --- a/src/ui/auth/ldap/ldap.go +++ b/src/ui/auth/ldap/ldap.go @@ -19,7 +19,10 @@ import ( "regexp" "strings" + "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" ldapUtils "github.com/vmware/harbor/src/common/utils/ldap" "github.com/vmware/harbor/src/common/utils/log" @@ -130,6 +133,45 @@ func (l *Auth) SearchUser(username string) (*models.User, error) { return &user, nil } +//SearchGroup -- Search group in ldap authenticator, groupKey is LDAP group DN. +func (l *Auth) SearchGroup(groupKey string) (*models.UserGroup, error) { + ldapSession, err := ldapUtils.LoadSystemLdapConfig() + + if err != nil { + return nil, fmt.Errorf("can not load system ldap config: %v", err) + } + + if err = ldapSession.Open(); err != nil { + log.Warningf("ldap connection fail: %v", err) + return nil, err + } + defer ldapSession.Close() + userGroupList, err := ldapSession.SearchGroupByDN(groupKey) + + if err != nil { + log.Warningf("ldap search group fail: %v", err) + return nil, err + } + + if len(userGroupList) == 0 { + return nil, fmt.Errorf("Failed to searh ldap group with groupDN:%v", groupKey) + } + userGroup := models.UserGroup{ + GroupName: userGroupList[0].GroupName, + LdapGroupDN: userGroupList[0].GroupDN, + } + return &userGroup, nil +} + +// OnBoardGroup -- Create Group in harbor DB, if altGroupName is not empty, take the altGroupName as groupName in harbor DB. +func (l *Auth) OnBoardGroup(u *models.UserGroup, altGroupName string) error { + if len(altGroupName) > 0 { + u.GroupName = altGroupName + } + u.GroupType = common.LdapGroupType + return group.OnBoardUserGroup(u, "LdapGroupDN", "GroupType") +} + //PostAuthenticate -- If user exist in harbor DB, sync email address, if not exist, call OnBoardUser func (l *Auth) PostAuthenticate(u *models.User) error { diff --git a/src/ui/auth/ldap/ldap_test.go b/src/ui/auth/ldap/ldap_test.go index 3f9c2bce59..ae6dc45e08 100644 --- a/src/ui/auth/ldap/ldap_test.go +++ b/src/ui/auth/ldap/ldap_test.go @@ -22,9 +22,11 @@ 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" "github.com/vmware/harbor/src/common/utils/test" + "github.com/vmware/harbor/src/ui/api" "github.com/vmware/harbor/src/ui/auth" uiConfig "github.com/vmware/harbor/src/ui/config" ) @@ -46,7 +48,7 @@ var adminServerLdapTestConfig = map[string]interface{}{ common.LDAPBaseDN: "dc=example,dc=com", common.LDAPUID: "uid", common.LDAPFilter: "", - common.LDAPScope: 3, + common.LDAPScope: 2, common.LDAPTimeout: 30, // config.TokenServiceURL: "", // config.RegistryURL: "", @@ -63,7 +65,11 @@ var adminServerLdapTestConfig = map[string]interface{}{ // config.TokenExpiration: 30, common.CfgExpiration: 5, // config.JobLogDir: "/var/log/jobs", - common.AdminInitialPassword: "password", + common.AdminInitialPassword: "password", + common.LDAPGroupSearchFilter: "objectclass=groupOfNames", + common.LDAPGroupBaseDN: "dc=example,dc=com", + common.LDAPGroupAttributeName: "cn", + common.LDAPGroupSearchScope: 2, } func TestMain(m *testing.M) { @@ -102,6 +108,24 @@ func TestMain(m *testing.M) { log.Fatalf("failed to initialize database: %v", err) } + //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 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 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 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)", + } + + clearSqls := []string{ + "delete from project where name='member_test_01'", + "delete from user where username='member_test_01' or username='pm_sample'", + "delete from user_group", + "delete from project_member", + } + dao.PrepareTestData(clearSqls, initSqls) + retCode := m.Run() os.Exit(retCode) } @@ -168,7 +192,7 @@ func TestSearchUser_02(t *testing.T) { } -func TestOnboardUser(t *testing.T) { +func TestOnBoardUser(t *testing.T) { user := &models.User{ Username: "sample", Email: "sample@example.com", @@ -186,7 +210,7 @@ func TestOnboardUser(t *testing.T) { assert.Equal(t, "sample@example.com", user.Email) } -func TestOnboardUser_02(t *testing.T) { +func TestOnBoardUser_02(t *testing.T) { user := &models.User{ Username: "sample02", Realname: "Sample02", @@ -204,7 +228,7 @@ func TestOnboardUser_02(t *testing.T) { dao.CleanUser(int64(user.UserID)) } -func TestOnboardUser_03(t *testing.T) { +func TestOnBoardUser_03(t *testing.T) { user := &models.User{ Username: "sample03@example.com", Realname: "Sample03", @@ -222,7 +246,7 @@ func TestOnboardUser_03(t *testing.T) { dao.CleanUser(int64(user.UserID)) } -func TestAuthenticateHelperOnboardUser(t *testing.T) { +func TestAuthenticateHelperOnBoardUser(t *testing.T) { user := models.User{ Username: "test01", Realname: "test01", @@ -240,6 +264,21 @@ func TestAuthenticateHelperOnboardUser(t *testing.T) { } +func TestOnBoardGroup(t *testing.T) { + group := models.UserGroup{ + GroupName: "harbor_group2", + LdapGroupDN: "cn=harbor_group2,ou=groups,dc=example,dc=com", + } + newGroupName := "group_name123" + err := auth.OnBoardGroup(&group, newGroupName) + if err != nil { + t.Errorf("Failed to OnBoardGroup, %+v", group) + } + if group.GroupName != "group_name123" { + t.Errorf("The OnBoardGroup should have name %v", newGroupName) + } +} + func TestAuthenticateHelperSearchUser(t *testing.T) { user, err := auth.SearchUser("test") @@ -307,3 +346,70 @@ func TestPostAuthentication(t *testing.T) { assert.EqualValues("test003@example.com", dbUser.Email) dao.CleanUser(int64(dbUser.UserID)) } + +func TestSearchAndOnBoardUser(t *testing.T) { + userID, err := auth.SearchAndOnBoardUser("mike02") + defer dao.CleanUser(int64(userID)) + if err != nil { + t.Errorf("Error occurred when SearchAndOnBoardUser: %v", err) + } + if userID == 0 { + t.Errorf("Can not search and onboard user %v", "mike") + } +} +func TestAddOrUpdateProjectMemberWithLdapUser(t *testing.T) { + + currentProject, err := dao.GetProjectByName("member_test_01") + if err != nil { + t.Errorf("Error occurred when GetProjectByName: %v", err) + } + member := models.MemberReq{ + ProjectID: currentProject.ProjectID, + MemberUser: models.User{ + Username: "mike", + }, + Role: models.PROJECTADMIN, + } + + pmid, err := api.AddOrUpdateProjectMember(currentProject.ProjectID, member) + if err != nil { + t.Errorf("Error occurred in AddOrUpdateProjectMember: %v", err) + } + if pmid == 0 { + t.Errorf("Error occurred in AddOrUpdateProjectMember: pmid:%v", pmid) + } + +} +func TestAddProjectMemberWithLdapGroup(t *testing.T) { + + currentProject, err := dao.GetProjectByName("member_test_01") + if err != nil { + t.Errorf("Error occurred when GetProjectByName: %v", err) + } + member := models.MemberReq{ + ProjectID: currentProject.ProjectID, + MemberGroup: models.UserGroup{ + LdapGroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com", + }, + Role: models.PROJECTADMIN, + } + + pmid, err := api.AddOrUpdateProjectMember(currentProject.ProjectID, member) + if err != nil { + t.Errorf("Error occurred in AddOrUpdateProjectMember: %v", err) + } + if pmid == 0 { + t.Errorf("Error occurred in AddOrUpdateProjectMember: pmid: %v", pmid) + } + queryMember := models.Member{ + ProjectID: currentProject.ProjectID, + } + memberList, err := project.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) + } +} diff --git a/src/ui/router.go b/src/ui/router.go index 59a0b62881..121f8cb2e8 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -46,6 +46,7 @@ func initRouters() { //API: beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectUserMemberAPI{}) + beego.Router("/api/projects/:pid([0-9]+)/projectmembers/?:pmid([0-9]+)", &api.ProjectMemberAPI{}) beego.Router("/api/projects/", &api.ProjectAPI{}, "head:Head") beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{}) @@ -53,8 +54,10 @@ func initRouters() { beego.Router("/api/users", &api.UserAPI{}, "get:List;post:Post") beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword") beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole") + beego.Router("/api/usergroups/?:ugid([0-9]+)", &api.UserGroupAPI{}) beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping") - beego.Router("/api/ldap/users/search", &api.LdapAPI{}, "post:Search") + beego.Router("/api/ldap/users/search", &api.LdapAPI{}, "get:Search") + beego.Router("/api/ldap/groups/search", &api.LdapAPI{}, "get:SearchGroup") beego.Router("/api/ldap/users/import", &api.LdapAPI{}, "post:ImportUser") beego.Router("/api/email/ping", &api.EmailAPI{}, "post:Ping") } @@ -115,7 +118,7 @@ func initRouters() { beego.Router("/service/token", &token.Handler{}) beego.Router("/registryproxy/*", &controllers.RegistryProxy{}, "*:Handle") - + //Error pages beego.ErrorController(&controllers.ErrorController{}) diff --git a/tests/ldap_test.ldif b/tests/ldap_test.ldif index bc8ddde56e..7053102b69 100644 --- a/tests/ldap_test.ldif +++ b/tests/ldap_test.ldif @@ -33,6 +33,24 @@ member: cn=mike02,ou=people,dc=example,dc=com objectclass: groupOfNames objectclass: top +# Group Entry harbor_group +dn: cn=harbor_group,ou=groups,dc=example,dc=com +cn: harbor_group +description: harbor group +member: cn=mike,ou=people,dc=example,dc=com +member: cn=mike02,ou=people,dc=example,dc=com +objectclass: groupOfNames +objectclass: top + +# Group Entry harbor_group2 +dn: cn=harbor_group2,ou=groups,dc=example,dc=com +cn: harbor_group2 +description: harbor group2 +member: cn=mike,ou=people,dc=example,dc=com +member: cn=mike02,ou=people,dc=example,dc=com +objectclass: groupOfNames +objectclass: top + # User belongs to harbor_user dn: cn=mike,ou=people,dc=example,dc=com cn: mike