add ladp config

4.0
zypo 2022-11-23 10:30:12 +08:00
parent b6463e958d
commit 2eb55e0879
7 changed files with 343 additions and 50 deletions

View File

@ -201,7 +201,7 @@ def login(request):
if not config:
return handle_response(error='请在系统设置中配置LDAP后再尝试通过该方式登录')
ldap = LDAP(**config)
is_success, message = ldap.valid_user(form.username, form.password)
is_success, message = ldap.verify_user(form.username, form.password)
if is_success:
if not user:
user = User.objects.create(username=form.username, nickname=form.username, type=form.type)

View File

@ -9,7 +9,9 @@ from apps.setting.user import UserSettingView
urlpatterns = [
url(r'^$', SettingView.as_view()),
url(r'^user/$', UserSettingView.as_view()),
url(r'^ldap/$', LDAPUserView.as_view()),
url(r'^ldap_test/$', ldap_test),
url(r'^ldap_import/$', ldap_import),
url(r'^email_test/$', email_test),
url(r'^mfa/$', MFAView.as_view()),
url(r'^about/$', get_about)

View File

@ -6,14 +6,16 @@ from django.core.cache import cache
from django.conf import settings
from libs import JsonParser, Argument, json_response, auth
from libs.utils import generate_random_str
from libs.ldap import LDAP
from libs.mail import Mail
from libs.spug import send_login_wx_code
from libs.mixins import AdminView
from apps.setting.utils import AppSetting
from apps.setting.models import Setting, KEYS_DEFAULT
from apps.account.models import User
from copy import deepcopy
import platform
import ldap
import json
class SettingView(AdminView):
@ -68,21 +70,79 @@ class MFAView(AdminView):
def ldap_test(request):
form, error = JsonParser(
Argument('server'),
Argument('port', type=int),
Argument('admin_dn'),
Argument('password'),
Argument('admin_password'),
Argument('user_ou'),
Argument('user_filter'),
Argument('map_username'),
Argument('map_nickname'),
).parse(request.body)
print('form', form)
if error is None:
ldap = LDAP(form.server, form.admin_dn, form.admin_password, form.user_ou, form.user_filter, form.map_username, form.map_nickname)
status, ret = ldap.all_user()
if status:
return json_response(ret)
return json_response(error=ret)
return json_response(error=error)
@auth('admin')
def ldap_import(request):
form, error = JsonParser(
Argument('ldap_data', type=list),
Argument('username'),
Argument('nickname'),
).parse(request.body)
if error is None:
try:
con = ldap.initialize("ldap://{0}:{1}".format(form.server, form.port), bytes_mode=False)
con.simple_bind_s(form.admin_dn, form.password)
return json_response()
except Exception as e:
error = eval(str(e))
return json_response(error=error['desc'])
for x in form.ldap_data:
User.objects.update_or_create(
username=x[form.username],
defaults={'nickname': x[form.nickname], 'type': 'ldap'}
)
return json_response()
return json_response(error=error)
class LDAPUserView(AdminView):
def get(self, request):
ldap_config = AppSetting.get('ldap_service')
if not ldap_config:
return json_response(error='LDAP服务未配置')
ldap = LDAP(**ldap_config)
status, ret = ldap.all_user()
if status:
cn_key, sn_key = ldap_config.get('map_username'), ldap_config.get('map_nickname')
system_users = [x.username for x in User.objects.filter(type='ldap', deleted_by_id__isnull=True)]
for index, u in enumerate(ret):
u['cn'] = u[cn_key]
u['sn'] = u[sn_key]
u['is_exist'] = u.get(cn_key) in system_users
u['id'] = index
return json_response(ret)
return json_response(error=ret)
def post(self, request):
form, error = JsonParser(
Argument('server'),
Argument('admin_dn'),
Argument('admin_password'),
Argument('user_ou'),
Argument('user_filter'),
Argument('map_username'),
Argument('map_nickname'),
Argument('ldap_user', help='LDAP用户不能为空'),
Argument('ldap_password', help='LDAP密码不能为空'),
).parse(request.body)
if error is None:
ldap = LDAP(form.server, form.admin_dn, form.admin_password, form.user_ou, form.user_filter, form.map_username, form.map_nickname)
status, msg = ldap.verify_user(form.ldap_user, form.ldap_password)
if status:
return json_response()
return json_response(error=msg)
return json_response(error=error)
@auth('admin')
def email_test(request):
form, error = JsonParser(

View File

@ -5,26 +5,68 @@ import ldap
class LDAP:
def __init__(self, server, port, rules, admin_dn, password, base_dn):
def __init__(self, server, admin_dn, admin_password, user_ou, user_filter, map_username, map_nickname):
self.server = server
self.port = port
self.rules = rules
self.admin_dn = admin_dn
self.password = password
self.base_dn = base_dn
self.admin_dn = admin_dn
self.admin_password = admin_password
self.user_ou = user_ou
self.user_filter = user_filter
self.map_username = map_username
self.map_nickname = map_nickname
def valid_user(self, username, password):
def connect(self):
try:
conn = ldap.initialize("ldap://{0}:{1}".format(self.server, self.port), bytes_mode=False)
conn.simple_bind_s(self.admin_dn, self.password)
search_filter = f'({self.rules}={username})'
ldap_result_id = conn.search(self.base_dn, ldap.SCOPE_SUBTREE, search_filter, None)
result_type, result_data = conn.result(ldap_result_id, 0)
if result_type == ldap.RES_SEARCH_ENTRY:
conn.simple_bind_s(result_data[0][0], password)
return True, None
else:
return False, None
conn = ldap.initialize(f'{self.server}', bytes_mode=False)
conn.set_option(ldap.OPT_TIMEOUT, 3)
conn.set_option(ldap.OPT_NETWORK_TIMEOUT, 3)
conn.simple_bind_s(self.admin_dn, self.admin_password)
return True, conn
except Exception as error:
args = error.args
return False, args[0].get('desc', '未知错误') if args else '%s' % error
return False, error.args[0].get('desc')
def all_user(self):
status, conn = self.connect()
if status:
try:
# user_filter = '(cn=*)'
# map = ['cn', 'sn']
# user_map = list(self.user_map.values())
user_filter = "({}=*)".format(self.user_filter.split('=')[0][1:])
user_map = [self.map_username, self.map_nickname]
ldap_result = conn.search_s(self.user_ou, ldap.SCOPE_SUBTREE, user_filter, user_map)
ldap_users = []
for dn,entry in ldap_result:
if dn == self.user_ou:
continue
tmp_user = {}
for k,v in entry.items():
tmp_user.update({k: v[0].decode()})
ldap_users.append(tmp_user)
return True, ldap_users
except Exception as error:
return False, error.args[0].get('desc')
else:
return False, conn
def verify_user(self, username, password):
status, conn = self.connect()
if status:
try:
user_filter = f'({self.map_username}={username})'
ldap_result_id = conn.search(self.user_ou, ldap.SCOPE_SUBTREE, user_filter, [self.map_username])
_, result_data = conn.result(ldap_result_id, 0)
if result_data:
conn.simple_bind_s(result_data[0][0], password)
return True, True
else:
return False, '账户未找到'
except Exception as error:
return False, error.args[0].get('desc')
else:
return False, conn

View File

@ -5,8 +5,9 @@
*/
import React, { useState } from 'react';
import styles from './index.module.css';
import { Form, Button, Input, Space, message } from 'antd';
import { Form, Button, Input, Space, message, Modal } from 'antd';
import { http } from 'libs';
import LdapImport from './LdapImport';
import { observer } from 'mobx-react'
import store from './store';
@ -17,7 +18,7 @@ export default observer(function () {
function handleSubmit() {
store.loading = true;
const formData = form.getFieldsValue();
http.post('/api/setting/', {data: [{key: 'ldap_service', value: formData}]})
http.post('/api/setting/', { data: [{ key: 'ldap_service', value: formData }] })
.then(() => {
message.success('保存成功');
store.fetchSettings()
@ -28,39 +29,78 @@ export default observer(function () {
function ldapTest() {
setLoading(true);
const formData = form.getFieldsValue();
http.post('/api/setting/ldap_test/', formData).then(() => {
message.success('LDAP服务连接成功')
http.post('/api/setting/ldap_test/', formData).then((res) => {
message.success("成功匹配" + res.length + "个用户")
}).finally(() => setLoading(false))
}
function ldapLogin(info) {
let ldadUser;
let ldadPwd;
Modal.confirm({
title: 'LDAP用户测试登录',
content: <Form layout="vertical" style={{marginTop: 24}}>
<Form.Item required label="LDAP用户名">
<Input onChange={val => ldadUser = val.target.value }/>
</Form.Item>
<Form.Item required label="LDAP用户密码" >
<Input.Password onChange={val => ldadPwd = val.target.value}/>
</Form.Item>
</Form>,
onOk: () => {
setLoading(true);
const formData = form.getFieldsValue();
formData.ldap_user = ldadUser;
formData.ldap_password = ldadPwd;
return http.post('/api/setting/ldap/', formData)
.then(() => message.success('登录成功', 1)).finally(() => setLoading(false))
},
})
};
return (
<React.Fragment>
<div className={styles.title}>LDAP设置</div>
<Form form={form} initialValues={store.settings.ldap_service} style={{maxWidth: 400}} labelCol={{span: 8}}
wrapperCol={{span: 16}}>
<Form.Item required name="server" label="LDAP服务地址">
<Input placeholder="例如ldap.spug.cc"/>
<Form form={form} initialValues={store.settings.ldap_service} style={{ maxWidth: 400 }} labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}>
<Form.Item required name="server" label="LDAP服务地址" >
<Input placeholder="例如ldap://127.0.0.1:389" />
</Form.Item>
<Form.Item required name="port" label="LDAP服务端口">
<Input placeholder="例如389"/>
<Form.Item required name="admin_dn" label="绑定DN" >
<Input placeholder="例如cn=admin,dc=spug,dc=cc" />
</Form.Item>
<Form.Item required name="admin_dn" label="管理员DN">
<Input placeholder="例如cn=admin,dc=spug,dc=dev"/>
<Form.Item required name="admin_password" label="密码">
<Input.Password placeholder="LDAP管理密码" />
</Form.Item>
<Form.Item required name="password" label="管理员密码">
<Input.Password placeholder="请输入LDAP管理员密码"/>
<Form.Item required name="user_ou" label="用户OU">
<Input placeholder="例如ou=users,dc=spug,dc=cc" />
</Form.Item>
<Form.Item required name="rules" label="LDAP搜索规则">
<Input placeholder="例如cn"/>
<Form.Item required name="user_filter" label="用户过滤器">
<Input placeholder="例如:(cn或uid或sAMAccountName=%(user)s)" value="(cn=%(user)s)" />
</Form.Item>
<Form.Item required name="base_dn" label="基本DN">
<Input placeholder="例如dc=spug,dc=dev"/>
{/* <Form.Item required name="user_map" label="" extra="LDAPSpugusername, nickname Spug">
<Input.TextArea row={4} placeholder="例如:" />
</Form.Item> */}
<Form.Item required name="map_username" label="登录名映射" extra="登录名映射代表将LDAP用户的某个属性映射到Spug的登录名中例如cn对应登录名">
<Input placeholder="例如cn" />
</Form.Item>
<Form.Item required name="map_nickname" label="姓名映射" extra="姓名映射代表将LDAP用户的某个属性映射到Spug的姓名中例如sn对应姓名">
<Input placeholder="例如sn" />
</Form.Item>
<Space>
<Button type="danger" loading={loading} onClick={ldapTest}>测试LDAP</Button>
<Button loading={loading} onClick={ldapTest}>测试连接</Button>
<Button loading={loading} onClick={ldapLogin}>测试登录</Button>
<Button loading={loading} onClick={store.handleLdapImport}>用户导入</Button>
<Button type="primary" loading={store.loading} onClick={handleSubmit}>保存设置</Button>
</Space>
</Form>
</React.Fragment>
{store.importVisible && <LdapImport />}
</React.Fragment>
)
})

View File

@ -0,0 +1,123 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, { useState } from 'react';
import { observer } from 'mobx-react';
import { Modal, Input, message, Badge, Button } from 'antd';
import http from 'libs/http';
import { TableCard } from 'components';
import store from './store';
export default observer(function () {
const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
function handleImportSelect() {
setLoading(true);
let ldap_data = [];
for (let item of store.dataSource) {
if (selectedRowKeys.includes(item.id)) {
ldap_data.push(item);
}
}
handleSubmit(ldap_data);
}
function handleImportAll() {
setLoading(true);
handleSubmit(store.dataSource);
}
function handleSubmit(data) {
if (data) {
http.post('/api/setting/ldap_import/',
{'ldap_data': data, 'username': store.username, 'nickname': store.nickname})
.then(() => {
message.success('操作成功');
store.importVisible = false;
}, () => setLoading(false))
}
}
function handleClickRow(record) {
let tmp = new Set(selectedRowKeys)
if (!tmp.delete(record.id)) {
tmp.add(record.id)
}
setSelectedRowKeys([...tmp])
}
function handleSelectAll(selected) {
let tmp = new Set(selectedRowKeys)
for (let item of store.dataSource) {
if (selected) {
tmp.add(item.id)
} else {
tmp.delete(item.id)
}
}
setSelectedRowKeys([...tmp])
}
let columns = [{
title: '登录名',
dataIndex: "cn",
}, {
title: '姓名',
dataIndex: "sn",
}, {
title: '是否存在',
render: text => text.is_exist ? <Badge status="success" text='是' /> : <Badge status="error" text='否' />,
}
];
return (
<Modal
visible
width={700}
maskClosable={false}
title={'Ldap用户导入'}
onCancel={() => store.importVisible = false}
confirmLoading={loading}
onOk={handleImportAll}
footer={[
<Button key="back" onClick={() => store.importVisible = false}>取消</Button>,
<Button key="select" type="primary" loading={loading} onClick={handleImportSelect}>导入选中</Button>,
<Button key="import" type="primary" loading={loading} onClick={handleImportAll}>导入全部</Button>,
]}>
<TableCard
tKey="sa"
rowKey="id"
title="LDAP用户列表"
loading={store.isFetching}
dataSource={store.dataSource}
onReload={store.fetchLdapRecords}
onRow={record => {
return {
onClick: () => handleClickRow(record)
}
}}
actions={[
<Input value={store.f_name} onChange={e => store.f_name = e.target.value } placeholder="搜索LDAP用户" />,
]}
pagination={{
showSizeChanger: true,
showLessItems: true,
showTotal: total => `${total}`,
pageSizeOptions: ['10', '20', '50', '100']
}}
rowSelection={{
selectedRowKeys,
onSelect: handleClickRow,
onSelectAll: handleSelectAll
}}
columns={columns} />
</Modal>
)
})

View File

@ -3,13 +3,23 @@
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import { observable } from "mobx";
import { observable, computed } from "mobx";
import http from 'libs/http';
class Store {
@observable settings = {};
@observable isFetching = false;
@observable loading = false;
@observable importVisible = false;
@observable records = [];
@observable f_name;
@computed get dataSource() {
let records = this.records;
if (this.f_name) records = records.filter(x => x.cn.toLowerCase().includes(this.f_name.toLowerCase()));
return records
}
fetchSettings = () => {
this.isFetching = true;
@ -21,6 +31,22 @@ class Store {
update = (key, value) => {
this.settings[key] = value
}
fetchLdapRecords = () => {
this.isFetching = true;
http.get('/api/setting/ldap/')
.then((res) => {
this.records = res;
})
.finally(() => this.isFetching = false)
};
handleLdapImport = () => {
this.importVisible = true;
this.fetchLdapRecords();
}
}
export default new Store()