Merge pull request #4483 from stonezdj/api4assign_role_to_group4

Add REST API for assign role to group
This commit is contained in:
stone 2018-04-04 16:19:37 +08:00 committed by GitHub
commit df63a73fd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1464 additions and 77 deletions

View File

@ -627,6 +627,167 @@ paths:
description: Project ID does not exist. description: Project ID does not exist.
'500': '500':
description: Unexpected internal errors. 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: /statistics:
get: get:
summary: Get projects number and repositories number relevant to the user summary: Get projects number and repositories number relevant to the user
@ -2307,8 +2468,153 @@ paths:
$ref: '#/responses/UnsupportedMediaType' $ref: '#/responses/UnsupportedMediaType'
'500': '500':
description: Unexpected internal errors. description: Unexpected internal errors.
/ldap/users/search: /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: 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:
get:
summary: Search available ldap users. summary: Search available ldap users.
description: > description: >
This endpoint searches the available ldap users based on related This endpoint searches the available ldap users based on related
@ -2320,15 +2626,6 @@ paths:
type: string type: string
required: false required: false
description: Registered user ID 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: tags:
- Products - Products
responses: responses:
@ -2338,14 +2635,10 @@ paths:
type: array type: array
items: items:
$ref: '#/definitions/LdapUsers' $ref: '#/definitions/LdapUsers'
'400':
description: Inviald ldap configuration parameters.
'401': '401':
description: User need to login first. description: User need to login first.
'403': '403':
description: Only admin has this authority. description: Only admin has this authority.
'415':
$ref: '#/responses/UnsupportedMediaType'
'500': '500':
description: Unexpected internal errors. description: Unexpected internal errors.
/ldap/users/import: /ldap/users/import:
@ -2379,7 +2672,7 @@ paths:
description: Only admin has this authority. description: Only admin has this authority.
'415': '415':
$ref: '#/responses/UnsupportedMediaType' $ref: '#/responses/UnsupportedMediaType'
'500': '404':
description: Failed import some users. description: Failed import some users.
schema: schema:
type: array type: array
@ -3440,3 +3733,70 @@ definitions:
update_time: update_time:
type: string type: string
description: The update time of label. 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).

View File

@ -106,4 +106,5 @@ const (
DefaultJobserviceEndpoint = "http://jobservice:8080" DefaultJobserviceEndpoint = "http://jobservice:8080"
DefaultUIEndpoint = "http://ui:8080" DefaultUIEndpoint = "http://ui:8080"
DefaultNotaryEndpoint = "http://notary-server:4443" DefaultNotaryEndpoint = "http://notary-server:4443"
LdapGroupType = 1
) )

View File

@ -50,6 +50,10 @@ func QueryUserGroup(query models.UserGroup) ([]*models.UserGroup, error) {
sql += ` and ldap_group_dn = ? ` sql += ` and ldap_group_dn = ? `
sqlParam = append(sqlParam, query.LdapGroupDN) sqlParam = append(sqlParam, query.LdapGroupDN)
} }
if query.ID != 0 {
sql += ` and id = ? `
sqlParam = append(sqlParam, query.ID)
}
_, err := o.Raw(sql, sqlParam).QueryRows(&groups) _, err := o.Raw(sql, sqlParam).QueryRows(&groups)
if err != nil { if err != nil {
return nil, err return nil, err
@ -60,12 +64,14 @@ func QueryUserGroup(query models.UserGroup) ([]*models.UserGroup, error) {
// GetUserGroup ... // GetUserGroup ...
func GetUserGroup(id int) (*models.UserGroup, error) { func GetUserGroup(id int) (*models.UserGroup, error) {
userGroup := models.UserGroup{ID: id} userGroup := models.UserGroup{ID: id}
o := dao.GetOrmer() userGroupList, err := QueryUserGroup(userGroup)
err := o.Read(&userGroup)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &userGroup, nil if len(userGroupList) > 0 {
return userGroupList[0], nil
}
return nil, nil
} }
// DeleteUserGroup ... // DeleteUserGroup ...
@ -92,3 +98,30 @@ func UpdateUserGroupName(id int, groupName string) error {
_, err := o.Raw(sql, groupName, id).Exec() _, err := o.Raw(sql, groupName, id).Exec()
return err 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
}

View File

@ -19,6 +19,7 @@ import (
"os" "os"
"testing" "testing"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
@ -80,7 +81,7 @@ func TestAddUserGroup(t *testing.T) {
want int want int
wantErr bool 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}, {"Insert other user group", args{userGroup: models.UserGroup{GroupName: "other_group", GroupType: 3, LdapGroupDN: "other information"}}, 0, false},
} }
for _, tt := range tests { for _, tt := range tests {
@ -108,8 +109,8 @@ func TestQueryUserGroup(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{"Query all user group", args{query: models.UserGroup{GroupName: "test_group_01"}}, 1, false}, {"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 all ldap group", args{query: models.UserGroup{GroupType: common.LdapGroupType}}, 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 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -126,7 +127,7 @@ func TestQueryUserGroup(t *testing.T) {
} }
func TestGetUserGroup(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) result, err := AddUserGroup(userGroup)
if err != nil { if err != nil {
t.Errorf("Error occurred when AddUserGroup: %v", err) t.Errorf("Error occurred when AddUserGroup: %v", err)
@ -142,6 +143,7 @@ func TestGetUserGroup(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{"Get User Group", args{id: result}, "insert_group", false}, {"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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) t.Errorf("GetUserGroup() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if got.GroupName != tt.want { if got != nil && got.GroupName != tt.want {
t.Errorf("GetUserGroup() = %v, want %v", 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)
}
})
}
}

View File

@ -74,6 +74,7 @@ func GetProjectMember(queryMember models.Member) ([]*models.Member, error) {
func AddProjectMember(member models.Member) (int, error) { func AddProjectMember(member models.Member) (int, error) {
log.Debugf("Adding project member %+v", member) log.Debugf("Adding project member %+v", member)
o := dao.GetOrmer() o := dao.GetOrmer()
if member.EntityID <= 0 { 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 // UpdateProjectMemberRole updates the record in table project_member, only role can be changed
func UpdateProjectMemberRole(pmID int, role int) error { 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() o := dao.GetOrmer()
sql := "update project_member set role = ? where id = ? " sql := "update project_member set role = ? where id = ? "
_, err := o.Raw(sql, role, pmID).Exec() _, err := o.Raw(sql, role, pmID).Exec()

View File

@ -58,5 +58,5 @@ type LdapFailedImportUser struct {
// LdapGroup ... // LdapGroup ...
type LdapGroup struct { type LdapGroup struct {
GroupName string `json:"group_name,omitempty"` GroupName string `json:"group_name,omitempty"`
GroupDN string `json:"group_dn,omitempty"` GroupDN string `json:"ldap_group_dn,omitempty"`
} }

View File

@ -108,9 +108,11 @@ func init() {
beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &MetadataAPI{}, "get:Get") beego.Router("/api/projects/:id([0-9]+)/metadatas/?:name", &MetadataAPI{}, "get:Get")
beego.Router("/api/projects/:id([0-9]+)/metadatas/", &MetadataAPI{}, "post:Post") beego.Router("/api/projects/:id([0-9]+)/metadatas/", &MetadataAPI{}, "post:Post")
beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &MetadataAPI{}, "put:Put;delete:Delete") beego.Router("/api/projects/:id([0-9]+)/metadatas/:name", &MetadataAPI{}, "put:Put;delete:Delete")
beego.Router("/api/projects/:pid([0-9]+)/projectmembers/?:pmid([0-9]+)", &ProjectMemberAPI{})
beego.Router("/api/repositories", &RepositoryAPI{}) beego.Router("/api/repositories", &RepositoryAPI{})
beego.Router("/api/statistics", &StatisticAPI{}) beego.Router("/api/statistics", &StatisticAPI{})
beego.Router("/api/users/?:id", &UserAPI{}) beego.Router("/api/users/?:id", &UserAPI{})
beego.Router("/api/usergroups/?:ugid([0-9]+)", &UserGroupAPI{})
beego.Router("/api/logs", &LogAPI{}) beego.Router("/api/logs", &LogAPI{})
beego.Router("/api/repositories/*", &RepositoryAPI{}, "put:Put") beego.Router("/api/repositories/*", &RepositoryAPI{}, "put:Put")
beego.Router("/api/repositories/*/labels", &RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository") beego.Router("/api/repositories/*/labels", &RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository")

View File

@ -15,7 +15,7 @@
package api package api
import ( import (
"net/http" "fmt"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
ldapUtils "github.com/vmware/harbor/src/common/utils/ldap" ldapUtils "github.com/vmware/harbor/src/common/utils/ldap"
@ -62,8 +62,7 @@ func (l *LdapAPI) Ping() {
if string(l.Ctx.Input.RequestBody) == "" { if string(l.Ctx.Input.RequestBody) == "" {
ldapSession, err = ldapUtils.LoadSystemLdapConfig() ldapSession, err = ldapUtils.LoadSystemLdapConfig()
if err != nil { if err != nil {
log.Errorf("Can't load system configuration, error: %v", err) l.HandleInternalServerError(fmt.Sprintf("Can't load system configuration, error: %v", err))
l.RenderError(http.StatusInternalServerError, pingErrorMessage)
return return
} }
err = ldapSession.ConnectionTest() err = ldapSession.ConnectionTest()
@ -73,9 +72,7 @@ func (l *LdapAPI) Ping() {
} }
if err != nil { if err != nil {
log.Errorf("ldap connect fail, error: %v", err) l.HandleInternalServerError(fmt.Sprintf("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)
return return
} }
} }
@ -84,24 +81,9 @@ func (l *LdapAPI) Ping() {
func (l *LdapAPI) Search() { func (l *LdapAPI) Search() {
var err error var err error
var ldapUsers []models.LdapUser var ldapUsers []models.LdapUser
var ldapConfs models.LdapConf ldapSession, err := ldapUtils.LoadSystemLdapConfig()
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)
}
if err = ldapSession.Open(); err != nil { if err = ldapSession.Open(); err != nil {
log.Errorf("can't Open ldap session, error: %v", err) l.HandleInternalServerError(fmt.Sprintf("Can't Open LDAP session, error: %v", err))
l.RenderError(http.StatusInternalServerError, canNotOpenLdapSession)
return return
} }
defer ldapSession.Close() defer ldapSession.Close()
@ -111,8 +93,7 @@ func (l *LdapAPI) Search() {
ldapUsers, err = ldapSession.SearchUser(searchName) ldapUsers, err = ldapSession.SearchUser(searchName)
if err != nil { if err != nil {
log.Errorf("Ldap search fail, error: %v", err) l.HandleInternalServerError(fmt.Sprintf("LDAP search fail, error: %v", err))
l.RenderError(http.StatusBadRequest, searchLdapFailMessage)
return return
} }
@ -132,14 +113,12 @@ func (l *LdapAPI) ImportUser() {
ldapFailedImportUsers, err := importUsers(ldapConfs, ldapImportUsers.LdapUIDList) ldapFailedImportUsers, err := importUsers(ldapConfs, ldapImportUsers.LdapUIDList)
if err != nil { if err != nil {
log.Errorf("Ldap import user fail, error: %v", err) l.HandleInternalServerError(fmt.Sprintf("LDAP import user fail, error: %v", err))
l.RenderError(http.StatusBadRequest, importUserError)
return return
} }
if len(ldapFailedImportUsers) > 0 { if len(ldapFailedImportUsers) > 0 {
log.Errorf("Import ldap user have internal error") l.HandleNotFound("Import LDAP user have internal error")
l.RenderError(http.StatusInternalServerError, importUserError)
l.Data["json"] = ldapFailedImportUsers l.Data["json"] = ldapFailedImportUsers
l.ServeJSON() l.ServeJSON()
return return
@ -153,12 +132,12 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models.
ldapSession, err := ldapUtils.LoadSystemLdapConfig() ldapSession, err := ldapUtils.LoadSystemLdapConfig()
if err != nil { 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 return nil, err
} }
if err = ldapSession.Open(); err != nil { 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() defer ldapSession.Close()
@ -182,7 +161,7 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models.
u.UID = tempUID u.UID = tempUID
u.Error = "failed_search_user" u.Error = "failed_search_user"
failedImportUser = append(failedImportUser, u) 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 continue
} }
@ -211,3 +190,22 @@ func importUsers(ldapConfs models.LdapConf, ldapImportUsers []string) ([]models.
return failedImportUser, nil 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()
}

206
src/ui/api/projectmember.go Normal file
View File

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

View File

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

146
src/ui/api/usergroup.go Normal file
View File

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

View File

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

View File

@ -68,7 +68,7 @@ func TestDefaultAuthenticate(t *testing.T) {
authHelper := DefaultAuthenticateHelper{} authHelper := DefaultAuthenticateHelper{}
m := models.AuthModel{} m := models.AuthModel{}
user, err := authHelper.Authenticate(m) user, err := authHelper.Authenticate(m)
if user != nil || err != nil { if user != nil || err == nil {
t.Fatal("Default implementation should return nil") t.Fatal("Default implementation should return nil")
} }
} }
@ -77,8 +77,26 @@ func TestDefaultOnBoardUser(t *testing.T) {
user := &models.User{} user := &models.User{}
authHelper := DefaultAuthenticateHelper{} authHelper := DefaultAuthenticateHelper{}
err := authHelper.OnBoardUser(user) err := authHelper.OnBoardUser(user)
if err != nil { if err == nil {
t.Fatal("Default implementation should return 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")
} }
} }

View File

@ -15,6 +15,7 @@
package auth package auth
import ( import (
"errors"
"fmt" "fmt"
"time" "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 // 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 // on the data record of the user
OnBoardUser(u *models.User) error 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 // Get user information from account repository
SearchUser(username string) (*models.User, error) 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 PostAuthenticate(u *models.User) error
} }
@ -67,26 +72,36 @@ type DefaultAuthenticateHelper struct {
// Authenticate ... // Authenticate ...
func (d *DefaultAuthenticateHelper) Authenticate(m models.AuthModel) (*models.User, error) { 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 // 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 // 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 // on the data record of the user
func (d *DefaultAuthenticateHelper) OnBoardUser(u *models.User) error { func (d *DefaultAuthenticateHelper) OnBoardUser(u *models.User) error {
return nil return errors.New("Not supported")
} }
//SearchUser - Get user information from account repository //SearchUser - Get user information from account repository
func (d *DefaultAuthenticateHelper) SearchUser(username string) (*models.User, error) { 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 { func (d *DefaultAuthenticateHelper) PostAuthenticate(u *models.User) error {
return nil 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) var registry = make(map[string]AuthenticateHelper)
// Register add different authenticators to registry map. // Register add different authenticators to registry map.
@ -128,9 +143,7 @@ func Login(m models.AuthModel) (*models.User, error) {
} }
return nil, err return nil, err
} }
err = authenticator.PostAuthenticate(user) err = authenticator.PostAuthenticate(user)
return user, err return user, err
} }
@ -166,6 +179,51 @@ func SearchUser(username string) (*models.User, error) {
return helper.SearchUser(username) 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 - // PostAuthenticate -
func PostAuthenticate(u *models.User) error { func PostAuthenticate(u *models.User) error {
helper, err := getHelper() helper, err := getHelper()

View File

@ -126,7 +126,7 @@ func TestSearchUser(t *testing.T) {
} }
func TestAuthenticateHelperOnboardUser(t *testing.T) { func TestAuthenticateHelperOnBoardUser(t *testing.T) {
user := models.User{ user := models.User{
Username: "test01", Username: "test01",
Realname: "test01", Realname: "test01",

View File

@ -19,7 +19,10 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao" "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/models"
ldapUtils "github.com/vmware/harbor/src/common/utils/ldap" ldapUtils "github.com/vmware/harbor/src/common/utils/ldap"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
@ -130,6 +133,45 @@ func (l *Auth) SearchUser(username string) (*models.User, error) {
return &user, nil 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 //PostAuthenticate -- If user exist in harbor DB, sync email address, if not exist, call OnBoardUser
func (l *Auth) PostAuthenticate(u *models.User) error { func (l *Auth) PostAuthenticate(u *models.User) error {

View File

@ -22,9 +22,11 @@ import (
"github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao" "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/models"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/test" "github.com/vmware/harbor/src/common/utils/test"
"github.com/vmware/harbor/src/ui/api"
"github.com/vmware/harbor/src/ui/auth" "github.com/vmware/harbor/src/ui/auth"
uiConfig "github.com/vmware/harbor/src/ui/config" uiConfig "github.com/vmware/harbor/src/ui/config"
) )
@ -46,7 +48,7 @@ var adminServerLdapTestConfig = map[string]interface{}{
common.LDAPBaseDN: "dc=example,dc=com", common.LDAPBaseDN: "dc=example,dc=com",
common.LDAPUID: "uid", common.LDAPUID: "uid",
common.LDAPFilter: "", common.LDAPFilter: "",
common.LDAPScope: 3, common.LDAPScope: 2,
common.LDAPTimeout: 30, common.LDAPTimeout: 30,
// config.TokenServiceURL: "", // config.TokenServiceURL: "",
// config.RegistryURL: "", // config.RegistryURL: "",
@ -64,6 +66,10 @@ var adminServerLdapTestConfig = map[string]interface{}{
common.CfgExpiration: 5, common.CfgExpiration: 5,
// config.JobLogDir: "/var/log/jobs", // 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) { func TestMain(m *testing.M) {
@ -102,6 +108,24 @@ func TestMain(m *testing.M) {
log.Fatalf("failed to initialize database: %v", err) 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() retCode := m.Run()
os.Exit(retCode) 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{ user := &models.User{
Username: "sample", Username: "sample",
Email: "sample@example.com", Email: "sample@example.com",
@ -186,7 +210,7 @@ func TestOnboardUser(t *testing.T) {
assert.Equal(t, "sample@example.com", user.Email) assert.Equal(t, "sample@example.com", user.Email)
} }
func TestOnboardUser_02(t *testing.T) { func TestOnBoardUser_02(t *testing.T) {
user := &models.User{ user := &models.User{
Username: "sample02", Username: "sample02",
Realname: "Sample02", Realname: "Sample02",
@ -204,7 +228,7 @@ func TestOnboardUser_02(t *testing.T) {
dao.CleanUser(int64(user.UserID)) dao.CleanUser(int64(user.UserID))
} }
func TestOnboardUser_03(t *testing.T) { func TestOnBoardUser_03(t *testing.T) {
user := &models.User{ user := &models.User{
Username: "sample03@example.com", Username: "sample03@example.com",
Realname: "Sample03", Realname: "Sample03",
@ -222,7 +246,7 @@ func TestOnboardUser_03(t *testing.T) {
dao.CleanUser(int64(user.UserID)) dao.CleanUser(int64(user.UserID))
} }
func TestAuthenticateHelperOnboardUser(t *testing.T) { func TestAuthenticateHelperOnBoardUser(t *testing.T) {
user := models.User{ user := models.User{
Username: "test01", Username: "test01",
Realname: "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) { func TestAuthenticateHelperSearchUser(t *testing.T) {
user, err := auth.SearchUser("test") user, err := auth.SearchUser("test")
@ -307,3 +346,70 @@ func TestPostAuthentication(t *testing.T) {
assert.EqualValues("test003@example.com", dbUser.Email) assert.EqualValues("test003@example.com", dbUser.Email)
dao.CleanUser(int64(dbUser.UserID)) 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)
}
}

View File

@ -47,6 +47,7 @@ func initRouters() {
//API: //API:
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectUserMemberAPI{}) 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/", &api.ProjectAPI{}, "head:Head")
beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{}) beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{})
@ -54,8 +55,10 @@ func initRouters() {
beego.Router("/api/users", &api.UserAPI{}, "get:List;post:Post") 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([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole") 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/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/ldap/users/import", &api.LdapAPI{}, "post:ImportUser")
beego.Router("/api/email/ping", &api.EmailAPI{}, "post:Ping") beego.Router("/api/email/ping", &api.EmailAPI{}, "post:Ping")
} }

View File

@ -36,6 +36,24 @@ member: cn=mike05,ou=people,dc=example,dc=com
objectclass: groupOfNames objectclass: groupOfNames
objectclass: top 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 # User belongs to harbor_user
dn: cn=mike,ou=people,dc=example,dc=com dn: cn=mike,ou=people,dc=example,dc=com
cn: mike cn: mike