Ldap synchronization (#2512)

* [Add]初步实现ldap一键导入用户到jumpserver用户表里

* [update]增加定时延迟一秒刷新页面

* [Update]更改前端以表格形式显示用户信息,优化代码结构

* [Update]增加用户显示表格

* [Update]settings配置文件取消注释

* [Update]优化ldap同步用功能代码

* [Update]删除ldap同步用户旧html模版

* [Update]修改登录页面图片拉伸问题

* [Update]增加 是否已经导入,在前端提示用户

* [Update]优化ldap同步用户的代码,以及翻译

* [Update] 更新翻译(改密计划) (#2525)

* [Update] 更新翻译(添加改密计划)

* [Update] 更新翻译(改密计划)

* [Update] 更新翻译

* [Update] 更新翻译

* Export login log (#2511)

* [Add]增加登录日志导出功能

* [Update]优化导出登录日志代码

* [Update]优化导出登录日志代码

* [Update]更改导出登录日志按钮
pull/2530/head^2^2
八千流 2019-03-22 15:55:46 +08:00 committed by 老广
parent 9aae106970
commit ce8f4b4a48
11 changed files with 641 additions and 417 deletions

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -5,17 +5,16 @@ import os
import json
import jms_storage
from rest_framework.views import Response, APIView
from ldap3 import Server, Connection
from rest_framework.views import Response, APIView
from django.conf import settings
from django.core.mail import send_mail
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from common.permissions import IsOrgAdmin, IsSuperUser
from .serializers import (
MailTestSerializer, LDAPTestSerializer
)
from .models import Setting
from .utils import get_ldap_users_list, save_user
from common.permissions import IsOrgAdmin, IsSuperUser
from .serializers import MailTestSerializer, LDAPTestSerializer
class MailTestingAPI(APIView):
@ -91,6 +90,36 @@ class LDAPTestingAPI(APIView):
return Response({"error": str(serializer.errors)}, status=401)
class LDAPSyncAPI(APIView):
permission_classes = (IsOrgAdmin,)
def get(self, request):
ldap_users_list = get_ldap_users_list()
if not isinstance(ldap_users_list, list):
return Response(ldap_users_list, status=401)
return Response(ldap_users_list)
class LDAPConfirmSyncAPI(APIView):
permission_classes = (IsOrgAdmin,)
def post(self, request):
user_names = request.data.get('user_names', '')
if not user_names:
error = _('User is not currently selected, please check the user '
'you want to import')
return Response({'error': error}, status=401)
ldap_users_list = get_ldap_users_list(user_names=user_names)
if not isinstance(ldap_users_list, list):
return Response(ldap_users_list, status=401)
save_result = save_user(ldap_users_list)
if 'error' in save_result.keys():
return Response(save_result, status=401)
return Response(save_result)
class ReplayStorageCreateAPI(APIView):
permission_classes = (IsSuperUser,)

View File

@ -0,0 +1,90 @@
{% extends '_modal.html' %}
{% load i18n %}
{% load static %}
{% block modal_class %}modal-lg{% endblock %}
{% block modal_id %}ldap_list_users_modal{% endblock %}
{% block modal_title%}{% trans "Ldap users" %}{% endblock %}
{% block modal_body %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
<script src="{% static 'js/jquery.form.min.js' %}"></script>
<style>
.inmodal .modal-header {
padding: 10px 10px;
text-align: center;
}
#assetTree2.ztree * {
background-color: #f8fafb;
}
#assetTree2.ztree {
background-color: #f8fafb;
}
</style>
<div class="wrapper wrapper-content">
<div class="row">
<div class="col-lg-12 animated fadeInRight" id="split-right">
<div class="mail-box-header">
<table class="table table-striped table-bordered table-hover " id="ldap_list_users_table" style="width: 100%">
<thead>
<tr>
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
<th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Email' %}</th>
<th class="text-center">{% trans 'Is imported' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
var ldap_users_table = 0;
function initLdapTable() {
if(ldap_users_table){
return
}
var options = {
ele: $('#ldap_list_users_table'),
ajax_url: '{% url "api-settings:ldap-sync" %}',
columns: [
{data: "username" },{data: "username" }, {data: "name" },
{data:"email"}, {data:'is_imported'}
],
pageLength: 10
};
ldap_users_table = jumpserver.initDataTable(options);
return ldap_users_table
}
$(document).ready(function(){
}).on('show.bs.modal', function () {
initLdapTable();
})
.on('click','.close_btn1',function () {
window.location.reload()
})
.on('click','.close_btn2',function () {
window.location.reload()
})
</script>
{% endblock %}
{% block modal_button %}
{{ block.super }}
{% endblock %}
{% block modal_confirm_id %}btn_ldap_modal_confirm{% endblock %}

View File

@ -31,7 +31,7 @@
<div class="tab-content">
<div class="col-sm-12" style="padding-left:0">
<div class="ibox-content" style="border-width: 0;padding-top: 40px;">
<form action="" method="post" class="form-horizontal">
<form id="ldap_form" action="" method="post" class="form-horizontal">
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
@ -61,6 +61,8 @@
<button class="btn btn-default btn-test" type="button"> {% trans 'Test connection' %}</button>
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
{# <button class="btn btn-primary sync_button " data-toggle="modal" data-target="#sync_users_modal" type="button">{% trans 'Synchronization' %}</button>#}
<button class="btn btn-primary sync_button " data-toggle="modal" data-target="#ldap_list_users_modal" type="button">{% trans 'Sync User' %}</button>
</div>
</div>
</form>
@ -72,10 +74,12 @@
</div>
</div>
</div>
{% include 'settings/_ldap_list_users_modal.html' %}
{% endblock %}
{% block custom_foot_js %}
<script>
$(document).ready(function () {
})
.on("click", ".btn-test", function () {
var data = {};
@ -102,5 +106,30 @@ $(document).ready(function () {
error: error
});
})
.on("click","#btn_ldap_modal_confirm",function () {
var user_names=[];
var cheked = $("tbody input[type='checkbox']:checked").each(function () {
user_names.push($(this).attr('id'));
});
var the_url = "{% url "api-settings:ldap-comfirm-sync" %}";
function error(message) {
toastr.error(message)
}
function success(message) {
toastr.success(message.msg)
}
APIUpdateAttr({
url: the_url,
body: JSON.stringify({'user_names':user_names}),
method: "POST",
flash_message: false,
success: success,
error: error
});
})
</script>
{% endblock %}

View File

@ -9,6 +9,8 @@ app_name = 'common'
urlpatterns = [
path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'),
path('ldap/testing/', api.LDAPTestingAPI.as_view(), name='ldap-testing'),
path('ldap/sync/', api.LDAPSyncAPI.as_view(), name='ldap-sync'),
path('ldap/comfirm/sync/', api.LDAPConfirmSyncAPI.as_view(), name='ldap-comfirm-sync'),
path('terminal/replay-storage/create/', api.ReplayStorageCreateAPI.as_view(), name='replay-storage-create'),
path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'),
path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'),

151
apps/settings/utils.py Normal file
View File

@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
#
from ldap3 import Server, Connection
from django.utils.translation import ugettext_lazy as _
from .models import settings
from users.models import User
def ldap_conn(host, use_ssl, bind_dn, password):
server = Server(host, use_ssl=use_ssl)
conn = Connection(server, bind_dn, password)
return conn
def ldap_search(conn, search_ougroup, search_filter, attr_map, user_names=None):
users_list = []
for search_ou in str(search_ougroup).split("|"):
ok = conn.search(search_ou, search_filter % ({"user": "*"}),
attributes=list(attr_map.values()))
if not ok:
error = _("Search no entry matched in ou {}").format(search_ou)
return {"error": error}
ldap_map_users(conn, attr_map, users_list, user_names)
if len(users_list) > 0:
return users_list
return {"error": _("Have user but attr mapping error")}
def get_ldap_users_list(user_names=None):
ldap_setting = get_ldap_setting()
conn = ldap_conn(ldap_setting['host'], ldap_setting['use_ssl'],
ldap_setting['bind_dn'], ldap_setting['password'])
try:
conn.bind()
except Exception as e:
return {"error": str(e)}
result_search = ldap_search(conn, ldap_setting['search_ougroup'],
ldap_setting['search_filter'],
ldap_setting['attr_map'], user_names=user_names)
return result_search
def ldap_map_users(conn, attr_map, users, user_names=None):
for entry in conn.entries:
user = entry_user(entry, attr_map)
if user_names:
if user.get('username', '') in user_names:
users.append(user)
else:
users.append(user)
def entry_user(entry, attr_map):
user = {}
user['is_imported'] = _('No')
for attr, mapping in attr_map.items():
if not hasattr(entry, mapping):
continue
value = getattr(entry, mapping).value
user[attr] = value if value else ''
if attr != 'username':
continue
if User.objects.filter(username=user[attr]):
user['is_imported'] = _('Yes')
return user
def get_ldap_setting():
host = settings.AUTH_LDAP_SERVER_URI
bind_dn = settings.AUTH_LDAP_BIND_DN
password = settings.AUTH_LDAP_BIND_PASSWORD
use_ssl = settings.AUTH_LDAP_START_TLS
search_ougroup = settings.AUTH_LDAP_SEARCH_OU
search_filter = settings.AUTH_LDAP_SEARCH_FILTER
attr_map = settings.AUTH_LDAP_USER_ATTR_MAP
auth_ldap = settings.AUTH_LDAP
ldap_setting = {
'host': host, 'bind_dn': bind_dn, 'password': password,
'search_ougroup': search_ougroup, 'search_filter': search_filter,
'attr_map': attr_map, 'auth_ldap': auth_ldap, 'use_ssl': use_ssl,
}
return ldap_setting
def save_user(users):
exist = []
username_list = [item.get('username') for item in users]
for name in username_list:
if User.objects.filter(username=name).exclude(source='ldap'):
exist.append(name)
users = [user for user in users if (user.get('username') not in exist)]
result_save = save(users, exist)
return result_save
def save(users, exist):
fail_user = []
for item in users:
item = set_default_item(item)
user = User.objects.filter(username=item['username'], source='ldap')
user = user.first()
if not user:
try:
user = User.objects.create(**item)
except Exception as e:
fail_user.append(item.get('username'))
continue
for key, value in item.items():
user.key = value
user.save()
get_msg = get_messages(users, exist, fail_user)
return get_msg
def set_default_item(item):
item['source'] = 'ldap'
if not item.get('email', ''):
item['email'] = item['username'] + '@' + item['username'] + '.com'
if 'is_imported' in item.keys():
item.pop('is_imported')
return item
def get_messages(users, exist, fail_user):
if exist:
info = _("Import {} users successfully; import {} users failed, the "
"database already exists with the same name")
msg = info.format(len(users), str(exist))
if fail_user:
info = _("Import {} users successfully; import {} users failed, "
"the database already exists with the same name; import {}"
"users failed, BecauseTypeError' object has no attribute "
"'keys'")
msg = info.format(len(users)-len(fail_user), str(exist), str(fail_user))
else:
msg = _("Import {} users successfully").format(len(users))
if fail_user:
info = _("Import {} users successfully;import {} users failed, "
"BecauseTypeError' object has no attribute 'keys'")
msg = info.format(len(users)-len(fail_user), str(fail_user))
return {'msg': msg}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

View File

@ -8,7 +8,7 @@
<div class="modal-dialog {% block modal_class %}{% endblock %}">
<div class="modal-content animated fadeIn">
<div class="modal-header">
<button data-dismiss="modal" class="close" type="button"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
<button data-dismiss="modal" class="close close_btn1" type="button"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
<h4 class="modal-title">{% block modal_title %}{% endblock %}</h4>
<small>{% block modal_comment %}{% endblock %}</small>
</div>
@ -18,7 +18,7 @@
</div>
<div class="modal-footer">
{% block modal_button %}
<button data-dismiss="modal" class="btn btn-white" type="button">{% trans "Close" %}</button>
<button data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button>
<button class="btn btn-primary" type="button" id="{% block modal_confirm_id %}{% endblock %}">{% trans 'Confirm' %}</button>
{% endblock %}
</div>

View File

@ -32,95 +32,106 @@
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/plugins/datatables/datatables.min.js' %}"></script>
{# <script src="{% static 'js/angular.min.js' %}"></script>#}
<style>
.box-1{
height: 472px;
width: 984px;
margin-right: auto;
margin-left: auto;
margin-top: calc((100vh - 470px)/2);
}
.box-2{
height: 100%;
width: 50%;
float: right;
}
.box-3{
text-align: center;
background-color: white;
height: 100%;
width: 50%;
}
.captcha {
float: right;
}
</style>
</head>
<body>
<div class="login-dialog">
<div class="">
<div class="row" style="height: 472px">
<div class="col-md-4 col-md-offset-2 input_shadow-1" style="text-align: center;background-color: white; padding-right: 0px;height: 100%">
<div style="background-color: white">
{% if interface.login_title %}
<div style="margin-top: 40px">
<span style="font-size: 24px;font-weight:400;color: #151515;letter-spacing: 0;">{{ interface.login_title }}</span>
</div>
<body style="height: 100%">
<div>
<div class="box-1">
<div class="box-2">
{% if interface.login_image %}
<img src="{{ MEDIA_URL }}{{ interface.login_image }}" style="height: 100%; width: 100%"/>
{% else %}
<div style="margin-top: 40px">
<span style="font-size: 24px;font-weight:400;color: #151515;letter-spacing: 0;">{% trans 'Welcome to the Jumpserver open source fortress' %}</span>
</div>
<img src="{% static 'img/login/login_image_1.png' %}" style=" height: 100%; width: 100%"/>
{% endif %}
</div>
<div class="box-3">
<div style="background-color: white">
{% if interface.login_title %}
<div style="margin-top: 40px;padding-top: 50px;">
<span style="font-size: 24px;font-weight:400;color: #151515;letter-spacing: 0;">{{ interface.login_title }}</span>
</div>
{% else %}
<div style="margin-top: 40px;padding-top: 50px;">
<span style="font-size: 24px;font-weight:400;color: #151515;letter-spacing: 0;">{% trans 'Welcome to the Jumpserver open source fortress' %}</span>
</div>
{% endif %}
<div style="font-size: 12px;color: #999999;letter-spacing: 0;line-height: 18px;margin-top: 10px">
{% trans 'Welcome back, please enter username and password to login' %}
{% trans 'Welcome back, please enter username and password to login' %}
</div>
<div style="margin-bottom: 10px">
<div>
<div class="col-md-1"></div>
<div class="contact-form col-md-10" style="margin-top: 20px;height: 35px">
<form id="contact-form" action="" method="post" role="form" novalidate="novalidate">
{% csrf_token %}
<div style="height: 48px;color: red">
{% if block_login %}
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
{% elif password_expired %}
<p class="red-fonts">{% trans 'The user password has expired' %}</p>
{% elif form.errors %}
{% if 'captcha' in form.errors %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
{% else %}
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
{% endif %}
{% endif %}
</div>
<div>
<div class="col-md-1"></div>
<div class="contact-form col-md-10" style="margin-top: 20px;height: 35px">
<form id="contact-form" action="" method="post" role="form" novalidate="novalidate">
{% csrf_token %}
<div style="height: 48px;color: red">
{% if block_login %}
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
{% elif password_expired %}
<p class="red-fonts">{% trans 'The user password has expired' %}</p>
{% elif form.errors %}
{% if 'captcha' in form.errors %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
{% else %}
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
{% endif %}
{% endif %}
</div>
<div class="form-group">
<input type="text" class="form-control" name="{{ form.username.html_name }}"
placeholder="{% trans 'Username' %}" required=""
value="{% if form.username.value %}{{ form.username.value }}{% endif %}" style="height: 35px">
<div class="form-group">
<input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}" style="height: 35px">
</div>
<div class="form-group">
<input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
</div>
<div class="form-group" style="height: 50px;margin-bottom: 0">
{{ form.captcha }}
</div>
<div class="form-group" style="margin-top: 10px">
<button type="submit" class="btn btn-transparent">{% trans 'Login' %}</button>
</div>
<div style="text-align: center">
<a href="{% url 'users:forgot-password' %}">
<small>{% trans 'Forgot password' %}?</small>
</a>
</div>
</form>
</div>
<div class="form-group">
<input type="password" class="form-control" name="{{ form.password.html_name }}"
placeholder="{% trans 'Password' %}" required="">
</div>
<div class="form-group" style="height: 50px;margin-bottom: 0px">
{{ form.captcha }}
</div>
<div class="form-group" style="margin-top: 10px">
<button type="submit" class="btn btn-transparent">{% trans 'Login' %}</button>
</div>
<div style="text-align: center">
<a href="{% url 'users:forgot-password' %}">
<small>{% trans 'Forgot password' %}?</small>
</a>
</div>
</form>
<div class="col-md-1"></div>
</div>
</div>
<div class="col-md-1"></div>
</div>
</div>
</div>
</div>
<div class="col-md-4 " style="padding-left: 0px; height: 100%">
{% if interface.login_image %}
<img src="{{ MEDIA_URL }}{{ interface.login_image }}" style="width: 100%; height: 100%;" class="input_shadow-1" />
{% else %}
<img src="{% static 'img/login/login-image.jpg' %}" style="width: 100%; height: 100%;" class="input_shadow-1" />
{% endif %}
</div>
</div>
<div class="col-md-2"></div>
</div>
</div>
</div>
</body>
</html>