Przeglądaj źródła

feat: LDAP用户新增和修改

tuonian 1 miesiąc temu
rodzic
commit
3a82f47103

+ 1 - 7
frontend/src/api/ldap.ts

@@ -33,6 +33,7 @@ export namespace LDAP {
         givenName?: string
         sn?: string
         objectClass?: string
+        organize?: string
     }
 
     export type VerifyData = {
@@ -93,13 +94,6 @@ export const LDAPApis = {
     saveUser: (user: Partial<LDAP.User>) => {
         return request.post<BaseResp<LDAP.User>>(`/ldap/user/save`, user)
     },
-    /**
-     * 新增用户
-     * @param user
-     */
-    add: (user: Partial<LDAP.User>) => {
-        return request.post<BaseResp<LDAP.User>>(`/ldap/user/add`, user)
-    },
     syncUsers: (search: any) => {
         return request.post<BaseResp<{ count: number}>>('/ldap/user/sync', search)
     }

+ 14 - 12
frontend/src/pages/layout/MainLayout.tsx

@@ -1,9 +1,8 @@
 import {Breadcrumb, Dropdown, Layout, Menu, MenuProps, Space} from 'antd';
 import './layout.less'
-import {useAppDispatch, useAppSelector} from "../../store";
-import {routeActions} from "../../store/slice/route.ts";
+import {useAppSelector} from "../../store";
 import {Outlet, useMatches, useNavigate} from "react-router";
-import {useEffect, useMemo} from "react";
+import {useEffect, useMemo, useState} from "react";
 import {Link} from "react-router-dom";
 import {DownOutlined} from "@ant-design/icons";
 import {ModifyPassword} from "../user/components/password";
@@ -18,9 +17,7 @@ const {Header, Content} = Layout;
 export const MainLayout = () => {
 
     const navList = useAppSelector(state => state.user.navList) || []
-    const nav = useAppSelector(state => state.route.nav)
-
-    const dispatch = useAppDispatch()
+    const [nav,setNav] = useState('')
     const navigate = useNavigate()
 
     const matches = useMatches()
@@ -31,11 +28,15 @@ export const MainLayout = () => {
         .reverse()
 
     useEffect(() => {
-        console.log('matches', matches)
-    }, [matches]);
+        if (!nav && matches.length){
+            setNav(matches[0].pathname)
+        }
+        console.log('matches', matches, nav)
+    }, [matches, nav]);
+
 
-    const setNav = (key: string) => {
-        dispatch(routeActions.setNav(key))
+    const handleSetNav = (key: string) => {
+        setNav(key)
         navigate(key)
     }
 
@@ -71,10 +72,11 @@ export const MainLayout = () => {
                 <Menu
                     theme="dark"
                     mode="horizontal"
-                    defaultSelectedKeys={[nav]}
+                    selectedKeys={[nav]}
+                    activeKey={nav}
                     items={navList}
                     style={{flex: 1, minWidth: 0}}
-                    onClick={event => setNav(event.key)}
+                    onClick={event => handleSetNav(event.key)}
                 />
                 <div style={{flex: 1}} />
                 <Dropdown menu={personMenus}>

+ 9 - 4
frontend/src/pages/ldap/server/index.tsx

@@ -58,6 +58,11 @@ const columns: CurdColumn[] = [
         required: false,
         width: 180
     },
+    {
+        title: '组织ObjectClass',
+        key: 'organizeClass',
+        type: 'string',
+    },
     {
         key: 'remark',
         title: '备注',
@@ -143,7 +148,7 @@ export const Server = () => {
                 setSearchUsers(res.data || [])
                 Message.success('操作成功!')
             })
-            .finally(()=>{
+            .finally(() => {
                 setLoading(false)
             })
     }
@@ -176,9 +181,9 @@ export const Server = () => {
                 <Form.Item label="搜索结果">
                     <div className="search-results">
                         <Table dataSource={searchUsers} pagination={false}>
-                            <Table.Column dataIndex="account" title="uid" />
-                            <Table.Column dataIndex="userName" title="姓名" />
-                            <Table.Column dataIndex="dn" title="DN" />
+                            <Table.Column dataIndex="account" title="uid"/>
+                            <Table.Column dataIndex="userName" title="姓名"/>
+                            <Table.Column dataIndex="dn" title="DN"/>
                         </Table>
                     </div>
                 </Form.Item>

+ 38 - 11
frontend/src/pages/ldap/user/list/index.tsx

@@ -7,7 +7,7 @@ import './index.less'
 import {Link} from "react-router-dom";
 import {useRouteLoaderData} from "react-router";
 import {SyncOutlined} from "@ant-design/icons";
-import {isNullOrTrue, Message} from "auto-antd";
+import {isNull, isNullOrTrue, Message} from "auto-antd";
 import {UserAttributesText} from "../components/UserAttributes.tsx";
 import {safeParse} from "../../../../utils/json.ts";
 
@@ -102,6 +102,13 @@ const columns: CurdColumn[] = [
         hidden: true,
         required: false,
     },
+    {
+        key: 'lastSyncDate',
+        title: '同步时间',
+        type: 'string',
+        editable: false,
+        addable: false,
+    },
     {
         key: 'remark',
         title: '备注',
@@ -115,9 +122,11 @@ const delKeys = ['cn','mail','userPassword','uid']
 
 const parseUser = (user: LDAP.User) => {
     const attrs = safeParse(user.attributes, [])
-    console.log('attrs', attrs)
     const attrList = []
     for (const attr of attrs) {
+        if (!attr.Name){
+            continue
+        }
         attr.Value = Array.isArray(attr.Values) ? attr.Values[0] : undefined
         if (attr.Name == 'objectClass'){
             attr.Value = attr.Values || []
@@ -128,6 +137,12 @@ const parseUser = (user: LDAP.User) => {
             attrList.push(attr)
         }
     }
+    if (!user.organize){
+        const index = user.dn.indexOf('ou=')
+        user.organize = user.dn.substring(index)
+    }
+    console.log('attrs', user)
+
     return {
         ...user,
         attributes: attrList,
@@ -137,6 +152,9 @@ const parseUser = (user: LDAP.User) => {
 const stringifyUser = (user: Partial<LDAP.User>) => {
     const attributes = []
     for (const attr of (user.attributes as any[])) {
+        if (!attr.Name){
+            continue
+        }
         attributes.push({
             Name: attr.Name,
             Values: isNullOrTrue(attr.Value) ? [attr.Values] : []
@@ -144,13 +162,27 @@ const stringifyUser = (user: Partial<LDAP.User>) => {
     }
     for (const k of toObjKeys){
         const v = (user as any)[k]
-        if (isNullOrTrue(v)) {
+        if (!isNull(v)) {
             attributes.push({
                 Name: k,
-                Values: [v]
+                Values: Array.isArray(v) ? v: [v]
             })
         }
     }
+    const cn = `${user.givenName}${user.sn}`
+    attributes.push({
+        Name: 'uid',
+        Values: [user.account]
+    })
+    attributes.push({
+        Name: 'mail',
+        Values: [user.mail]
+    })
+    attributes.push({
+        Name: 'cn',
+        Values: [cn],
+    })
+    user.dn = `cn=${cn},${user.organize}`
     return {
         ...user,
         attributes: JSON.stringify(attributes),
@@ -202,13 +234,8 @@ export const List = () => {
 
     const onSaveUser = (data: Partial<LDAP.User>) => {
         const user = stringifyUser(data)
-        if (data.id){
-            return LDAPApis.saveUser(user)
-                .then(res => {
-                    return res.data.data as LDAP.User;
-                })
-        }
-        return LDAPApis.add(user)
+        user.serverKey = server.key
+        return LDAPApis.saveUser(user)
             .then(res => {
                 return res.data.data as LDAP.User;
             })

+ 9 - 1
frontend/src/routes/index.tsx

@@ -32,10 +32,19 @@ export const RouteWrapper = ({Component, ...props}: RouteWrapperProps) => {
 
     const [loading, setLoading] = useState(false)
     const user = useAppSelector(state => state.user.user)
+    const isLogin = useAppSelector(state => state.user.isLogin)
+
     const navigate = useNavigate()
     const location = useLocation()
     const dispatch = useAppDispatch()
 
+
+    useEffect(() => {
+        if (!isLogin){
+           navigate('/login')
+        }
+    }, [isLogin]);
+
     const fetchUser = () => {
         setLoading(true);
         LoginApis.userinfo().then(({data}) => {
@@ -105,7 +114,6 @@ const router = createHashRouter([
 ])
 
 export const MyRouter = () => {
-
     return (
         <SSOWrapper>
             <RouterProvider router={router} />

+ 4 - 7
frontend/src/store/slice/route.ts

@@ -1,14 +1,12 @@
-import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { createSlice } from '@reduxjs/toolkit';
 import {MenuProps} from "antd";
 
 export type IRouteState = {
-  nav: string
   navList: MenuProps['items'],
   routes: {[key: string]: MenuProps['items']}
 };
 
 const initialState: IRouteState = {
-  nav: '/nginx',
   navList: [
     {
       key: '/nginx',
@@ -33,12 +31,11 @@ const routeSlice = createSlice({
   name: 'route',
   initialState,
   reducers: {
-    setNav(state, action: PayloadAction<string>) {
-      state.nav = action.payload;
-    },
+    // setNav(state, action: PayloadAction<string>) {
+    //   state.nav = action.payload;
+    // },
   },
 });
 
-export const { setNav } = routeSlice.actions;
 export default routeSlice.reducer;
 export const routeActions = routeSlice.actions;

+ 1 - 0
server/db/db.go

@@ -41,6 +41,7 @@ func Init() {
 
 	orm.RegisterModel(new(models.LdapServer))
 	orm.RegisterModel(new(models.LdapUser))
+	orm.RegisterModel(new(models.LdapOrganize))
 
 	orm.RunSyncdb("default", false, true)
 

+ 12 - 4
server/models/ldap.go

@@ -22,10 +22,10 @@ type LdapServer struct {
 // LdapUser User 用户表
 // https://blog.csdn.net/wzjking0929/article/details/81153206
 type LdapUser struct {
-	Id int `orm:"pk;auto" json:"id"`
+	Id  int    `orm:"pk;auto" json:"id"`
+	Uid string `json:"uid"`
 	// 用户账号,唯一标识, Uid
-	Uid          string    `orm:"unique" json:"uid"`
-	Account      string    `json:"account"` // 即DN,eg. cn=test,dc=xxxx,dc=cn
+	Account      string    `orm:"unique" json:"account"` // 即DN,eg. cn=test,dc=xxxx,dc=cn
 	UserName     string    `json:"userName"`
 	Mail         string    `json:"mail"`
 	DN           string    `orm:"column(db)" json:"dn"`
@@ -34,6 +34,14 @@ type LdapUser struct {
 	Password     string    `json:"password"`
 	Remark       string    `json:"remark"`
 	ServerKey    string    `json:"serverKey"`
-	LastSyncDate time.Time `orm:"type(datetime)" json:"lastSyncDate"` // 最后一次同步的时间
+	LastSyncDate time.Time `orm:"null;type(datetime)" json:"lastSyncDate"` // 最后一次同步的时间
 	Organize     string    `orm:"default('');type(text)" json:"organize"`
 }
+
+type LdapOrganize struct {
+	Id          int    `orm:"pk;auto" json:"id"`
+	ServerKey   string `orm:"size(255)" json:"serverKey"`
+	DN          string `orm:"unique;column(dn)" json:"dn"`
+	ObjectClass string `orm:"size(255)" json:"objectClass"`
+	Name        string `orm:"size(255)" json:"ou"`
+}

+ 59 - 47
server/modules/ldap/client.go

@@ -90,26 +90,8 @@ func (c *Client) Bind(username string, password string, isAdmin bool) error {
 	return nil
 }
 
-func createUser(entry *ldap.Entry) models.LdapUser {
-	user := models.LdapUser{
-		Account:  entry.GetAttributeValue("uid"),
-		DN:       entry.DN,
-		Password: entry.GetAttributeValue("userPassword"),
-		UserName: entry.GetAttributeValue("cn"),
-		Mail:     entry.GetAttributeValue("mail"),
-	}
-	jsonBytes, err := json.Marshal(entry.Attributes)
-	if err != nil {
-		logs.Error("marshal fail : %v", err)
-		user.Remark = "attributes marshal fail: " + err.Error()
-	} else {
-		user.Attributes = string(jsonBytes)
-	}
-	return user
-}
-
 // Search 搜索用户 eg. (&(objectClass=organizationalPerson))
-func (c *Client) Search(filter string) ([]models.LdapUser, error) {
+func (c *Client) Search(filter string) ([]*ldap.Entry, error) {
 
 	if filter == "" {
 		filter = "(objectClass=*)"
@@ -127,13 +109,28 @@ func (c *Client) Search(filter string) ([]models.LdapUser, error) {
 		logs.Error("search fail : %v", err)
 		return nil, err
 	}
-	var users []models.LdapUser
-	for _, entry := range sr.Entries {
-		user := createUser(entry)
-		user.ServerKey = c.ServerKey
-		users = append(users, user)
+	return sr.Entries, nil
+}
+
+// SearchByAccount 指定账号搜索用户
+func (c *Client) SearchByAccount(account string) (*ldap.Entry, error) {
+	filter := fmt.Sprintf("(&(objectClass=*)(uid=%s))", account)
+	searchRequest := ldap.NewSearchRequest(
+		c.BaseDN, // The base dn to search
+		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+		filter,        // The filter to apply
+		[]string{"*"}, // A list attributes to retrieve,"dn", "cn", "objectClass",
+		nil,
+	)
+	sr, err := c.Conn.Search(searchRequest)
+	if err != nil {
+		logs.Error("search fail : %v", err)
+		return nil, err
 	}
-	return users, nil
+	if len(sr.Entries) != 1 {
+		logs.Error("no account found for: %v", account)
+	}
+	return sr.Entries[0], nil
 }
 
 // 通过管理员修改密码,而非自行修改密码
@@ -173,46 +170,61 @@ func (c *Client) Modify() error {
 }
 
 // Add 新增用户
+// 搜索指定账号:(&(objectClass=*)(uid=%s))
 func (c *Client) Add(user *models.LdapUser) error {
-	var attrs []ldap.Attribute
-	err := json.Unmarshal([]byte(user.Attributes), &attrs)
+	entries, err := c.Search(fmt.Sprintf("(&(objectClass=*)(uid=%s))", user.Account))
+	if err == nil {
+		entry := entries[0]
+		if entry.DN != user.DN {
+			logs.Warn("DN not match: {}, {}", entry.DN, user.DN)
+			return errors.New("已存在该账号,但DN不相同!")
+		}
+	}
+
+	var attrs []ldap.EntryAttribute
+	err = json.Unmarshal([]byte(user.Attributes), &attrs)
 	if err != nil {
 		return err
 	}
 
-	request := ldap.AddRequest{
-		DN:         user.DN,
-		Attributes: attrs,
+	isUpdate := len(entries) == 1
+	if isUpdate {
+		request := ldap.NewModifyRequest(user.DN, nil)
+		var attrMap = make(map[string][]string)
+		for _, attr := range entries[0].Attributes {
+			attrMap[attr.Name] = attr.Values
+		}
+		for _, attr := range attrs {
+			if attrMap[attr.Name] == nil {
+				request.Add(attr.Name, attr.Values)
+			} else {
+				request.Replace(attr.Name, attr.Values)
+			}
+		}
+		err = c.Conn.Modify(request)
+	} else {
+		request := ldap.NewAddRequest(user.DN, nil)
+		for _, attr := range attrs {
+			request.Attribute(attr.Name, attr.Values)
+		}
+		err = c.Conn.Add(request)
 	}
-	err = c.Conn.Add(&request)
 	if err != nil {
 		logs.Error("Add fail: %v", err)
 	}
 	return err
 }
 
-func (c *Client) Authentication(account string, password string) (*models.LdapUser, error) {
-	// The username and password we want to check
-	users, err := c.Search(fmt.Sprintf("(&(objectClass=*)(uid=%s))", ldap.EscapeFilter(account)))
-	if err != nil {
-		logs.Error("search fail: %v", err)
-		return nil, err
-	}
-	if len(users) != 1 {
-		logs.Error("User does not exist or too many entries returned")
-		return nil, errors.New("未找到用户或者账号重复!")
-	}
-
-	userDN := users[0].DN
+func (c *Client) Authentication(userDN string, password string) error {
 	client, err := c.Acquire()
 	if err != nil {
-		return nil, err
+		return err
 	}
 	err = client.Bind(userDN, password, false)
 	if err != nil {
 		logs.Error("GSSAPIBind failed, err:%v", err)
-		return nil, errors.New("登录失败,账号或者密码不正确!")
+		return errors.New("登录失败,账号或者密码不正确!")
 	}
 	c.pool.Release(client)
-	return &users[0], nil
+	return nil
 }

+ 1 - 2
server/modules/ldap/router.go

@@ -16,9 +16,8 @@ func InitRouter(prefix string) *beego.Namespace {
 		beego.NSRouter("/login", &UserController{}, "post:Login"),
 		beego.NSRouter("/user/sync", &UserController{}, "post:SyncUsers"),
 		beego.NSRouter("/user/updatePassword", &UserController{}, "post:UpdateUserPassword"),
-		beego.NSRouter("/user/update", &UserController{}, "post:UpdateUser"),
 		beego.NSRouter("/user/detail", &UserController{}, "get:GetDetail"),
-		beego.NSRouter("/user/add", &UserController{}, "post:AddUser"),
+		beego.NSRouter("/user/save", &UserController{}, "post:SaveUser"),
 		beego.NSRouter("/user/list", &UserController{}, "post:GetUsers"),
 	)
 	return ns

+ 7 - 157
server/modules/ldap/service.go

@@ -5,7 +5,6 @@ import (
 	"encoding/hex"
 	"errors"
 	"fmt"
-	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
 	"nginx-ui/server/config"
 	"nginx-ui/server/models"
@@ -32,55 +31,18 @@ func (c *Service) GetServer() (*models.LdapServer, error) {
 }
 
 func (c *Service) Login(req *LDAPLoginReq) (*models.User, error) {
-
 	server := models.LdapServer{Key: req.ServerKey}
 	o := orm.NewOrm()
 	err := o.Read(&server, "Key")
 	if err != nil {
 		return nil, errors.New("未找到对应的LDAP服务!")
 	}
-	client, err := GetActiveClient(&server)
-	if err != nil {
-		return nil, err
-	}
-
-	ldapUser, err := client.Authentication(req.Account, req.Password)
-	if err != nil {
-		return nil, err
-	}
-	ldapUser.Uid = server.Uid
-	ldapUser.ServerKey = server.Key
-	_, err = o.InsertOrUpdate(ldapUser, "DN")
+	user, err := UserIns.Authentication(&server, req.Account, req.Password)
 	if err != nil {
 		return nil, err
 	}
-	var user = models.User{
-		Account: ldapUser.Account,
-	}
-	err = o.Read(&user, "Account")
-	if err != nil {
-		if !errors.Is(err, orm.ErrNoRows) {
-			return nil, err
-		}
-		user.Password = ldapUser.Password
-		user.Account = ldapUser.Account
-		user.Nickname = ldapUser.UserName
-		user.Source = "LDAP"
-		_, err := o.Insert(&user)
-		if err != nil {
-			return nil, err
-		}
-	} else if user.Source == "LDAP" {
-		// 更新用户
-		user.Password = ldapUser.Password
-		user.Nickname = ldapUser.UserName
-		_, err = o.Update(&user, "Password", "Nickname")
-		if err != nil {
-			return nil, err
-		}
-	}
 	user.Password = ""
-	return &user, nil
+	return user, nil
 }
 
 // GetServers 获取用户所有的LDAP连接
@@ -120,71 +82,6 @@ func (c *Service) GetServers(current *models.User, req *vo.PageReq) (*vo.PageRes
 	return &resp, err
 }
 
-// SyncUsers 同步用户信息
-// post /ldap/user/sync
-func (c *Service) SyncUsers(current *models.User, req *LDAPUserSyncReq) (int, error) {
-	server := models.LdapServer{Key: req.ServerKey}
-	o := orm.NewOrm()
-	err := o.Read(&server, "Key")
-	if err != nil {
-		return 0, err
-	}
-	client, err := GetActiveClient(&server)
-	if err != nil {
-		return 0, err
-	}
-
-	users, err := client.Search(req.Filter)
-	if err != nil {
-		return 0, err
-	}
-	for _, user := range users {
-		user.ServerKey = server.Key
-		user.Uid = string(rune(current.Id))
-		_, err := o.InsertOrUpdate(&user, "DN")
-		if err != nil {
-			logs.Error("save user fail: %v", err)
-		}
-	}
-	return len(users), nil
-}
-
-// SyncUser SyncUsers 同步用户信息
-// post /ldap/user/sync
-func (c *Service) SyncUser(server *models.LdapServer, current *models.LdapUser) error {
-	o := orm.NewOrm()
-	if server == nil {
-		server := &models.LdapServer{Key: current.ServerKey}
-		err := o.Read(server, "Key")
-		if err != nil {
-			return err
-		}
-	}
-	client, err := GetActiveClient(server)
-	if err != nil {
-		return err
-	}
-
-	users, err := client.Search(fmt.Sprintf("(&(objectClass=*)(uid=%s))", current.Account))
-	if err != nil {
-		return err
-	}
-	if len(users) != 1 {
-		return errors.New("账号不存在或者账号重复!")
-	}
-	user := users[0]
-	user.Id = current.Id
-	user.ServerKey = current.ServerKey
-	user.Uid = current.Uid
-	user.Remark = current.Remark
-
-	_, err = o.Update(&user)
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
 // Update 保存或者修改
 // post /ldap/server
 func (c *Service) Update(current *models.User, body *models.LdapServer) (*models.LdapServer, error) {
@@ -275,7 +172,7 @@ func (c *Service) Add(current *models.User, body *models.LdapServer) (*models.Ld
 }
 
 // VerifyServer 验证服务
-func (c *Service) VerifyServer(req *VerifyReq) ([]models.LdapUser, error) {
+func (c *Service) VerifyServer(req *VerifyReq) ([]*models.LdapUser, error) {
 	var server = &models.LdapServer{
 		Id: req.Id,
 	}
@@ -284,63 +181,16 @@ func (c *Service) VerifyServer(req *VerifyReq) ([]models.LdapUser, error) {
 	if err != nil {
 		return nil, err
 	}
-	client, err := GetActiveClient(server)
-	if err != nil {
-		return nil, err
-	}
 
 	if req.Filter == "" && req.Username != "" {
 		req.Filter = fmt.Sprintf("(&(objectClass=*)(uid=%s))", req.Username)
 	}
-	if req.Filter != "" {
-		users, err := client.Search(req.Filter)
-		if err != nil {
-			return nil, err
-		}
-		return users, nil
-	}
-	return make([]models.LdapUser, 0), nil
-}
-
-// UpdateUserPassword 更新用户密码
-// post /ldap/user/modifyPassword
-func (c *Service) UpdateUserPassword(req *UpdatePasswordReq, byAdmin bool) error {
-	o := orm.NewOrm()
-	user := models.LdapUser{
-		Account: req.Account,
-	}
-	err := o.Read(&user, "Account")
-	if err != nil {
-		if errors.Is(err, orm.ErrNoRows) {
-			return nil
-		}
-		logs.Error("read user fail: %v", err)
-		return err
-	}
-	server := models.LdapServer{
-		Key: user.ServerKey,
-	}
-	err = o.Read(&server, "Key")
-	if err != nil {
-		return err
-	}
-	client, err := GetActiveClient(&server)
-	if err != nil {
-		return err
-	}
-	if byAdmin {
-		err = client.ModifyPasswordByAdmin(user.DN, req.Password)
-	} else {
-		err = client.ModifyPassword(user.DN, req.OldPassword, req.Password)
-	}
+	users, _, err := UserIns.Search(server, req.Filter)
 	if err != nil {
-		return err
-	}
-	err = c.SyncUser(&server, &user)
-	if err != nil {
-		return errors.New("密码更新成功,但更新用户信息失败:" + err.Error())
+		return nil, err
 	}
-	return nil
+
+	return users, nil
 }
 
 // GetUsers 获取全部用户

+ 13 - 9
server/modules/ldap/user_controller.go

@@ -1,6 +1,7 @@
 package ldap
 
 import (
+	"errors"
 	"nginx-ui/server/base"
 	"nginx-ui/server/models"
 )
@@ -36,7 +37,7 @@ func (c *UserController) SyncUsers() {
 	if !c.ReadBody(&req) {
 		return
 	}
-	count, err := ServiceInstance.SyncUsers(current, &req)
+	count, err := UserIns.SyncUsers(current, &req)
 	if err != nil {
 		c.ErrorJson(err)
 		return
@@ -52,14 +53,8 @@ func (c *UserController) UpdateUserPassword() {
 
 }
 
-// UpdateUser 更新用户信息或者新增用户
-// post /ldap/user/update
-func (c *UserController) UpdateUser() {
-
-}
-
-// AddUser 新增用户
-func (c *UserController) AddUser() {
+// SaveUser 新增或者修改用户
+func (c *UserController) SaveUser() {
 	current := c.RequiredUser()
 	if current == nil {
 		return
@@ -68,6 +63,15 @@ func (c *UserController) AddUser() {
 	if !c.ReadBody(&user) {
 		return
 	}
+	if user.ServerKey == "" {
+		server, err := ServiceInstance.GetServer()
+		if err != nil {
+			c.ErrorJson(errors.New("请先激活LDAP服务"))
+			return
+		}
+		user.ServerKey = server.Key
+	}
+
 	resp, err := UserIns.Add(&user)
 	if err != nil {
 		c.ErrorJson(err)

+ 208 - 5
server/modules/ldap/user_service.go

@@ -1,10 +1,13 @@
 package ldap
 
 import (
+	"errors"
+	"fmt"
+	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
+	"github.com/go-ldap/ldap/v3"
 	"nginx-ui/server/config"
 	"nginx-ui/server/models"
-	"time"
 )
 
 type UserService struct {
@@ -12,7 +15,6 @@ type UserService struct {
 
 // Add Update 保存或者修改
 func (c *UserService) Add(body *models.LdapUser) (*models.LdapUser, error) {
-
 	server := models.LdapServer{
 		Key: body.ServerKey,
 	}
@@ -22,8 +24,7 @@ func (c *UserService) Add(body *models.LdapUser) (*models.LdapUser, error) {
 		return nil, err
 	}
 	body.Uid = server.Uid
-
-	_, err = o.Insert(body)
+	_, err = o.InsertOrUpdate(body)
 	if err != nil {
 		return nil, err
 	}
@@ -36,7 +37,17 @@ func (c *UserService) Add(body *models.LdapUser) (*models.LdapUser, error) {
 	if err != nil {
 		return nil, err
 	}
-	body.LastSyncDate = time.Now()
+	if body.Password != config.ReplacePassword {
+		err := client.ModifyPasswordByAdmin(body.DN, body.Password)
+		if err != nil {
+			return nil, errors.New("新增成功,但密码修改失败!")
+		}
+	}
+	entry, err := client.SearchByAccount(body.Account)
+	if err != nil {
+		return nil, err
+	}
+	modifyLDAPUser(body, entry)
 	_, _ = o.Update(body)
 	return body, nil
 }
@@ -53,3 +64,195 @@ func (c *UserService) GetDetail(id int) (*models.LdapUser, error) {
 	user.Password = config.ReplacePassword
 	return &user, nil
 }
+
+func (c *UserService) Search(server *models.LdapServer, filter string) ([]*models.LdapUser, []*models.LdapOrganize, error) {
+
+	client, err := GetActiveClient(server)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	entries, err := client.Search(filter)
+	if err != nil {
+		return nil, nil, err
+	}
+	var users []*models.LdapUser
+	var organizeList []*models.LdapOrganize
+	for _, entry := range entries {
+		var isOrganize = false
+		objectClass := entry.GetAttributeValues("objectClass")
+		for _, oc := range objectClass {
+			if oc == server.OrganizeClass {
+				isOrganize = true
+				break
+			}
+		}
+		if isOrganize {
+			organize := models.LdapOrganize{
+				Name:        entry.GetAttributeValue("ou"),
+				DN:          entry.DN,
+				ServerKey:   server.Key,
+				ObjectClass: entry.GetAttributeValue("objectClass"),
+			}
+			organizeList = append(organizeList, &organize)
+		} else {
+			user := createUser(entry)
+			user.ServerKey = server.Key
+			users = append(users, &user)
+		}
+
+	}
+	return users, organizeList, nil
+}
+
+// SyncUser SyncUsers 同步用户信息
+// post /ldap/user/sync
+func (c *UserService) SyncUser(server *models.LdapServer, current *models.LdapUser) error {
+	o := orm.NewOrm()
+	if server == nil {
+		server := &models.LdapServer{Key: current.ServerKey}
+		err := o.Read(server, "Key")
+		if err != nil {
+			return err
+		}
+	}
+
+	filter := fmt.Sprintf("(&(objectClass=*)(uid=%s))", current.Account)
+
+	users, _, err := c.Search(server, filter)
+	if len(users) != 1 {
+		return errors.New("账号不存在或者账号重复!")
+	}
+	user := users[0]
+	user.Id = current.Id
+	user.ServerKey = current.ServerKey
+	user.Uid = current.Uid
+	user.Remark = current.Remark
+
+	_, err = o.Update(&user)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// SyncUsers 同步用户信息
+// post /ldap/user/sync
+func (c *UserService) SyncUsers(current *models.User, req *LDAPUserSyncReq) (int, error) {
+	server := &models.LdapServer{Key: req.ServerKey}
+	o := orm.NewOrm()
+	err := o.Read(server, "Key")
+	if err != nil {
+		return 0, err
+	}
+	users, organizeList, err := c.Search(server, req.Filter)
+	if err != nil {
+		return 0, err
+	}
+	for _, user := range users {
+		user.Uid = string(rune(current.Id))
+		_, err := o.InsertOrUpdate(user, "DN")
+		if err != nil {
+			logs.Error("save user fail: %v", err)
+		}
+	}
+	for _, organize := range organizeList {
+		_, err = o.InsertOrUpdate(organize, "DN")
+		if err != nil {
+			logs.Error("save organize fail: %v", err)
+		}
+	}
+	return len(users), nil
+}
+
+func (c *UserService) Authentication(server *models.LdapServer, account string, password string) (*models.User, error) {
+	o := orm.NewOrm()
+	ldapUser := &models.LdapUser{
+		Account: account,
+	}
+	err := o.Read(ldapUser, "Account")
+	if err != nil && !errors.Is(err, orm.ErrNoRows) {
+		return nil, err
+	} else if err != nil {
+		// The username and password we want to check
+		filter := fmt.Sprintf("(&(objectClass=*)(uid=%s))", ldap.EscapeFilter(account))
+		users, _, err := c.Search(server, filter)
+		if err != nil || len(users) != 1 {
+			logs.Error("search fail: %v", err)
+			return nil, errors.New("您输入的账号或者密码错误!")
+		}
+		ldapUser = users[0]
+		_, err = o.InsertOrUpdate(ldapUser, "DN")
+		if err != nil {
+			return nil, err
+		}
+	}
+	userDN := ldapUser.DN
+	client, err := GetActiveClient(server)
+	if err != nil {
+		return nil, err
+	}
+	err = client.Authentication(userDN, password)
+	if err != nil {
+		return nil, err
+	}
+
+	user := &models.User{
+		Account: account,
+	}
+	err = o.Read(user, "Account")
+	if err != nil && !errors.Is(err, orm.ErrNoRows) {
+		return nil, err
+	} else if err != nil {
+		createLocalUser(user, ldapUser)
+		_, err = o.Insert(user)
+		if err != nil {
+			return nil, err
+		}
+	} else if user.Source == "LDAP" {
+		user.Nickname = ldapUser.UserName
+		_, _ = o.Update(user, "Nickname")
+	}
+	return user, nil
+}
+
+// UpdateUserPassword 更新用户密码
+// post /ldap/user/modifyPassword
+func (c *UserService) UpdateUserPassword(req *UpdatePasswordReq, byAdmin bool) error {
+	o := orm.NewOrm()
+	user := models.LdapUser{
+		Account: req.Account,
+	}
+	err := o.Read(&user, "Account")
+	if err != nil {
+		if errors.Is(err, orm.ErrNoRows) {
+			return nil
+		}
+		logs.Error("read user fail: %v", err)
+		return err
+	}
+	server := models.LdapServer{
+		Key: user.ServerKey,
+	}
+	err = o.Read(&server, "Key")
+	if err != nil {
+		return err
+	}
+	client, err := GetActiveClient(&server)
+	if err != nil {
+		return err
+	}
+	if byAdmin {
+		err = client.ModifyPasswordByAdmin(user.DN, req.Password)
+	} else {
+		err = client.ModifyPassword(user.DN, req.OldPassword, req.Password)
+	}
+	if err != nil {
+		return err
+	}
+	err = c.SyncUser(&server, &user)
+	if err != nil {
+		return errors.New("密码更新成功,但更新用户信息失败:" + err.Error())
+	}
+	return nil
+}

+ 39 - 0
server/modules/ldap/utils.go

@@ -0,0 +1,39 @@
+package ldap
+
+import (
+	"encoding/json"
+	"github.com/astaxie/beego/logs"
+	"github.com/go-ldap/ldap/v3"
+	"nginx-ui/server/models"
+	"time"
+)
+
+func modifyLDAPUser(user *models.LdapUser, entry *ldap.Entry) {
+	user.LastSyncDate = time.Now()
+	user.Password = entry.GetAttributeValue("userPassword")
+	user.UserName = entry.GetAttributeValue("cn")
+	user.Mail = entry.GetAttributeValue("mail")
+	user.DN = entry.DN
+}
+
+func createUser(entry *ldap.Entry) models.LdapUser {
+	user := models.LdapUser{
+		Account: entry.GetAttributeValue("uid"),
+	}
+	modifyLDAPUser(&user, entry)
+	jsonBytes, err := json.Marshal(entry.Attributes)
+	if err != nil {
+		logs.Error("marshal fail : %v", err)
+		user.Remark = "attributes marshal fail: " + err.Error()
+	} else {
+		user.Attributes = string(jsonBytes)
+	}
+	return user
+}
+
+func createLocalUser(user *models.User, from *models.LdapUser) {
+	user.Password = from.Password
+	user.Account = from.Account
+	user.Nickname = from.UserName
+	user.Source = "LDAP"
+}

+ 2 - 2
server/modules/user/service.go

@@ -138,7 +138,7 @@ func (u *UserService) UpdatePassword(req *vo.UserUpdatePassword) error {
 		return errors.New("该用户不存在或者已被删除!")
 	}
 	if user.Source == "LDAP" {
-		err = ldap.ServiceInstance.UpdateUserPassword(&ldap.UpdatePasswordReq{
+		err = ldap.UserIns.UpdateUserPassword(&ldap.UpdatePasswordReq{
 			Password:    req.NewPassword,
 			Account:     user.Account,
 			OldPassword: req.OldPassword,
@@ -160,7 +160,7 @@ func (u *UserService) UpdatePassword(req *vo.UserUpdatePassword) error {
 		if err != nil {
 			return err
 		}
-		err = ldap.ServiceInstance.UpdateUserPassword(&ldap.UpdatePasswordReq{
+		err = ldap.UserIns.UpdateUserPassword(&ldap.UpdatePasswordReq{
 			Password: req.NewPassword,
 			Account:  user.Account,
 		}, true)