mirror of https://github.com/openspug/spug
add ladp config
parent
b6463e958d
commit
2eb55e0879
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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="用户属性映射代表怎样将LDAP中用户属性映射到Spug用户上,username, 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>
|
||||
{store.importVisible && <LdapImport />}
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
})
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue