diff --git a/controller/group_controller.go b/controller/group_controller.go index 53b800e..d152e0b 100644 --- a/controller/group_controller.go +++ b/controller/group_controller.go @@ -104,3 +104,11 @@ func (m *GroupController) SyncFeiShuDepts(c *gin.Context) { return logic.FeiShu.SyncFeiShuDepts(c, req) }) } + +//同步原ldap部门信息 +func (m *GroupController) SyncOpenLdapDepts(c *gin.Context) { + req := new(request.SyncOpenLdapDeptsReq) + Run(c, req, func() (interface{}, interface{}) { + return logic.OpenLdap.SyncOpenLdapDepts(c, req) + }) +} diff --git a/controller/user_controller.go b/controller/user_controller.go index 97a2f8a..b38f430 100644 --- a/controller/user_controller.go +++ b/controller/user_controller.go @@ -88,3 +88,11 @@ func (uc UserController) SyncFeiShuUsers(c *gin.Context) { return logic.FeiShu.SyncFeiShuUsers(c, req) }) } + +// 同步ldap用户信息 +func (uc UserController) SyncOpenLdapUsers(c *gin.Context) { + req := new(request.SyncOpenLdapUserReq) + Run(c, req, func() (interface{}, interface{}) { + return logic.OpenLdap.SyncOpenLdapUsers(c, req) + }) +} diff --git a/logic/a_logic.go b/logic/a_logic.go index 77415cc..2499406 100644 --- a/logic/a_logic.go +++ b/logic/a_logic.go @@ -24,6 +24,7 @@ var ( DingTalk = &DingTalkLogic{} WeCom = &WeComLogic{} FeiShu = &FeiShuLogic{} + OpenLdap = &OpenLdapLogic{} Base = &BaseLogic{} FieldRelation = &FieldRelationLogic{} diff --git a/logic/openldap_logic.go b/logic/openldap_logic.go new file mode 100644 index 0000000..2a010ee --- /dev/null +++ b/logic/openldap_logic.go @@ -0,0 +1,179 @@ +package logic + +import ( + "fmt" + + "github.com/eryajf/go-ldap-admin/model" + "github.com/eryajf/go-ldap-admin/public/client/openldap" + + "github.com/eryajf/go-ldap-admin/public/tools" + "github.com/eryajf/go-ldap-admin/service/isql" + "github.com/gin-gonic/gin" +) + +type OpenLdapLogic struct { +} + +//通过ldap获取部门信息 +func (d *OpenLdapLogic) SyncOpenLdapDepts(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) { + // 1.获取所有部门 + depts, err := openldap.GetAllDepts() + if err != nil { + return nil, tools.NewOperationError(fmt.Errorf("获取ldap部门列表失败:%s", err.Error())) + } + // 2.将部门这个数组进行拆分,一组是父ID为根的,一组是父ID不为根的 + var firstDepts []*openldap.Dept // 父ID为根的部门 + var otherDepts []*openldap.Dept // 父ID不为根的部门 + for _, dept := range depts { + if dept.ParentId == "1" { + firstDepts = append(firstDepts, dept) + } else { + otherDepts = append(otherDepts, dept) + } + } + // 3.先写父ID为根的,再写父ID不为根的 + for _, dept := range firstDepts { + err := d.AddDepts(&model.Group{ + GroupName: dept.Name, + Remark: dept.Remark, + Creator: "system", + GroupType: "cn", + SourceDeptId: dept.Id, + Source: "openldap", + SourceDeptParentId: dept.ParentId, + GroupDN: dept.DN, + }) + if err != nil { + return nil, tools.NewOperationError(fmt.Errorf("SyncOpenLdapDepts添加根部门失败:%s", err.Error())) + } + } + + for _, dept := range otherDepts { + err := d.AddDepts(&model.Group{ + GroupName: dept.Name, + Remark: dept.Remark, + Creator: "system", + GroupType: "cn", + SourceDeptId: dept.Id, + Source: "openldap", + SourceDeptParentId: dept.ParentId, + GroupDN: dept.DN, + }) + if err != nil { + return nil, tools.NewOperationError(fmt.Errorf("SyncOpenLdapDepts添加其他部门失败:%s", err.Error())) + } + } + return nil, nil +} + +// AddGroup 添加部门数据 +func (d OpenLdapLogic) AddDepts(group *model.Group) error { + // 判断部门名称是否存在 + parentGroup := new(model.Group) + err := isql.Group.Find(tools.H{"source_dept_id": group.SourceDeptParentId}, parentGroup) + if err != nil { + return tools.NewMySqlError(fmt.Errorf("查询父级部门失败:%s", err.Error())) + } + if !isql.Group.Exist(tools.H{"source_dept_id": group.SourceDeptId}) { + group.ParentId = parentGroup.ID + // 在数据库中创建组 + err = isql.Group.Add(group) + if err != nil { + return err + } + } + return nil +} + +//根据现有数据库同步到的部门信息,开启用户同步 +func (d OpenLdapLogic) SyncOpenLdapUsers(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) { + // 1.获取ldap用户列表 + staffs, err := openldap.GetAllUsers() + if err != nil { + return nil, tools.NewOperationError(fmt.Errorf("获取ldap用户列表失败:%s", err.Error())) + } + // 2.遍历用户,开始写入 + for _, staff := range staffs { + groupIds, err := isql.Group.DeptIdsToGroupIds(staff.DepartmentIds) + if err != nil { + return nil, tools.NewMySqlError(fmt.Errorf("将部门ids转换为内部部门id失败:%s", err.Error())) + } + // 根据角色id获取角色 + roles, err := isql.Role.GetRolesByIds([]uint{2}) + if err != nil { + return nil, tools.NewValidatorError(fmt.Errorf("根据角色ID获取角色信息失败:%s", err.Error())) + } + // 入库 + err = d.AddUsers(&model.User{ + Username: staff.Name, + Nickname: staff.DisplayName, + GivenName: staff.GivenName, + Mail: staff.Mail, + JobNumber: staff.EmployeeNumber, + Mobile: staff.Mobile, + PostalAddress: staff.PostalAddress, + Departments: staff.BusinessCategory, + Position: staff.DepartmentNumber, + Introduction: staff.CN, + Creator: "system", + Source: "openldap", + DepartmentId: tools.SliceToString(groupIds, ","), + SourceUserId: staff.Name, + SourceUnionId: staff.Name, + Roles: roles, + UserDN: staff.DN, + }) + if err != nil { + return nil, tools.NewOperationError(fmt.Errorf("SyncOpenLdapUsers写入用户失败:%s", err.Error())) + } + } + return nil, nil +} + +// AddUser 添加用户数据 +func (d OpenLdapLogic) AddUsers(user *model.User) error { + // 根据 unionid 查询用户,不存在则创建 + if !isql.User.Exist(tools.H{"source_union_id": user.SourceUnionId}) { + if user.Departments == "" { + user.Departments = "默认:研发中心" + } + if user.GivenName == "" { + user.GivenName = user.Nickname + } + if user.PostalAddress == "" { + user.PostalAddress = "默认:地球" + } + if user.Position == "" { + user.Position = "默认:技术" + } + if user.Introduction == "" { + user.Introduction = user.Nickname + } + if user.JobNumber == "" { + user.JobNumber = "未启用" + } + // 先将用户添加到MySQL + err := isql.User.Add(user) + if err != nil { + return tools.NewMySqlError(fmt.Errorf("向MySQL创建用户失败:" + err.Error())) + } + + // 获取用户将要添加的分组 + groups, err := isql.Group.GetGroupByIds(tools.StringToSlice(user.DepartmentId, ",")) + if err != nil { + return tools.NewMySqlError(fmt.Errorf("根据部门ID获取部门信息失败" + err.Error())) + } + for _, group := range groups { + if group.GroupDN[:3] == "ou=" { + continue + } + // 先将用户和部门信息维护到MySQL + err := isql.Group.AddUserToGroup(group, []model.User{*user}) + if err != nil { + return tools.NewMySqlError(fmt.Errorf("向MySQL添加用户到分组关系失败:" + err.Error())) + } + } + return nil + } + return nil +} diff --git a/model/group.go b/model/group.go index 227d72f..d269134 100644 --- a/model/group.go +++ b/model/group.go @@ -6,8 +6,8 @@ import ( type Group struct { gorm.Model - GroupName string `gorm:"type:varchar(20);comment:'分组名称'" json:"groupName"` - Remark string `gorm:"type:varchar(100);comment:'分组中文说明'" json:"remark"` + GroupName string `gorm:"type:varchar(128);comment:'分组名称'" json:"groupName"` + Remark string `gorm:"type:varchar(128);comment:'分组中文说明'" json:"remark"` Creator string `gorm:"type:varchar(20);comment:'创建人'" json:"creator"` GroupType string `gorm:"type:varchar(20);comment:'分组类型:cn、ou'" json:"groupType"` Users []*User `gorm:"many2many:group_users" json:"users"` diff --git a/model/request/group_req.go b/model/request/group_req.go index 208702e..b18a759 100644 --- a/model/request/group_req.go +++ b/model/request/group_req.go @@ -106,3 +106,7 @@ type SyncWeComDeptsReq struct { // SyncFeiShuDeptsReq 同步飞书部门信息 type SyncFeiShuDeptsReq struct { } + +// SyncOpenLdapDeptsReq 同步原ldap部门信息 +type SyncOpenLdapDeptsReq struct { +} diff --git a/model/request/user_req.go b/model/request/user_req.go index 94b2943..2f1582e 100644 --- a/model/request/user_req.go +++ b/model/request/user_req.go @@ -116,6 +116,10 @@ type SyncWeComUserReq struct { type SyncFeiShuUserReq struct { } +// SyncOpenLdapUserReq 同步ldap用户信息 +type SyncOpenLdapUserReq struct { +} + // UserListReq 获取用户列表结构体 type UserListReq struct { Username string `json:"username" form:"username"` diff --git a/public/client/openldap/openldap.go b/public/client/openldap/openldap.go new file mode 100644 index 0000000..2f90150 --- /dev/null +++ b/public/client/openldap/openldap.go @@ -0,0 +1,143 @@ +package openldap + +import ( + "fmt" + "strings" + + "github.com/eryajf/go-ldap-admin/config" + "github.com/eryajf/go-ldap-admin/public/common" + ldap "github.com/go-ldap/ldap/v3" +) + +type Dept struct { + DN string `json:"dn"` + Id string `json:"id"` // 部门ID + Name string `json:"name"` // 部门名称拼音 + Remark string `json:"remark"` // 部门中文名 + ParentId string `json:"parentid"` // 父部门ID +} + +type User struct { + Name string `json:"name"` + DN string `json:"dn"` + CN string `json:"cn"` + SN string `json:"sn"` + Mobile string `json:"mobile"` + BusinessCategory string `json:"businessCategory"` // 业务类别,部门名字 + DepartmentNumber string `json:"departmentNumber"` // 部门编号,此处可以存放员工的职位 + Description string `json:"description"` // 描述 + DisplayName string `json:"displayName"` // 展示名字,可以是中文名字 + Mail string `json:"mail"` // 邮箱 + EmployeeNumber string `json:"employeeNumber"` // 员工工号 + GivenName string `json:"givenName"` // 给定名字,如果公司有花名,可以用这个字段 + PostalAddress string `json:"postalAddress"` // 家庭住址 + DepartmentIds []string `json:"department_ids"` +} + +// GetAllDepts 获取所有部门 +func GetAllDepts() (ret []*Dept, err error) { + // Construct query request + searchRequest := ldap.NewSearchRequest( + config.Conf.Ldap.BaseDN, // This is basedn, we will start searching from this node. + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, // Here several parameters are respectively scope, derefAliases, sizeLimit, timeLimit, typesOnly + "(&(objectClass=*))", // This is Filter for LDAP query + []string{}, // Here are the attributes returned by the query, provided as an array. If empty, all attributes are returned + nil, + ) + // Search through ldap built-in search + sr, err := common.LDAP.Search(searchRequest) + if err != nil { + return ret, err + } + // Refers to the entry that returns data. If it is greater than 0, the interface returns normally. + if len(sr.Entries) > 0 { + for _, v := range sr.Entries { + if v.DN == config.Conf.Ldap.BaseDN || v.DN == config.Conf.Ldap.AdminDN || strings.Contains(v.DN, config.Conf.Ldap.UserDN) { + continue + } + var ele Dept + ele.DN = v.DN + ele.Name = strings.Split(strings.Split(v.DN, ",")[0], "=")[1] + ele.Id = strings.Split(strings.Split(v.DN, ",")[0], "=")[1] + ele.Remark = v.GetAttributeValue("description") + if len(strings.Split(v.DN, ","))-len(strings.Split(config.Conf.Ldap.BaseDN, ",")) == 1 { + ele.ParentId = "openldap_1" + } else { + ele.ParentId = strings.Split(strings.Split(v.DN, ",")[1], "=")[1] + } + ret = append(ret, &ele) + } + } + return +} + +// GetAllUsers 获取所有员工信息 +func GetAllUsers() (ret []*User, err error) { + // Construct query request + searchRequest := ldap.NewSearchRequest( + config.Conf.Ldap.BaseDN, // This is basedn, we will start searching from this node. + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, // Here several parameters are respectively scope, derefAliases, sizeLimit, timeLimit, typesOnly + "(&(objectClass=*))", // This is Filter for LDAP query + []string{}, // Here are the attributes returned by the query, provided as an array. If empty, all attributes are returned + nil, + ) + // Search through ldap built-in search + sr, err := common.LDAP.Search(searchRequest) + if err != nil { + return ret, err + } + // Refers to the entry that returns data. If it is greater than 0, the interface returns normally. + if len(sr.Entries) > 0 { + for _, v := range sr.Entries { + if v.DN == config.Conf.Ldap.UserDN || !strings.Contains(v.DN, config.Conf.Ldap.UserDN) { + continue + } + name := strings.Split(strings.Split(v.DN, ",")[0], "=")[1] + deptIds, err := GetUserDeptIds(v.DN) + if err != nil { + return ret, err + } + ret = append(ret, &User{ + Name: name, + DN: v.DN, + CN: v.GetAttributeValue("cn"), + SN: v.GetAttributeValue("sn"), + Mobile: v.GetAttributeValue("mobile"), + BusinessCategory: v.GetAttributeValue("businessCategory"), + DepartmentNumber: v.GetAttributeValue("departmentNumber"), + Description: v.GetAttributeValue("description"), + DisplayName: v.GetAttributeValue("displayName"), + Mail: v.GetAttributeValue("mail"), + EmployeeNumber: v.GetAttributeValue("employeeNumber"), + GivenName: v.GetAttributeValue("givenName"), + PostalAddress: v.GetAttributeValue("postalAddress"), + DepartmentIds: deptIds, + }) + } + } + return +} + +// GetUserDeptIds 获取用户所在的部门 +func GetUserDeptIds(udn string) (ret []string, err error) { + // Construct query request + searchRequest := ldap.NewSearchRequest( + config.Conf.Ldap.BaseDN, // This is basedn, we will start searching from this node. + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, // Here several parameters are respectively scope, derefAliases, sizeLimit, timeLimit, typesOnly + fmt.Sprintf("(|(Member=%s)(uniqueMember=%s))", udn, udn), // This is Filter for LDAP query + []string{}, // Here are the attributes returned by the query, provided as an array. If empty, all attributes are returned + nil, + ) + // Search through ldap built-in search + sr, err := common.LDAP.Search(searchRequest) + if err != nil { + return ret, err + } + // Refers to the entry that returns data. If it is greater than 0, the interface returns normally. + if len(sr.Entries) > 0 { + for _, v := range sr.Entries { + ret = append(ret, strings.Split(strings.Split(v.DN, ",")[0], "=")[1]) + } + } + return ret, nil +} diff --git a/public/common/init_mysql_data.go b/public/common/init_mysql_data.go index 554006b..1402d3d 100644 --- a/public/common/init_mysql_data.go +++ b/public/common/init_mysql_data.go @@ -360,6 +360,13 @@ func InitData() { Remark: "从飞书拉取用户信息", Creator: "系统", }, + { + Method: "POST", + Path: "/user/syncOpenLdapUsers", + Category: "user", + Remark: "从openldap拉取用户信息", + Creator: "系统", + }, { Method: "GET", Path: "/group/list", @@ -444,6 +451,13 @@ func InitData() { Remark: "从飞书拉取部门信息", Creator: "系统", }, + { + Method: "POST", + Path: "/group/syncOpenLdapDepts", + Category: "group", + Remark: "从openldap拉取部门信息", + Creator: "系统", + }, { Method: "GET", Path: "/role/list", @@ -688,6 +702,18 @@ func InitData() { groups := []model.Group{ { Model: gorm.Model{ID: 1}, + GroupName: "openldaproot", + Remark: "ldap根部门", + Creator: "system", + GroupType: "ou", + ParentId: 0, + SourceDeptId: "openldap_1", + Source: "openldap", + SourceDeptParentId: "openldap_0", + GroupDN: fmt.Sprintf("ou=%s,%s", "openldaproot", config.Conf.Ldap.BaseDN), + }, + { + Model: gorm.Model{ID: 2}, GroupName: config.Conf.DingTalk.Flag + "root", Remark: "钉钉根部门", Creator: "system", @@ -699,7 +725,7 @@ func InitData() { GroupDN: fmt.Sprintf("ou=%s,%s", config.Conf.DingTalk.Flag+"root", config.Conf.Ldap.BaseDN), }, { - Model: gorm.Model{ID: 2}, + Model: gorm.Model{ID: 3}, GroupName: "wecomroot", Remark: "企业微信根部门", Creator: "system", @@ -711,7 +737,7 @@ func InitData() { GroupDN: fmt.Sprintf("ou=%s,%s", config.Conf.WeCom.Flag+"root", config.Conf.Ldap.BaseDN), }, { - Model: gorm.Model{ID: 3}, + Model: gorm.Model{ID: 4}, GroupName: config.Conf.FeiShu.Flag + "root", Remark: "飞书根部门", Creator: "system", diff --git a/routes/group_routes.go b/routes/group_routes.go index d5c4fb6..438b833 100644 --- a/routes/group_routes.go +++ b/routes/group_routes.go @@ -29,6 +29,7 @@ func InitGroupRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) g group.POST("/syncDingTalkDepts", controller.Group.SyncDingTalkDepts) // 同步部门 group.POST("/syncWeComDepts", controller.Group.SyncWeComDepts) // 同步部门 group.POST("/syncFeiShuDepts", controller.Group.SyncFeiShuDepts) // 同步部门 + group.POST("/syncOpenLdapDepts", controller.Group.SyncOpenLdapDepts) // 同步部门 } return r diff --git a/routes/user_routes.go b/routes/user_routes.go index 4f6e735..f359903 100644 --- a/routes/user_routes.go +++ b/routes/user_routes.go @@ -27,6 +27,7 @@ func InitUserRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) gi user.POST("/syncDingTalkUsers", controller.User.SyncDingTalkUsers) // 同步用户 user.POST("/syncWeComUsers", controller.User.SyncWeComUsers) // 同步用户 user.POST("/syncFeiShuUsers", controller.User.SyncFeiShuUsers) // 同步用户 + user.POST("/syncOpenLdapUsers", controller.User.SyncOpenLdapUsers) // 同步用户 } return r }