Unverified Commit b3e2b30e authored by BaiJiangJie's avatar BaiJiangJie Committed by GitHub

Merge pull request #3488 from jumpserver/1.5.5

1.5.5
parents 98d3a36d 7427f02e
{% extends '_base_list.html' %} {% extends '_base_list.html' %}
{% load i18n static %} {% load i18n static %}
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message">
{% trans 'Before using this feature, make sure that the application loader has been uploaded to the application server and successfully published as a RemoteApp application' %} {% trans 'Before using this feature, make sure that the application loader has been uploaded to the application server and successfully published as a RemoteApp application' %}
<b><a href="https://github.com/jumpserver/Jmservisor/releases" target="view_window" >{% trans 'Download application loader' %}</a></b> <b><a href="https://github.com/jumpserver/Jmservisor/releases" target="view_window" >{% trans 'Download application loader' %}</a></b>
</div>
{% endblock %} {% endblock %}
{% block table_search %}{% endblock %} {% block table_search %}{% endblock %}
{% block table_container %} {% block table_container %}
......
# Generated by Django 2.2.5 on 2019-11-14 03:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0042_favoriteasset'),
]
operations = [
migrations.AddField(
model_name='gathereduser',
name='date_last_login',
field=models.DateTimeField(null=True, verbose_name='Date last login'),
),
migrations.AddField(
model_name='gathereduser',
name='ip_last_login',
field=models.CharField(default='', max_length=39, verbose_name='IP last login'),
),
]
...@@ -12,13 +12,12 @@ __all__ = ['GatheredUser'] ...@@ -12,13 +12,12 @@ __all__ = ['GatheredUser']
class GatheredUser(OrgModelMixin): class GatheredUser(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset")) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset"))
username = models.CharField(max_length=32, blank=True, db_index=True, username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username'))
verbose_name=_('Username'))
present = models.BooleanField(default=True, verbose_name=_("Present")) present = models.BooleanField(default=True, verbose_name=_("Present"))
date_created = models.DateTimeField(auto_now_add=True, date_last_login = models.DateTimeField(null=True, verbose_name=_("Date last login"))
verbose_name=_("Date created")) ip_last_login = models.CharField(max_length=39, default='', verbose_name=_("IP last login"))
date_updated = models.DateTimeField(auto_now=True, date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
verbose_name=_("Date updated")) date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
@property @property
def hostname(self): def hostname(self):
......
...@@ -12,6 +12,7 @@ class GatheredUserSerializer(OrgResourceModelSerializerMixin): ...@@ -12,6 +12,7 @@ class GatheredUserSerializer(OrgResourceModelSerializerMixin):
model = GatheredUser model = GatheredUser
fields = [ fields = [
'id', 'asset', 'hostname', 'ip', 'username', 'id', 'asset', 'hostname', 'ip', 'username',
'date_last_login', 'ip_last_login',
'present', 'date_created', 'date_updated' 'present', 'date_created', 'date_updated'
] ]
read_only_fields = fields read_only_fields = fields
......
...@@ -94,6 +94,13 @@ GATHER_ASSET_USERS_TASKS = [ ...@@ -94,6 +94,13 @@ GATHER_ASSET_USERS_TASKS = [
"args": "database=passwd" "args": "database=passwd"
}, },
}, },
{
"name": "get last login",
"action": {
"module": "shell",
"args": "users=$(getent passwd | grep -v 'nologin' | grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -F $i -1 | head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done"
}
}
] ]
GATHER_ASSET_USERS_TASKS_WINDOWS = [ GATHER_ASSET_USERS_TASKS_WINDOWS = [
......
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
import re import re
from collections import defaultdict from collections import defaultdict
from celery import shared_task
from celery import shared_task
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils import timezone
from orgs.utils import tmp_to_org from orgs.utils import tmp_to_org
from common.utils import get_logger from common.utils import get_logger
...@@ -19,19 +20,25 @@ ignore_login_shell = re.compile(r'nologin$|sync$|shutdown$|halt$') ...@@ -19,19 +20,25 @@ ignore_login_shell = re.compile(r'nologin$|sync$|shutdown$|halt$')
def parse_linux_result_to_users(result): def parse_linux_result_to_users(result):
task_result = {} users = defaultdict(dict)
for task_name, raw in result.items(): users_result = result.get('gather host users', {})\
res = raw.get('ansible_facts', {}).get('getent_passwd') .get('ansible_facts', {})\
if res: .get('getent_passwd')
task_result = res if not isinstance(users_result, dict):
break users_result = {}
if not task_result or not isinstance(task_result, dict): for username, attr in users_result.items():
return []
users = []
for username, attr in task_result.items():
if ignore_login_shell.search(attr[-1]): if ignore_login_shell.search(attr[-1]):
continue continue
users.append(username) users[username] = {}
last_login_result = result.get('get last login', {}).get('stdout_lines', [])
for line in last_login_result:
data = line.split('@')
if len(data) != 3:
continue
username, ip, dt = data
dt += ' +0800'
date = timezone.datetime.strptime(dt, '%b %d %H:%M:%S %Y %z')
users[username] = {"ip": ip, "date": date}
return users return users
...@@ -45,7 +52,7 @@ def parse_windows_result_to_users(result): ...@@ -45,7 +52,7 @@ def parse_windows_result_to_users(result):
if not task_result: if not task_result:
return [] return []
users = [] users = {}
for i in range(4): for i in range(4):
task_result.pop(0) task_result.pop(0)
...@@ -55,7 +62,7 @@ def parse_windows_result_to_users(result): ...@@ -55,7 +62,7 @@ def parse_windows_result_to_users(result):
for line in task_result: for line in task_result:
user = space.split(line) user = space.split(line)
if user[0]: if user[0]:
users.append(user[0]) users[user[0]] = {}
return users return users
...@@ -82,8 +89,12 @@ def add_asset_users(assets, results): ...@@ -82,8 +89,12 @@ def add_asset_users(assets, results):
with tmp_to_org(asset.org_id): with tmp_to_org(asset.org_id):
GatheredUser.objects.filter(asset=asset, present=True)\ GatheredUser.objects.filter(asset=asset, present=True)\
.update(present=False) .update(present=False)
for username in users: for username, data in users.items():
defaults = {'asset': asset, 'username': username, 'present': True} defaults = {'asset': asset, 'username': username, 'present': True}
if data.get("ip"):
defaults["ip_last_login"] = data["ip"]
if data.get("date"):
defaults["date_last_login"] = data["date"]
GatheredUser.objects.update_or_create( GatheredUser.objects.update_or_create(
defaults=defaults, asset=asset, username=username, defaults=defaults, asset=asset, username=username,
) )
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-sm-9 col-lg-9 col-sm-offset-2"> <div class="col-sm-9 col-lg-9 col-sm-offset-2">
<div class="checkbox checkbox-success"> <div class="checkbox checkbox-success">
<input type="checkbox" name="enable_otp" checked id="id_enable_otp"><label for="id_enable_otp">{% trans 'Enable-MFA' %}</label> <input type="checkbox" name="enable_mfa" checked id="id_enable_mfa"><label for="id_enable_mfa">{% trans 'Enable-MFA' %}</label>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -135,7 +135,8 @@ function initAssetModalTable() { ...@@ -135,7 +135,8 @@ function initAssetModalTable() {
], ],
lengthMenu: [[10, 25, 50], [10, 25, 50]], lengthMenu: [[10, 25, 50], [10, 25, 50]],
pageLength: 10, pageLength: 10,
select_style: assetModalOption.selectStyle select_style: assetModalOption.selectStyle,
paging_numbers_length: 3
}; };
assetModalTable = jumpserver.initServerSideDataTable(options); assetModalTable = jumpserver.initServerSideDataTable(options);
if (assetModalOption.onModalTableDone) { if (assetModalOption.onModalTableDone) {
......
...@@ -303,9 +303,24 @@ function defaultCallback(action) { ...@@ -303,9 +303,24 @@ function defaultCallback(action) {
return logging return logging
} }
function toggle() {
if (show === 0) {
$("#split-left").hide(500, function () {
$("#split-right").attr("class", "col-lg-12");
$("#toggle-icon").attr("class", "fa fa-angle-right fa-x");
show = 1;
});
} else {
$("#split-right").attr("class", "col-lg-9");
$("#toggle-icon").attr("class", "fa fa-angle-left fa-x");
$("#split-left").show(500);
show = 0;
}
}
$(document).ready(function () { $(document).ready(function () {
$('.treebox').css('height', window.innerHeight - 180); $('.treebox').css('height', window.innerHeight - 60);
}) })
.on('click', '.btn-show-current-asset', function(){ .on('click', '.btn-show-current-asset', function(){
hideRMenu(); hideRMenu();
...@@ -320,6 +335,9 @@ $(document).ready(function () { ...@@ -320,6 +335,9 @@ $(document).ready(function () {
$('#show_current_asset').css('display', 'inline-block'); $('#show_current_asset').css('display', 'inline-block');
setCookie('show_current_asset', ''); setCookie('show_current_asset', '');
location.reload(); location.reload();
}).on('click', '.tree-toggle-btn', function (e) {
e.preventDefault();
toggle();
}) })
</script> </script>
{% extends '_base_list.html' %} {% extends '_base_list.html' %}
{% load i18n static %} {% load i18n static %}
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message">
{% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%} {% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%}
{% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%} {% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%}
</div>
{% endblock %} {% endblock %}
{% block table_search %} {% block table_search %}
<div class="" style="float: right"> <div class="" style="float: right">
......
...@@ -3,16 +3,16 @@ ...@@ -3,16 +3,16 @@
{% load i18n %} {% load i18n %}
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message"> {# <div class="alert alert-info help-message">#}
{# <button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button>#}
{# 左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产#} {# 左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产#}
{% trans 'The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node' %} {% trans 'The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node' %}
</div> {# </div>#}
{% endblock %} {% endblock %}
{% block custom_head_css_js %} {% block custom_head_css_js %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet"> {# <link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">#}
{# <link href="https://cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css" rel="stylesheet">#} {# <script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>#}
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
<script src="{% static 'js/jquery.form.min.js' %}"></script> <script src="{% static 'js/jquery.form.min.js' %}"></script>
<style type="text/css"> <style type="text/css">
div#rMenu { div#rMenu {
...@@ -48,12 +48,12 @@ ...@@ -48,12 +48,12 @@
{% block content %} {% block content %}
<div class="wrapper wrapper-content"> <div class="wrapper wrapper-content">
<div class="row"> <div class="row">
<div class="col-lg-3" id="split-left" style="padding-left: 3px"> <div class="col-lg-3" id="split-left" style="padding-left: 3px;padding-right: 0">
{% include 'assets/_node_tree.html' %} {% include 'assets/_node_tree.html' %}
</div> </div>
<div class="col-lg-9 animated fadeInRight" id="split-right"> <div class="col-lg-9 animated fadeInRight" id="split-right">
<div class="tree-toggle"> <div class="tree-toggle" style="z-index: 9999">
<div class="btn btn-sm btn-primary tree-toggle-btn" onclick="toggle()"> <div class="btn btn-sm btn-primary tree-toggle-btn">
<i class="fa fa-angle-left fa-x" id="toggle-icon"></i> <i class="fa fa-angle-left fa-x" id="toggle-icon"></i>
</div> </div>
</div> </div>
...@@ -151,9 +151,9 @@ function initTable() { ...@@ -151,9 +151,9 @@ function initTable() {
}}, }},
{targets: 4, createdCell: function (td, cellData, rowData) { {targets: 4, createdCell: function (td, cellData, rowData) {
var innerHtml = ""; var innerHtml = "";
if (cellData.status == 1) { if (cellData.status === 1) {
innerHtml = '<i class="fa fa-circle text-navy"></i>' innerHtml = '<i class="fa fa-circle text-navy"></i>'
} else if (cellData.status == 0) { } else if (cellData.status === 0) {
innerHtml = '<i class="fa fa-circle text-danger"></i>' innerHtml = '<i class="fa fa-circle text-danger"></i>'
} else { } else {
innerHtml = '<i class="fa fa-circle text-warning"></i>' innerHtml = '<i class="fa fa-circle text-warning"></i>'
...@@ -386,6 +386,10 @@ $(document).ready(function(){ ...@@ -386,6 +386,10 @@ $(document).ready(function(){
setTimeout( function () {window.location.reload();}, 300); setTimeout( function () {window.location.reload();}, 300);
} }
function reloadTable() {
asset_table.ajax.reload();
}
function doDeactive() { function doDeactive() {
var data = []; var data = [];
$.each(id_list, function(index, object_id) { $.each(id_list, function(index, object_id) {
...@@ -396,7 +400,7 @@ $(document).ready(function(){ ...@@ -396,7 +400,7 @@ $(document).ready(function(){
url: the_url, url: the_url,
method: 'PATCH', method: 'PATCH',
body: JSON.stringify(data), body: JSON.stringify(data),
success: refreshPage success: reloadTable
}); });
} }
function doActive() { function doActive() {
...@@ -409,7 +413,7 @@ $(document).ready(function(){ ...@@ -409,7 +413,7 @@ $(document).ready(function(){
url: the_url, url: the_url,
method: 'PATCH', method: 'PATCH',
body: JSON.stringify(data), body: JSON.stringify(data),
success: refreshPage success: reloadTable
}); });
} }
function doDelete() { function doDelete() {
...@@ -431,7 +435,7 @@ $(document).ready(function(){ ...@@ -431,7 +435,7 @@ $(document).ready(function(){
success: function () { success: function () {
var msg = "{% trans 'Asset Deleted.' %}"; var msg = "{% trans 'Asset Deleted.' %}";
swal("{% trans 'Asset Delete' %}", msg, "success"); swal("{% trans 'Asset Delete' %}", msg, "success");
refreshPage(); reloadTable();
}, },
flash_message: false, flash_message: false,
}); });
...@@ -478,16 +482,12 @@ $(document).ready(function(){ ...@@ -478,16 +482,12 @@ $(document).ready(function(){
'assets': id_list 'assets': id_list
}; };
var success = function () {
asset_table.ajax.reload()
};
var url = "{% url 'api-assets:node-remove-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id); var url = "{% url 'api-assets:node-remove-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
requestApi({ requestApi({
'url': url, 'url': url,
'method': 'PUT', 'method': 'PUT',
'body': JSON.stringify(data), 'body': JSON.stringify(data),
'success': success 'success': reloadTable
}) })
} }
......
...@@ -2,14 +2,12 @@ ...@@ -2,14 +2,12 @@
{% load i18n static %} {% load i18n static %}
{% block table_search %}{% endblock %} {% block table_search %}{% endblock %}
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message">
{% trans 'System user bound some command filter, each command filter has some rules,'%} {% trans 'System user bound some command filter, each command filter has some rules,'%}
{% trans 'When user login asset with this system user, then run a command,' %} {% trans 'When user login asset with this system user, then run a command,' %}
{% trans 'The command will be filter by rules, higher priority rule run first,' %} {% trans 'The command will be filter by rules, higher priority rule run first,' %}
{% trans 'When a rule matched, if rule action is allow, then allow command execute,' %} {% trans 'When a rule matched, if rule action is allow, then allow command execute,' %}
{% trans 'else if action is deny, then command with be deny,' %} {% trans 'else if action is deny, then command with be deny,' %}
{% trans 'else match next rule, if none matched, allowed' %} {% trans 'else match next rule, if none matched, allowed' %}
</div>
{% endblock %} {% endblock %}
{% block table_container %} {% block table_container %}
<div class="uc pull-left m-r-5"> <div class="uc pull-left m-r-5">
......
...@@ -3,13 +3,9 @@ ...@@ -3,13 +3,9 @@
{% block table_search %}{% endblock %} {% block table_search %}{% endblock %}
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message">
{# 网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。<br>#}
{# JMS => 网域网关 => 目标资产#}
{% trans 'The domain function is added to address the fact that some environments (such as the hybrid cloud) cannot be connected directly by jumping on the gateway server.' %} {% trans 'The domain function is added to address the fact that some environments (such as the hybrid cloud) cannot be connected directly by jumping on the gateway server.' %}
<br> <br>
{% trans 'JMS => Domain gateway => Target assets' %} {% trans 'JMS => Domain gateway => Target assets' %}
</div>
{% endblock %} {% endblock %}
{% block table_container %} {% block table_container %}
......
...@@ -2,11 +2,9 @@ ...@@ -2,11 +2,9 @@
{% load i18n %} {% load i18n %}
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message">
{% trans 'System user is Jumpserver jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); '%} {% trans 'System user is Jumpserver jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); '%}
{% trans 'In simple terms, users log into Jumpserver using their own username, and Jumpserver uses system users to log into assets. '%} {% trans 'In simple terms, users log into Jumpserver using their own username, and Jumpserver uses system users to log into assets. '%}
{% trans 'When system users are created, if you choose auto push Jumpserver to use Ansible push system users into the asset, if the asset (Switch) does not support ansible, please manually fill in the account password.' %} {% trans 'When system users are created, if you choose auto push Jumpserver to use Ansible push system users into the asset, if the asset (Switch) does not support ansible, please manually fill in the account password.' %}
</div>
{% endblock %} {% endblock %}
{% block table_search %} {% block table_search %}
......
...@@ -7,7 +7,7 @@ from django.views.generic.detail import SingleObjectMixin ...@@ -7,7 +7,7 @@ from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from common.permissions import PermissionsMixin ,IsOrgAdmin from common.permissions import PermissionsMixin, IsOrgAdmin
from common.const import create_success_msg, update_success_msg from common.const import create_success_msg, update_success_msg
from common.utils import get_object_or_none from common.utils import get_object_or_none
from ..models import Domain, Gateway from ..models import Domain, Gateway
......
...@@ -110,5 +110,14 @@ class UserLoginLog(models.Model): ...@@ -110,5 +110,14 @@ class UserLoginLog(models.Model):
login_logs = login_logs.filter(username__in=username_list) login_logs = login_logs.filter(username__in=username_list)
return login_logs return login_logs
@property
def reason_display(self):
from authentication.errors import reason_choices, old_reason_choices
reason = reason_choices.get(self.reason)
if reason:
return reason
reason = old_reason_choices.get(self.reason, self.reason)
return reason
class Meta: class Meta:
ordering = ['-datetime', 'username'] ordering = ['-datetime', 'username']
...@@ -4,15 +4,18 @@ ...@@ -4,15 +4,18 @@
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.db import transaction from django.db import transaction
from django.utils import timezone
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework.request import Request
from jumpserver.utils import current_request from jumpserver.utils import current_request
from common.utils import get_request_ip, get_logger, get_syslogger from common.utils import get_request_ip, get_logger, get_syslogger
from users.models import User from users.models import User
from authentication.signals import post_auth_failed, post_auth_success
from terminal.models import Session, Command from terminal.models import Session, Command
from terminal.backends.command.serializers import SessionCommandSerializer from terminal.backends.command.serializers import SessionCommandSerializer
from . import models from . import models, serializers
from . import serializers from .tasks import write_login_log_async
logger = get_logger(__name__) logger = get_logger(__name__)
sys_logger = get_syslogger("audits") sys_logger = get_syslogger("audits")
...@@ -99,3 +102,39 @@ def on_audits_log_create(sender, instance=None, **kwargs): ...@@ -99,3 +102,39 @@ def on_audits_log_create(sender, instance=None, **kwargs):
data = json_render.render(s.data).decode(errors='ignore') data = json_render.render(s.data).decode(errors='ignore')
msg = "{} - {}".format(category, data) msg = "{} - {}".format(category, data)
sys_logger.info(msg) sys_logger.info(msg)
def generate_data(username, request):
user_agent = request.META.get('HTTP_USER_AGENT', '')
if isinstance(request, Request):
login_ip = request.data.get('remote_addr', '0.0.0.0')
login_type = request.data.get('login_type', '')
else:
login_ip = get_request_ip(request) or '0.0.0.0'
login_type = 'W'
data = {
'username': username,
'ip': login_ip,
'type': login_type,
'user_agent': user_agent,
'datetime': timezone.now()
}
return data
@receiver(post_auth_success)
def on_user_auth_success(sender, user, request, **kwargs):
logger.debug('User login success: {}'.format(user.username))
data = generate_data(user.username, request)
data.update({'mfa': int(user.mfa_enabled), 'status': True})
write_login_log_async.delay(**data)
@receiver(post_auth_failed)
def on_user_auth_failed(sender, username, request, reason, **kwargs):
logger.debug('User login failed: {}'.format(username))
data = generate_data(username, request)
data.update({'reason': reason, 'status': False})
write_login_log_async.delay(**data)
...@@ -7,6 +7,7 @@ from celery import shared_task ...@@ -7,6 +7,7 @@ from celery import shared_task
from ops.celery.decorator import register_as_period_task from ops.celery.decorator import register_as_period_task
from .models import UserLoginLog from .models import UserLoginLog
from .utils import write_login_log
@register_as_period_task(interval=3600*24) @register_as_period_task(interval=3600*24)
...@@ -19,3 +20,8 @@ def clean_login_log_period(): ...@@ -19,3 +20,8 @@ def clean_login_log_period():
days = 90 days = 90
expired_day = now - datetime.timedelta(days=days) expired_day = now - datetime.timedelta(days=days)
UserLoginLog.objects.filter(datetime__lt=expired_day).delete() UserLoginLog.objects.filter(datetime__lt=expired_day).delete()
@shared_task
def write_login_log_async(*args, **kwargs):
write_login_log(*args, **kwargs)
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
<td class="text-center">{{ login_log.ip }}</td> <td class="text-center">{{ login_log.ip }}</td>
<td class="text-center">{{ login_log.city }}</td> <td class="text-center">{{ login_log.city }}</td>
<td class="text-center">{{ login_log.get_mfa_display }}</td> <td class="text-center">{{ login_log.get_mfa_display }}</td>
<td class="text-center">{% trans login_log.reason %}</td> <td class="text-center">{{ login_log.reason_display }}</td>
<td class="text-center">{{ login_log.get_status_display }}</td> <td class="text-center">{{ login_log.get_status_display }}</td>
<td class="text-center">{{ login_log.datetime }}</td> <td class="text-center">{{ login_log.datetime }}</td>
</tr> </tr>
......
import csv import csv
import codecs import codecs
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.translation import ugettext as _
from common.utils import validate_ip, get_ip_city
def get_excel_response(filename): def get_excel_response(filename):
...@@ -20,3 +23,16 @@ def write_content_to_excel(response, header=None, login_logs=None, fields=None): ...@@ -20,3 +23,16 @@ def write_content_to_excel(response, header=None, login_logs=None, fields=None):
data = [getattr(log, field.name) for field in fields] data = [getattr(log, field.name) for field in fields]
writer.writerow(data) writer.writerow(data)
return response return response
def write_login_log(*args, **kwargs):
from audits.models import UserLoginLog
default_city = _("Unknown")
ip = kwargs.get('ip') or ''
if not (ip and validate_ip(ip)):
ip = ip[:15]
city = default_city
else:
city = get_ip_city(ip) or default_city
kwargs.update({'ip': ip, 'city': city})
UserLoginLog.objects.create(**kwargs)
...@@ -5,3 +5,4 @@ from .auth import * ...@@ -5,3 +5,4 @@ from .auth import *
from .token import * from .token import *
from .mfa import * from .mfa import *
from .access_key import * from .access_key import *
from .login_confirm import *
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import uuid import uuid
import time
from django.core.cache import cache from django.core.cache import cache
from django.urls import reverse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from rest_framework.views import APIView from rest_framework.views import APIView
from common.utils import get_logger, get_request_ip from common.utils import get_logger
from common.permissions import IsOrgAdminOrAppUser, IsValidUser from common.permissions import IsOrgAdminOrAppUser
from orgs.mixins.api import RootOrgViewMixin from orgs.mixins.api import RootOrgViewMixin
from users.serializers import UserSerializer
from users.models import User from users.models import User
from assets.models import Asset, SystemUser from assets.models import Asset, SystemUser
from audits.models import UserLoginLog as LoginLog
from users.utils import (
check_otp_code, increase_login_failed_count,
is_block_login, clean_failed_count
)
from .. import const
from ..utils import check_user_valid
from ..serializers import OtpVerifySerializer
from ..signals import post_auth_success, post_auth_failed
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = [ __all__ = [
'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi', 'UserConnectionTokenApi',
'UserOtpVerifyApi',
] ]
class UserAuthApi(RootOrgViewMixin, APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
def get_serializer_context(self):
return {
'request': self.request,
'view': self
}
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def post(self, request):
# limit login
username = request.data.get('username')
ip = request.data.get('remote_addr', None)
ip = ip or get_request_ip(request)
if is_block_login(username, ip):
msg = _("Log in frequently and try again later")
logger.warn(msg + ': ' + username + ':' + ip)
return Response({'msg': msg}, status=401)
user, msg = self.check_user_valid(request)
if not user:
username = request.data.get('username', '')
self.send_auth_signal(success=False, username=username, reason=msg)
increase_login_failed_count(username, ip)
return Response({'msg': msg}, status=401)
if not user.otp_enabled:
self.send_auth_signal(success=True, user=user)
# 登陆成功,清除原来的缓存计数
clean_failed_count(username, ip)
token, expired_at = user.create_bearer_token(request)
return Response(
{'token': token, 'user': self.get_serializer(user).data}
)
seed = uuid.uuid4().hex
cache.set(seed, user, 300)
return Response(
{
'code': 101,
'msg': _('Please carry seed value and '
'conduct MFA secondary certification'),
'otp_url': reverse('api-auth:user-otp-auth'),
'seed': seed,
'user': self.get_serializer(user).data
}, status=300
)
@staticmethod
def check_user_valid(request):
username = request.data.get('username', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, password=password,
public_key=public_key
)
return user, msg
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
class UserConnectionTokenApi(RootOrgViewMixin, APIView): class UserConnectionTokenApi(RootOrgViewMixin, APIView):
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
...@@ -153,59 +61,5 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView): ...@@ -153,59 +61,5 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView):
return super().get_permissions() return super().get_permissions()
class UserOtpAuthApi(RootOrgViewMixin, APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
def get_serializer_context(self):
return {
'request': self.request,
'view': self
}
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def post(self, request):
otp_code = request.data.get('otp_code', '')
seed = request.data.get('seed', '')
user = cache.get(seed, None)
if not user:
return Response(
{'msg': _('Please verify the user name and password first')},
status=401
)
if not check_otp_code(user.otp_secret_key, otp_code):
self.send_auth_signal(success=False, username=user.username, reason=const.mfa_failed)
return Response({'msg': _('MFA certification failed')}, status=401)
self.send_auth_signal(success=True, user=user)
token, expired_at = user.create_bearer_token(request)
data = {'token': token, 'user': self.get_serializer(user).data}
return Response(data)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
class UserOtpVerifyApi(CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = OtpVerifySerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
code = serializer.validated_data["code"]
if request.user.check_otp(code):
request.session["MFA_VERIFY_TIME"] = int(time.time())
return Response({"ok": "1"})
else:
return Response({"error": "Code not valid"}, status=400)
# -*- coding: utf-8 -*-
#
from rest_framework.generics import UpdateAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from common.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin
from ..models import LoginConfirmSetting
from ..serializers import LoginConfirmSettingSerializer
from .. import errors, mixins
__all__ = ['LoginConfirmSettingUpdateApi', 'TicketStatusApi']
logger = get_logger(__name__)
class LoginConfirmSettingUpdateApi(UpdateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = LoginConfirmSettingSerializer
def get_object(self):
from users.models import User
user_id = self.kwargs.get('user_id')
user = get_object_or_404(User, pk=user_id)
defaults = {'user': user}
s, created = LoginConfirmSetting.objects.get_or_create(
defaults, user=user,
)
return s
class TicketStatusApi(mixins.AuthMixin, APIView):
permission_classes = ()
def get_ticket(self):
from tickets.models import Ticket
ticket_id = self.request.session.get("auth_ticket_id")
logger.debug('Login confirm ticket id: {}'.format(ticket_id))
if not ticket_id:
ticket = None
else:
ticket = get_object_or_none(Ticket, pk=ticket_id)
return ticket
def get(self, request, *args, **kwargs):
try:
self.check_user_login_confirm()
return Response({"msg": "ok"})
except errors.NeedMoreInfoError as e:
return Response(e.as_data(), status=200)
def delete(self, request, *args, **kwargs):
ticket = self.get_ticket()
if ticket:
request.session.pop('auth_ticket_id', '')
ticket.perform_status('closed', request.user)
return Response('', status=200)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import time
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView
from rest_framework.serializers import ValidationError
from rest_framework.response import Response
from common.permissions import IsValidUser
from ..serializers import OtpVerifySerializer
from .. import serializers from .. import serializers
from .. import errors
from ..mixins import AuthMixin
class MFAChallengeApi(CreateAPIView): __all__ = ['MFAChallengeApi', 'UserOtpVerifyApi']
class MFAChallengeApi(AuthMixin, CreateAPIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = serializers.MFAChallengeSerializer serializer_class = serializers.MFAChallengeSerializer
def perform_create(self, serializer):
try:
user = self.get_user_from_session()
code = serializer.validated_data.get('code')
valid = user.check_mfa(code)
if not valid:
self.request.session['auth_mfa'] = ''
raise errors.MFAFailedError(
username=user.username, request=self.request
)
else:
self.request.session['auth_mfa'] = '1'
except errors.AuthFailedError as e:
data = {"error": e.error, "msg": e.msg}
raise ValidationError(data)
except errors.NeedMoreInfoError as e:
return Response(e.as_data(), status=200)
def create(self, request, *args, **kwargs):
super().create(request, *args, **kwargs)
return Response({'msg': 'ok'})
class UserOtpVerifyApi(CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = OtpVerifySerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
code = serializer.validated_data["code"]
if request.user.check_mfa(code):
request.session["MFA_VERIFY_TIME"] = int(time.time())
return Response({"ok": "1"})
else:
return Response({"error": "Code not valid"}, status=400)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import uuid
from django.core.cache import cache
from django.utils.translation import ugettext as _
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView
from drf_yasg.utils import swagger_auto_schema
from common.utils import get_request_ip, get_logger from common.utils import get_logger
from users.utils import (
check_otp_code, increase_login_failed_count, from .. import serializers, errors
is_block_login, clean_failed_count from ..mixins import AuthMixin
)
from ..utils import check_user_valid
from ..signals import post_auth_success, post_auth_failed
from .. import serializers
logger = get_logger(__name__) logger = get_logger(__name__)
...@@ -25,71 +16,26 @@ logger = get_logger(__name__) ...@@ -25,71 +16,26 @@ logger = get_logger(__name__)
__all__ = ['TokenCreateApi'] __all__ = ['TokenCreateApi']
class AuthFailedError(Exception): class TokenCreateApi(AuthMixin, CreateAPIView):
def __init__(self, msg, reason=None):
self.msg = msg
self.reason = reason
class MFARequiredError(Exception):
pass
class TokenCreateApi(CreateAPIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = serializers.BearerTokenSerializer serializer_class = serializers.BearerTokenSerializer
@staticmethod def create_session_if_need(self):
def check_is_block(username, ip): if self.request.session.is_empty():
if is_block_login(username, ip): self.request.session.create()
msg = _("Log in frequently and try again later")
logger.warn(msg + ': ' + username + ':' + ip)
raise AuthFailedError(msg)
def check_user_valid(self):
request = self.request
username = request.data.get('username', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, password=password,
public_key=public_key
)
if not user:
raise AuthFailedError(msg)
return user
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
username = self.request.data.get('username') self.create_session_if_need()
ip = self.request.data.get('remote_addr', None) # 如果认证没有过,检查账号密码
ip = ip or get_request_ip(self.request)
user = None
try: try:
self.check_is_block(username, ip) user = self.check_user_auth_if_need()
user = self.check_user_valid() self.check_user_mfa_if_need(user)
if user.otp_enabled: self.check_user_login_confirm_if_need(user)
raise MFARequiredError()
self.send_auth_signal(success=True, user=user) self.send_auth_signal(success=True, user=user)
clean_failed_count(username, ip) self.clear_auth_mark()
return super().create(request, *args, **kwargs) resp = super().create(request, *args, **kwargs)
except AuthFailedError as e: return resp
increase_login_failed_count(username, ip) except errors.AuthFailedError as e:
self.send_auth_signal(success=False, user=user, username=username, reason=str(e)) return Response(e.as_data(), status=400)
return Response({'msg': str(e)}, status=401) except errors.NeedMoreInfoError as e:
except MFARequiredError: return Response(e.as_data(), status=200)
msg = _("MFA required")
seed = uuid.uuid4().hex
cache.set(seed, user.username, 300)
resp = {'msg': msg, "choices": ["otp"], "req": seed}
return Response(resp, status=300)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(
sender=self.__class__, user=user, request=self.request
)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
# -*- coding: utf-8 -*-
#
from django.contrib.auth import get_user_model
UserModel = get_user_model()
__all__ = ['PublicKeyAuthBackend']
class PublicKeyAuthBackend:
def authenticate(self, request, username=None, public_key=None, **kwargs):
if not public_key:
return None
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
return None
else:
if user.check_public_key(public_key) and \
self.user_can_authenticate(user):
return user
@staticmethod
def user_can_authenticate(user):
"""
Reject users with is_active=False. Custom user models that don't have
that attribute are allowed.
"""
is_active = getattr(user, 'is_active', None)
return is_active or is_active is None
def get_user(self, user_id):
try:
user = UserModel._default_manager.get(pk=user_id)
except UserModel.DoesNotExist:
return None
return user if self.user_can_authenticate(user) else None
...@@ -5,6 +5,8 @@ from django.contrib.auth import get_user_model ...@@ -5,6 +5,8 @@ from django.contrib.auth import get_user_model
from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend
from django.conf import settings from django.conf import settings
from pyrad.packet import AccessRequest
User = get_user_model() User = get_user_model()
......
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
password_failed = _('Username/password check failed')
mfa_failed = _('MFA authentication failed')
user_not_exist = _("Username does not exist")
password_expired = _("Password expired")
user_invalid = _('Disabled or expired')
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse
from django.conf import settings
from .signals import post_auth_failed
from users.utils import (
increase_login_failed_count, get_login_failed_count
)
reason_password_failed = 'password_failed'
reason_mfa_failed = 'mfa_failed'
reason_user_not_exist = 'user_not_exist'
reason_password_expired = 'password_expired'
reason_user_invalid = 'user_invalid'
reason_user_inactive = 'user_inactive'
reason_choices = {
reason_password_failed: _('Username/password check failed'),
reason_mfa_failed: _('MFA authentication failed'),
reason_user_not_exist: _("Username does not exist"),
reason_password_expired: _("Password expired"),
reason_user_invalid: _('Disabled or expired'),
reason_user_inactive: _("This account is inactive.")
}
old_reason_choices = {
'0': '-',
'1': reason_choices[reason_password_failed],
'2': reason_choices[reason_mfa_failed],
'3': reason_choices[reason_user_not_exist],
'4': reason_choices[reason_password_expired],
}
session_empty_msg = _("No session found, check your cookie")
invalid_login_msg = _(
"The username or password you entered is incorrect, "
"please enter it again. "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
block_login_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
mfa_failed_msg = _("MFA code invalid, or ntp sync server time")
mfa_required_msg = _("MFA required")
login_confirm_required_msg = _("Login confirm required")
login_confirm_wait_msg = _("Wait login confirm ticket for accept")
login_confirm_error_msg = _("Login confirm ticket was {}")
class AuthFailedNeedLogMixin:
username = ''
request = None
error = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
post_auth_failed.send(
sender=self.__class__, username=self.username,
request=self.request, reason=self.error
)
class AuthFailedNeedBlockMixin:
username = ''
ip = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
increase_login_failed_count(self.username, self.ip)
class AuthFailedError(Exception):
username = ''
msg = ''
error = ''
request = None
ip = ''
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
}
class CredentialError(AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, AuthFailedError):
def __init__(self, error, username, ip, request):
super().__init__(error=error, username=username, ip=ip, request=request)
times_up = settings.SECURITY_LOGIN_LIMIT_COUNT
times_failed = get_login_failed_count(username, ip)
times_try = int(times_up) - int(times_failed)
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
default_msg = invalid_login_msg.format(
times_try=times_try, block_time=block_time
)
if error == reason_password_failed:
self.msg = default_msg
else:
self.msg = reason_choices.get(error, default_msg)
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
error = reason_mfa_failed
msg = mfa_failed_msg
def __init__(self, username, request):
super().__init__(username=username, request=request)
class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
error = 'block_login'
msg = block_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
def __init__(self, username, ip):
super().__init__(username=username, ip=ip)
class SessionEmptyError(AuthFailedError):
msg = session_empty_msg
error = 'session_empty'
class NeedMoreInfoError(Exception):
error = ''
msg = ''
def __init__(self, error='', msg=''):
if error:
self.error = error
if msg:
self.msg = msg
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
}
class MFARequiredError(NeedMoreInfoError):
msg = mfa_required_msg
error = 'mfa_required'
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
'data': {
'choices': ['otp'],
'url': reverse('api-auth:mfa-challenge')
}
}
class LoginConfirmBaseError(NeedMoreInfoError):
def __init__(self, ticket_id, **kwargs):
self.ticket_id = ticket_id
super().__init__(**kwargs)
def as_data(self):
return {
"error": self.error,
"msg": self.msg,
"data": {
"ticket_id": self.ticket_id
}
}
class LoginConfirmWaitError(LoginConfirmBaseError):
msg = login_confirm_wait_msg
error = 'login_confirm_wait'
class LoginConfirmOtherError(LoginConfirmBaseError):
error = 'login_confirm_error'
def __init__(self, ticket_id, status):
msg = login_confirm_error_msg.format(status)
super().__init__(ticket_id=ticket_id, msg=msg)
...@@ -9,53 +9,19 @@ from django.conf import settings ...@@ -9,53 +9,19 @@ from django.conf import settings
from users.utils import get_login_failed_count from users.utils import get_login_failed_count
class UserLoginForm(AuthenticationForm): class UserLoginForm(forms.Form):
username = forms.CharField(label=_('Username'), max_length=100) username = forms.CharField(label=_('Username'), max_length=100)
password = forms.CharField( password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput, label=_('Password'), widget=forms.PasswordInput,
max_length=128, strip=False max_length=128, strip=False
) )
error_messages = {
'invalid_login': _(
"The username or password you entered is incorrect, "
"please enter it again."
),
'inactive': _("This account is inactive."),
'limit_login': _(
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
),
'block_login': _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
}
def confirm_login_allowed(self, user): def confirm_login_allowed(self, user):
if not user.is_staff: if not user.is_staff:
raise forms.ValidationError( raise forms.ValidationError(
self.error_messages['inactive'], self.error_messages['inactive'],
code='inactive',) code='inactive',
def get_limit_login_error_message(self, username, ip):
times_up = settings.SECURITY_LOGIN_LIMIT_COUNT
times_failed = get_login_failed_count(username, ip)
times_try = int(times_up) - int(times_failed)
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_try <= 0:
error_message = self.error_messages['block_login']
error_message = error_message.format(block_time)
else:
error_message = self.error_messages['limit_login']
error_message = error_message.format(
times_try=times_try, block_time=block_time,
) )
return error_message
def add_limit_login_error(self, username, ip):
error = self.get_limit_login_error_message(username, ip)
self.add_error('password', error)
class UserLoginCaptchaForm(UserLoginForm): class UserLoginCaptchaForm(UserLoginForm):
......
# Generated by Django 2.2.5 on 2019-10-31 10:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('authentication', '0002_auto_20190729_1423'),
]
operations = [
migrations.CreateModel(
name='LoginConfirmSetting',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('reviewers', models.ManyToManyField(blank=True, related_name='review_login_confirm_settings', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='login_confirm_setting', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'abstract': False,
},
),
]
# -*- coding: utf-8 -*-
#
import time
from django.conf import settings
from common.utils import get_object_or_none, get_request_ip, get_logger
from users.models import User
from users.utils import (
is_block_login, clean_failed_count, increase_login_failed_count
)
from . import errors
from .utils import check_user_valid
from .signals import post_auth_success, post_auth_failed
logger = get_logger(__name__)
class AuthMixin:
request = None
def get_user_from_session(self):
if self.request.session.is_empty():
raise errors.SessionEmptyError()
if self.request.user and not self.request.user.is_anonymous:
return self.request.user
user_id = self.request.session.get('user_id')
if not user_id:
user = None
else:
user = get_object_or_none(User, pk=user_id)
if not user:
raise errors.SessionEmptyError()
user.backend = self.request.session.get("auth_backend")
return user
def get_request_ip(self):
ip = ''
if hasattr(self.request, 'data'):
ip = self.request.data.get('remote_addr', '')
ip = ip or get_request_ip(self.request)
return ip
def check_is_block(self):
if hasattr(self.request, 'data'):
username = self.request.data.get("username")
else:
username = self.request.POST.get("username")
ip = self.get_request_ip()
if is_block_login(username, ip):
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
raise errors.BlockLoginError(username=username, ip=ip)
def check_user_auth(self):
self.check_is_block()
request = self.request
if hasattr(request, 'data'):
username = request.data.get('username', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
else:
username = request.POST.get('username', '')
password = request.POST.get('password', '')
public_key = request.POST.get('public_key', '')
user, error = check_user_valid(
username=username, password=password,
public_key=public_key
)
ip = self.get_request_ip()
if not user:
raise errors.CredentialError(
username=username, error=error, ip=ip, request=request
)
clean_failed_count(username, ip)
request.session['auth_password'] = 1
request.session['user_id'] = str(user.id)
auth_backend = getattr(user, 'backend', 'django.contrib.auth.backends.ModelBackend')
request.session['auth_backend'] = auth_backend
return user
def check_user_auth_if_need(self):
request = self.request
if request.session.get('auth_password') and \
request.session.get('user_id'):
user = self.get_user_from_session()
if user:
return user
return self.check_user_auth()
def check_user_mfa_if_need(self, user):
if self.request.session.get('auth_mfa'):
return
if not user.mfa_enabled:
return
if not user.otp_secret_key and user.mfa_is_otp():
return
raise errors.MFARequiredError()
def check_user_mfa(self, code):
user = self.get_user_from_session()
ok = user.check_mfa(code)
if ok:
self.request.session['auth_mfa'] = 1
self.request.session['auth_mfa_time'] = time.time()
self.request.session['auth_mfa_type'] = 'otp'
return
raise errors.MFAFailedError(username=user.username, request=self.request)
def get_ticket(self):
from tickets.models import Ticket
ticket_id = self.request.session.get("auth_ticket_id")
logger.debug('Login confirm ticket id: {}'.format(ticket_id))
if not ticket_id:
ticket = None
else:
ticket = get_object_or_none(Ticket, pk=ticket_id)
return ticket
def get_ticket_or_create(self, confirm_setting):
ticket = self.get_ticket()
if not ticket or ticket.status == ticket.STATUS_CLOSED:
ticket = confirm_setting.create_confirm_ticket(self.request)
self.request.session['auth_ticket_id'] = str(ticket.id)
return ticket
def check_user_login_confirm(self):
ticket = self.get_ticket()
if not ticket:
raise errors.LoginConfirmOtherError('', "Not found")
if ticket.status == ticket.STATUS_OPEN:
raise errors.LoginConfirmWaitError(ticket.id)
elif ticket.action == ticket.ACTION_APPROVE:
self.request.session["auth_confirm"] = "1"
return
elif ticket.action == ticket.ACTION_REJECT:
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_action_display()
)
else:
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_status_display()
)
def check_user_login_confirm_if_need(self, user):
if not settings.CONFIG.LOGIN_CONFIRM_ENABLE:
return
confirm_setting = user.get_login_confirm_setting()
if self.request.session.get('auth_confirm') or not confirm_setting:
return
self.get_ticket_or_create(confirm_setting)
self.check_user_login_confirm()
def clear_auth_mark(self):
self.request.session['auth_password'] = ''
self.request.session['auth_user_id'] = ''
self.request.session['auth_mfa'] = ''
self.request.session['auth_confirm'] = ''
self.request.session['auth_ticket_id'] = ''
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(
sender=self.__class__, user=user, request=self.request
)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
import uuid import uuid
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext as __
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from django.conf import settings from django.conf import settings
from common.mixins.models import CommonModelMixin
from common.utils import get_object_or_none, get_request_ip, get_ip_city
class AccessKey(models.Model): class AccessKey(models.Model):
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True,
...@@ -33,3 +37,42 @@ class PrivateToken(Token): ...@@ -33,3 +37,42 @@ class PrivateToken(Token):
class Meta: class Meta:
verbose_name = _('Private Token') verbose_name = _('Private Token')
class LoginConfirmSetting(CommonModelMixin):
user = models.OneToOneField('users.User', on_delete=models.CASCADE, verbose_name=_("User"), related_name="login_confirm_setting")
reviewers = models.ManyToManyField('users.User', verbose_name=_("Reviewers"), related_name="review_login_confirm_settings", blank=True)
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
@classmethod
def get_user_confirm_setting(cls, user):
return get_object_or_none(cls, user=user)
def create_confirm_ticket(self, request=None):
from tickets.models import Ticket
title = _('Login confirm') + '{}'.format(self.user)
if request:
remote_addr = get_request_ip(request)
city = get_ip_city(remote_addr)
datetime = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
body = __("{user_key}: {username}<br>"
"IP: {ip}<br>"
"{city_key}: {city}<br>"
"{date_key}: {date}<br>").format(
user_key=__("User"), username=self.user,
ip=remote_addr, city_key=_("City"), city=city,
date_key=__("Datetime"), date=datetime
)
else:
body = ''
reviewer = self.reviewers.all()
ticket = Ticket.objects.create(
user=self.user, title=title, body=body,
type=Ticket.TYPE_LOGIN_CONFIRM,
)
ticket.assignees.set(reviewer)
return ticket
def __str__(self):
return '{} confirm'.format(self.user.username)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.core.cache import cache
from rest_framework import serializers from rest_framework import serializers
from common.utils import get_object_or_none
from users.models import User from users.models import User
from .models import AccessKey from users.serializers import UserProfileSerializer
from .models import AccessKey, LoginConfirmSetting
__all__ = [ __all__ = [
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer', 'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
'MFAChallengeSerializer', 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer',
] ]
class AccessKeySerializer(serializers.ModelSerializer): class AccessKeySerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = AccessKey model = AccessKey
fields = ['id', 'secret', 'is_active', 'date_created'] fields = ['id', 'secret', 'is_active', 'date_created']
...@@ -25,65 +25,54 @@ class OtpVerifySerializer(serializers.Serializer): ...@@ -25,65 +25,54 @@ class OtpVerifySerializer(serializers.Serializer):
code = serializers.CharField(max_length=6, min_length=6) code = serializers.CharField(max_length=6, min_length=6)
class BearerTokenMixin(serializers.Serializer): class BearerTokenSerializer(serializers.Serializer):
username = serializers.CharField(allow_null=True, required=False, write_only=True)
password = serializers.CharField(write_only=True, allow_null=True,
required=False, allow_blank=True)
public_key = serializers.CharField(write_only=True, allow_null=True,
allow_blank=True, required=False)
token = serializers.CharField(read_only=True) token = serializers.CharField(read_only=True)
keyword = serializers.SerializerMethodField() keyword = serializers.SerializerMethodField()
date_expired = serializers.DateTimeField(read_only=True) date_expired = serializers.DateTimeField(read_only=True)
user = UserProfileSerializer(read_only=True)
@staticmethod @staticmethod
def get_keyword(obj): def get_keyword(obj):
return 'Bearer' return 'Bearer'
def create_response(self, username): def create(self, validated_data):
request = self.context.get("request") request = self.context.get('request')
try: if request.user and not request.user.is_anonymous:
user = User.objects.get(username=username) user = request.user
except User.DoesNotExist: else:
raise serializers.ValidationError("username %s not exist" % username) user_id = request.session.get('user_id')
user = get_object_or_none(User, pk=user_id)
if not user:
raise serializers.ValidationError(
"user id {} not exist".format(user_id)
)
token, date_expired = user.create_bearer_token(request) token, date_expired = user.create_bearer_token(request)
instance = { instance = {
"username": username,
"token": token, "token": token,
"date_expired": date_expired, "date_expired": date_expired,
"user": user
} }
return instance return instance
def update(self, instance, validated_data):
pass
class BearerTokenSerializer(BearerTokenMixin, serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField(write_only=True, allow_null=True,
required=False)
public_key = serializers.CharField(write_only=True, allow_null=True,
required=False)
def create(self, validated_data): class MFAChallengeSerializer(serializers.Serializer):
username = validated_data.get("username") type = serializers.CharField(write_only=True, required=False, allow_blank=True)
return self.create_response(username)
class MFAChallengeSerializer(BearerTokenMixin, serializers.Serializer):
req = serializers.CharField(write_only=True)
auth_type = serializers.CharField(write_only=True)
code = serializers.CharField(write_only=True) code = serializers.CharField(write_only=True)
def validate_req(self, attr): def create(self, validated_data):
username = cache.get(attr) pass
if not username:
raise serializers.ValidationError("Not valid, may be expired")
self.context["username"] = username
def validate_code(self, code): def update(self, instance, validated_data):
username = self.context["username"] pass
user = User.objects.get(username=username)
ok = user.check_otp(code)
if not ok:
msg = "Otp code not valid, may be expired"
raise serializers.ValidationError(msg)
def create(self, validated_data):
username = self.context["username"]
return self.create_response(username)
class LoginConfirmSettingSerializer(serializers.ModelSerializer):
class Meta:
model = LoginConfirmSetting
fields = ['id', 'user', 'reviewers', 'date_created', 'date_updated']
read_only_fields = ['date_created', 'date_updated']
from rest_framework.request import Request
from django.http.request import QueryDict from django.http.request import QueryDict
from django.conf import settings from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_out from django.contrib.auth.signals import user_logged_out
from django.utils import timezone
from django_auth_ldap.backend import populate_user from django_auth_ldap.backend import populate_user
from common.utils import get_request_ip
from .backends.openid import new_client from .backends.openid import new_client
from .backends.openid.signals import ( from .backends.openid.signals import (
post_create_openid_user, post_openid_login_success post_create_openid_user, post_openid_login_success
) )
from .tasks import write_login_log_async from .signals import post_auth_success
from .signals import post_auth_success, post_auth_failed
@receiver(user_logged_out) @receiver(user_logged_out)
...@@ -52,35 +48,4 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): ...@@ -52,35 +48,4 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs):
user.save() user.save()
def generate_data(username, request):
user_agent = request.META.get('HTTP_USER_AGENT', '')
if isinstance(request, Request):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
else:
login_ip = get_request_ip(request)
login_type = 'W'
data = {
'username': username,
'ip': login_ip,
'type': login_type,
'user_agent': user_agent,
'datetime': timezone.now()
}
return data
@receiver(post_auth_success)
def on_user_auth_success(sender, user, request, **kwargs):
data = generate_data(user.username, request)
data.update({'mfa': int(user.otp_enabled), 'status': True})
write_login_log_async.delay(**data)
@receiver(post_auth_failed)
def on_user_auth_failed(sender, username, request, reason, **kwargs):
data = generate_data(username, request)
data.update({'reason': reason, 'status': False})
write_login_log_async.delay(**data)
...@@ -6,17 +6,8 @@ from ops.celery.decorator import register_as_period_task ...@@ -6,17 +6,8 @@ from ops.celery.decorator import register_as_period_task
from django.contrib.sessions.models import Session from django.contrib.sessions.models import Session
from django.utils import timezone from django.utils import timezone
from .utils import write_login_log
@shared_task
def write_login_log_async(*args, **kwargs):
write_login_log(*args, **kwargs)
@register_as_period_task(interval=3600*24) @register_as_period_task(interval=3600*24)
@shared_task @shared_task
def clean_django_sessions(): def clean_django_sessions():
Session.objects.filter(expire_date__lt=timezone.now()).delete() Session.objects.filter(expire_date__lt=timezone.now()).delete()
...@@ -37,7 +37,6 @@ ...@@ -37,7 +37,6 @@
<p> <p>
{% trans "Changes the world, starting with a little bit." %} {% trans "Changes the world, starting with a little bit." %}
</p> </p>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="ibox-content"> <div class="ibox-content">
...@@ -47,25 +46,29 @@ ...@@ -47,25 +46,29 @@
</div> </div>
<form class="m-t" role="form" method="post" action=""> <form class="m-t" role="form" method="post" action="">
{% csrf_token %} {% csrf_token %}
{% if form.non_field_errors %}
{% if block_login %} <div style="line-height: 17px;">
<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> <p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
{% endif %} </div>
<p class="red-fonts">{{ form.errors.password.as_text }}</p> {% elif form.errors.captcha %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
{% endif %} {% endif %}
<div class="form-group"> <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 %}"> <input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}">
{% if form.errors.username %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.username.as_text }}</p>
</div>
{% endif %}
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required=""> <input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
{% if form.errors.password %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
</div>
{% endif %}
</div> </div>
<div> <div>
{{ form.captcha }} {{ form.captcha }}
......
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
<title>{{ title }}</title>
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}"></script>
</head>
<body class="gray-bg">
<div class="passwordBox2 animated fadeInDown">
<div class="row">
<div class="col-md-12">
<div class="ibox-content">
<div>
<img src="{{ LOGO_URL }}" style="margin: auto" width="82" height="82">
<h2 style="display: inline">
{{ JMS_TITLE }}
</h2>
</div>
<p></p>
<div class="alert alert-success info-messages" >
{{ msg|safe }}
</div>
<div class="alert alert-danger error-messages" style="display: none">
</div>
<div class="progress progress-bar-default progress-striped active">
<div aria-valuemax="3600" aria-valuemin="0" aria-valuenow="43" role="progressbar" class="progress-bar">
</div>
</div>
<div class="row">
<div class="col-sm-3">
<a class="btn btn-primary btn-sm block btn-refresh">
<i class="fa fa-refresh"></i> {% trans 'Refresh' %}
</a>
</div>
<div class="col-sm-3">
<a class="btn btn-primary btn-sm block btn-copy" data-link="{{ ticket_detail_url }}">
<i class="fa fa-clipboard"></i> {% trans 'Copy link' %}
</a>
</div>
<div class="col-sm-3">
<a class="btn btn-default btn-sm block btn-return">
<i class="fa fa-reply"></i> {% trans 'Return' %}
</a>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-6">
{% include '_copyright.html' %}
</div>
</div>
</div>
</body>
{% include '_foot_js.html' %}
<script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script>
<script>
var errorMsgShow = false;
var errorMsgRef = $(".error-messages");
var infoMsgRef = $(".info-messages");
var timestamp = '{{ timestamp }}';
var progressBarRef = $(".progress-bar");
var interval, checkInterval;
var url = "{% url 'api-auth:login-confirm-ticket-status' %}";
var successUrl = "{% url 'authentication:login-guard' %}";
function doRequestAuth() {
requestApi({
url: url,
method: "GET",
success: function (data) {
if (!data.error && data.msg === 'ok') {
window.onbeforeunload = function(){};
window.location = "{% url 'authentication:login-guard' %}"
} else if (data.error !== "login_confirm_wait") {
if (!errorMsgShow) {
infoMsgRef.hide();
errorMsgRef.show();
progressBarRef.addClass('progress-bar-danger');
errorMsgShow = true;
}
clearInterval(interval);
clearInterval(checkInterval);
$(".copy-btn").attr('disabled', 'disabled');
errorMsgRef.html(data.msg)
}
},
error: function (text, data) {
},
flash_message: false
})
}
function initClipboard() {
var clipboard = new Clipboard('.btn-copy', {
text: function (trigger) {
var origin = window.location.origin;
var link = origin + $(".btn-copy").data('link');
return link
}
});
clipboard.on("success", function (e) {
toastr.success("{% trans "Copy success" %}")
})
}
function handleProgressBar() {
var now = new Date().getTime() / 1000;
var offset = now - timestamp;
var percent = offset / 3600 * 100;
if (percent > 100) {
percent = 100
}
progressBarRef.css("width", percent + '%');
progressBarRef.attr('aria-valuenow', offset);
}
function cancelTicket() {
requestApi({
url: url,
method: "DELETE",
flash_message: false
})
}
function cancelCloseConfirm() {
window.onbeforeunload = function() {};
window.onunload = function(){};
}
function setCloseConfirm() {
window.onbeforeunload = function (e) {
return 'Confirm';
};
window.onunload = function (e) {
cancelTicket();
}
}
$(document).ready(function () {
interval = setInterval(handleProgressBar, 1000);
checkInterval = setInterval(doRequestAuth, 5000);
doRequestAuth();
initClipboard();
setCloseConfirm();
}).on('click', '.btn-refresh', function () {
cancelCloseConfirm();
window.location.reload();
}).on('click', '.btn-return', function () {
cancelCloseConfirm();
window.location = "{% url 'authentication:login' %}"
})
</script>
</html>
...@@ -48,6 +48,13 @@ ...@@ -48,6 +48,13 @@
float: right; float: right;
} }
.red-fonts {
color: red;
}
.field-error {
text-align: left;
}
</style> </style>
</head> </head>
...@@ -69,30 +76,34 @@ ...@@ -69,30 +76,34 @@
<div style="margin-bottom: 10px"> <div style="margin-bottom: 10px">
<div> <div>
<div class="col-md-1"></div> <div class="col-md-1"></div>
<div class="contact-form col-md-10" style="margin-top: 10px;height: 35px"> <div class="contact-form col-md-10" style="margin-top: 20px;height: 35px">
<form id="contact-form" action="" method="post" role="form" novalidate="novalidate"> <form id="contact-form" action="" method="post" role="form" novalidate="novalidate">
{% csrf_token %} {% csrf_token %}
{% if form.non_field_errors %}
<div style="height: 70px;color: red;line-height: 17px;"> <div style="height: 70px;color: red;line-height: 17px;">
{% if block_login %} <p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p> </div>
<p class="red-fonts">{{ form.errors.password.as_text }}</p> {% elif form.errors.captcha %}
{% 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> <p class="red-fonts">{% trans 'Captcha invalid' %}</p>
{% else %} {% else %}
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p> <div style="height: 50px"></div>
{% endif %} {% endif %}
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
{% endif %}
</div>
<div class="form-group"> <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"> <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">
{% if form.errors.username %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.username.as_text }}</p>
</div>
{% endif %}
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required=""> <input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
{% if form.errors.password %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
</div>
{% endif %}
</div> </div>
<div class="form-group" style="height: 50px;margin-bottom: 0;font-size: 13px"> <div class="form-group" style="height: 50px;margin-bottom: 0;font-size: 13px">
{{ form.captcha }} {{ form.captcha }}
......
# coding:utf-8 # coding:utf-8
# #
from __future__ import absolute_import
from django.urls import path from django.urls import path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .. import api from .. import api
app_name = 'authentication'
router = DefaultRouter() router = DefaultRouter()
router.register('access-keys', api.AccessKeyViewSet, 'access-key') router.register('access-keys', api.AccessKeyViewSet, 'access-key')
app_name = 'authentication'
urlpatterns = [ urlpatterns = [
# path('token/', api.UserToken.as_view(), name='user-token'), # path('token/', api.UserToken.as_view(), name='user-token'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'), path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'), path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
path('connection-token/', path('connection-token/',
api.UserConnectionTokenApi.as_view(), name='connection-token'), api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
] ]
urlpatterns += router.urls urlpatterns += router.urls
......
...@@ -16,5 +16,7 @@ urlpatterns = [ ...@@ -16,5 +16,7 @@ urlpatterns = [
# login # login
path('login/', views.UserLoginView.as_view(), name='login'), path('login/', views.UserLoginView.as_view(), name='login'),
path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'), path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'),
path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'),
path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'),
path('logout/', views.UserLogoutView.as_view(), name='logout'), path('logout/', views.UserLogoutView.as_view(), name='logout'),
] ]
...@@ -3,22 +3,11 @@ ...@@ -3,22 +3,11 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from common.utils import get_ip_city, get_object_or_none, validate_ip from common.utils import (
get_ip_city, get_object_or_none, validate_ip
)
from users.models import User from users.models import User
from . import const from . import errors
def write_login_log(*args, **kwargs):
from audits.models import UserLoginLog
default_city = _("Unknown")
ip = kwargs.get('ip') or ''
if not (ip and validate_ip(ip)):
ip = ip[:15]
city = default_city
else:
city = get_ip_city(ip) or default_city
kwargs.update({'ip': ip, 'city': city})
UserLoginLog.objects.create(**kwargs)
def check_user_valid(**kwargs): def check_user_valid(**kwargs):
...@@ -26,6 +15,7 @@ def check_user_valid(**kwargs): ...@@ -26,6 +15,7 @@ def check_user_valid(**kwargs):
public_key = kwargs.pop('public_key', None) public_key = kwargs.pop('public_key', None)
email = kwargs.pop('email', None) email = kwargs.pop('email', None)
username = kwargs.pop('username', None) username = kwargs.pop('username', None)
request = kwargs.get('request')
if username: if username:
user = get_object_or_none(User, username=username) user = get_object_or_none(User, username=username)
...@@ -35,21 +25,17 @@ def check_user_valid(**kwargs): ...@@ -35,21 +25,17 @@ def check_user_valid(**kwargs):
user = None user = None
if user is None: if user is None:
return None, const.user_not_exist return None, errors.reason_user_not_exist
elif not user.is_valid: elif user.is_expired:
return None, const.user_invalid return None, errors.reason_user_inactive
elif not user.is_active:
return None, errors.reason_user_inactive
elif user.password_has_expired: elif user.password_has_expired:
return None, const.password_expired return None, errors.reason_password_expired
if password and authenticate(username=username, password=password): if password or public_key:
return user, '' user = authenticate(request, username=username,
password=password, public_key=public_key)
if public_key and user.public_key: if user:
public_key_saved = user.public_key.split()
if len(public_key_saved) == 1:
if public_key == public_key_saved[0]:
return user, ''
elif len(public_key_saved) > 1:
if public_key == public_key_saved[1]:
return user, '' return user, ''
return None, const.password_failed return None, errors.reason_password_failed
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .login import * from .login import *
from .mfa import *
This diff is collapsed.
# -*- coding: utf-8 -*-
#
from __future__ import unicode_literals
from django.views.generic.edit import FormView
from .. import forms, errors, mixins
from .utils import redirect_to_guard_view
__all__ = ['UserLoginOtpView']
class UserLoginOtpView(mixins.AuthMixin, FormView):
template_name = 'authentication/login_otp.html'
form_class = forms.UserCheckOtpCodeForm
redirect_field_name = 'next'
def form_valid(self, form):
otp_code = form.cleaned_data.get('otp_code')
try:
self.check_user_mfa(otp_code)
return redirect_to_guard_view()
except errors.MFAFailedError as e:
form.add_error('otp_code', e.msg)
return super().form_invalid(form)
# -*- coding: utf-8 -*-
#
from django.shortcuts import reverse, redirect
def redirect_to_guard_view():
continue_url = reverse('authentication:login-guard')
return redirect(continue_url)
...@@ -25,6 +25,15 @@ class IDSpmFilterMixin: ...@@ -25,6 +25,15 @@ class IDSpmFilterMixin:
return backends return backends
class SerializerMixin:
def get_serializer_class(self):
if self.request.method.lower() == 'get' and\
self.request.query_params.get('draw') \
and hasattr(self, 'serializer_display_class'):
return self.serializer_display_class
return super().get_serializer_class()
class ExtraFilterFieldsMixin: class ExtraFilterFieldsMixin:
default_added_filters = [CustomFilter, IDSpmFilter] default_added_filters = [CustomFilter, IDSpmFilter]
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
...@@ -44,5 +53,5 @@ class ExtraFilterFieldsMixin: ...@@ -44,5 +53,5 @@ class ExtraFilterFieldsMixin:
return queryset return queryset
class CommonApiMixin(ExtraFilterFieldsMixin): class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin):
pass pass
...@@ -53,3 +53,15 @@ class CommonModelMixin(models.Model): ...@@ -53,3 +53,15 @@ class CommonModelMixin(models.Model):
class Meta: class Meta:
abstract = True abstract = True
class DebugQueryManager(models.Manager):
def get_queryset(self):
import traceback
lines = traceback.format_stack()
print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>")
for line in lines[-10:-1]:
print(line)
print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
queryset = super().get_queryset()
return queryset
...@@ -153,6 +153,14 @@ def get_request_ip(request): ...@@ -153,6 +153,14 @@ def get_request_ip(request):
return login_ip return login_ip
def get_request_ip_or_data(request):
ip = ''
if hasattr(request, 'data'):
ip = request.data.get('remote_addr', '')
ip = ip or get_request_ip(request)
return ip
def validate_ip(ip): def validate_ip(ip):
try: try:
ipaddress.ip_address(ip) ipaddress.ip_address(ip)
......
...@@ -375,6 +375,7 @@ defaults = { ...@@ -375,6 +375,7 @@ defaults = {
'RADIUS_SERVER': 'localhost', 'RADIUS_SERVER': 'localhost',
'RADIUS_PORT': 1812, 'RADIUS_PORT': 1812,
'RADIUS_SECRET': '', 'RADIUS_SECRET': '',
'RADIUS_ENCRYPT_PASSWORD': True,
'AUTH_LDAP_SEARCH_PAGED_SIZE': 1000, 'AUTH_LDAP_SEARCH_PAGED_SIZE': 1000,
'AUTH_LDAP_SYNC_IS_PERIODIC': False, 'AUTH_LDAP_SYNC_IS_PERIODIC': False,
'AUTH_LDAP_SYNC_INTERVAL': None, 'AUTH_LDAP_SYNC_INTERVAL': None,
...@@ -394,8 +395,11 @@ defaults = { ...@@ -394,8 +395,11 @@ defaults = {
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd', 'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
'FLOWER_URL': "127.0.0.1:5555", 'FLOWER_URL': "127.0.0.1:5555",
'DEFAULT_ORG_SHOW_ALL_USERS': True, 'DEFAULT_ORG_SHOW_ALL_USERS': True,
'PERIOD_TASK_ENABLED': True, 'PERIOD_TASK_ENABLE': True,
'FORCE_SCRIPT_NAME': '',
'LOGIN_CONFIRM_ENABLE': False,
'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False, 'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False,
'OTP_IN_RADIUS': False,
} }
......
...@@ -18,7 +18,9 @@ def jumpserver_processor(request): ...@@ -18,7 +18,9 @@ def jumpserver_processor(request):
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2019', 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2019',
'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION, 'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION,
'SECURITY_MFA_VERIFY_TTL': settings.SECURITY_MFA_VERIFY_TTL, 'SECURITY_MFA_VERIFY_TTL': settings.SECURITY_MFA_VERIFY_TTL,
'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME,
'SECURITY_VIEW_AUTH_NEED_MFA': settings.CONFIG.SECURITY_VIEW_AUTH_NEED_MFA, 'SECURITY_VIEW_AUTH_NEED_MFA': settings.CONFIG.SECURITY_VIEW_AUTH_NEED_MFA,
'LOGIN_CONFIRM_ENABLE': settings.CONFIG.LOGIN_CONFIRM_ENABLE,
} }
return context return context
......
...@@ -71,6 +71,7 @@ INSTALLED_APPS = [ ...@@ -71,6 +71,7 @@ INSTALLED_APPS = [
'audits.apps.AuditsConfig', 'audits.apps.AuditsConfig',
'authentication.apps.AuthenticationConfig', # authentication 'authentication.apps.AuthenticationConfig', # authentication
'applications.apps.ApplicationsConfig', 'applications.apps.ApplicationsConfig',
'tickets.apps.TicketsConfig',
'rest_framework', 'rest_framework',
'rest_framework_swagger', 'rest_framework_swagger',
'drf_yasg', 'drf_yasg',
...@@ -331,7 +332,7 @@ LOCALE_PATHS = [ ...@@ -331,7 +332,7 @@ LOCALE_PATHS = [
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/ # https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '{}/static/'.format(CONFIG.FORCE_SCRIPT_NAME)
STATIC_ROOT = os.path.join(PROJECT_DIR, "data", "static") STATIC_ROOT = os.path.join(PROJECT_DIR, "data", "static")
STATIC_DIR = os.path.join(BASE_DIR, "static") STATIC_DIR = os.path.join(BASE_DIR, "static")
...@@ -410,6 +411,7 @@ REST_FRAMEWORK = { ...@@ -410,6 +411,7 @@ REST_FRAMEWORK = {
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'authentication.backends.pubkey.PublicKeyAuthBackend',
] ]
# Custom User Auth model # Custom User Auth model
...@@ -655,3 +657,4 @@ CHANNEL_LAYERS = { ...@@ -655,3 +657,4 @@ CHANNEL_LAYERS = {
# Enable internal period task # Enable internal period task
PERIOD_TASK_ENABLED = CONFIG.PERIOD_TASK_ENABLED PERIOD_TASK_ENABLED = CONFIG.PERIOD_TASK_ENABLED
FORCE_SCRIPT_NAME = CONFIG.FORCE_SCRIPT_NAME
...@@ -24,6 +24,7 @@ api_v1 = [ ...@@ -24,6 +24,7 @@ api_v1 = [
path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')), path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')),
path('common/', include('common.urls.api_urls', namespace='api-common')), path('common/', include('common.urls.api_urls', namespace='api-common')),
path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
] ]
api_v2 = [ api_v2 = [
...@@ -42,6 +43,7 @@ app_view_patterns = [ ...@@ -42,6 +43,7 @@ app_view_patterns = [
path('orgs/', include('orgs.urls.views_urls', namespace='orgs')), path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
path('auth/', include('authentication.urls.view_urls'), name='auth'), path('auth/', include('authentication.urls.view_urls'), name='auth'),
path('applications/', include('applications.urls.views_urls', namespace='applications')), path('applications/', include('applications.urls.views_urls', namespace='applications')),
path('tickets/', include('tickets.urls.views_urls', namespace='tickets')),
re_path(r'flower/(?P<path>.*)', celery_flower_view, name='flower-view'), re_path(r'flower/(?P<path>.*)', celery_flower_view, name='flower-view'),
] ]
...@@ -65,7 +67,8 @@ urlpatterns = [ ...@@ -65,7 +67,8 @@ urlpatterns = [
path('api/v2/', include(api_v2)), path('api/v2/', include(api_v2)),
re_path('api/(?P<app>\w+)/(?P<version>v\d)/.*', views.redirect_format_api), re_path('api/(?P<app>\w+)/(?P<version>v\d)/.*', views.redirect_format_api),
path('api/health/', views.HealthCheckView.as_view(), name="health"), path('api/health/', views.HealthCheckView.as_view(), name="health"),
path('luna/', views.LunaView.as_view(), name='luna-view'), re_path('luna/.*', views.LunaView.as_view(), name='luna-view'),
re_path('koko/.*', views.KokoView.as_view(), name='koko-view'),
re_path('ws/.*', views.WsView.as_view(), name='ws-view'), re_path('ws/.*', views.WsView.as_view(), name='ws-view'),
path('i18n/<str:lang>/', views.I18NView.as_view(), name='i18n-switch'), path('i18n/<str:lang>/', views.I18NView.as_view(), name='i18n-switch'),
path('settings/', include('settings.urls.view_urls', namespace='settings')), path('settings/', include('settings.urls.view_urls', namespace='settings')),
......
...@@ -234,3 +234,10 @@ class WsView(APIView): ...@@ -234,3 +234,10 @@ class WsView(APIView):
.format(self.ws_port)) .format(self.ws_port))
return JsonResponse({"msg": msg}) return JsonResponse({"msg": msg})
class KokoView(View):
def get(self, request):
msg = _(
"<div>Koko is a separately deployed program, you need to deploy Koko, configure nginx for url distribution,</div> "
"</div>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>")
return HttpResponse(msg)
This diff is collapsed.
...@@ -246,30 +246,35 @@ class AdHoc(models.Model): ...@@ -246,30 +246,35 @@ class AdHoc(models.Model):
time_start = time.time() time_start = time.time()
date_start = timezone.now() date_start = timezone.now()
is_success = False is_success = False
summary = {}
raw = ''
try: try:
date_start_s = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') date_start_s = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(_("{} Start task: {}").format(date_start_s, self.task.name)) print(_("{} Start task: {}").format(date_start_s, self.task.name))
raw, summary = self._run_only() raw, summary = self._run_only()
is_success = summary.get('success', False) is_success = summary.get('success', False)
return raw, summary
except Exception as e: except Exception as e:
logger.error(e, exc_info=True) logger.error(e, exc_info=True)
summary = {}
raw = {"dark": {"all": str(e)}, "contacted": []} raw = {"dark": {"all": str(e)}, "contacted": []}
return raw, summary
finally: finally:
date_end = timezone.now() date_end = timezone.now()
date_end_s = date_end.strftime('%Y-%m-%d %H:%M:%S') date_end_s = date_end.strftime('%Y-%m-%d %H:%M:%S')
print(_("{} Task finish").format(date_end_s)) print(_("{} Task finish").format(date_end_s))
print('.\n\n.') print('.\n\n.')
try:
summary_text = json.dumps(summary)
except json.JSONDecodeError:
summary_text = '{}'
AdHocRunHistory.objects.filter(id=history.id).update( AdHocRunHistory.objects.filter(id=history.id).update(
date_start=date_start, date_start=date_start,
is_finished=True, is_finished=True,
is_success=is_success, is_success=is_success,
date_finished=timezone.now(), date_finished=timezone.now(),
timedelta=time.time() - time_start timedelta=time.time() - time_start,
_summary=summary_text
) )
return raw, summary
def _run_only(self): def _run_only(self):
Task.objects.filter(id=self.task.id).update(date_updated=timezone.now()) Task.objects.filter(id=self.task.id).update(date_updated=timezone.now())
...@@ -321,10 +326,9 @@ class AdHoc(models.Model): ...@@ -321,10 +326,9 @@ class AdHoc(models.Model):
except AdHocRunHistory.DoesNotExist: except AdHocRunHistory.DoesNotExist:
return None return None
def save(self, force_insert=False, force_update=False, using=None, def save(self, **kwargs):
update_fields=None): instance = super().save(**kwargs)
super().save(force_insert=force_insert, force_update=force_update, return instance
using=using, update_fields=update_fields)
def __str__(self): def __str__(self):
return "{} of {}".format(self.task.name, self.short_id) return "{} of {}".format(self.task.name, self.short_id)
...@@ -393,7 +397,10 @@ class AdHocRunHistory(models.Model): ...@@ -393,7 +397,10 @@ class AdHocRunHistory(models.Model):
@summary.setter @summary.setter
def summary(self, item): def summary(self, item):
try:
self._summary = json.dumps(item) self._summary = json.dumps(item)
except json.JSONDecodeError:
self._summary = json.dumps({})
@property @property
def success_hosts(self): def success_hosts(self):
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import uuid import uuid
import json import json
from celery.exceptions import SoftTimeLimitExceeded
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext from django.utils.translation import ugettext
...@@ -64,6 +65,9 @@ class CommandExecution(models.Model): ...@@ -64,6 +65,9 @@ class CommandExecution(models.Model):
try: try:
result = runner.execute(self.command, 'all') result = runner.execute(self.command, 'all')
self.result = result.results_command self.result = result.results_command
except SoftTimeLimitExceeded as e:
print("Run timeout than 60s")
self.result = {"error": str(e)}
except Exception as e: except Exception as e:
print("Error occur: {}".format(e)) print("Error occur: {}".format(e))
self.result = {"error": str(e)} self.result = {"error": str(e)}
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import traceback
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
...@@ -9,7 +8,7 @@ from django.core.exceptions import ValidationError ...@@ -9,7 +8,7 @@ from django.core.exceptions import ValidationError
from common.utils import get_logger from common.utils import get_logger
from ..utils import ( from ..utils import (
set_current_org, get_current_org, current_org, set_current_org, get_current_org, current_org,
get_org_filters filter_org_queryset
) )
from ..models import Organization from ..models import Organization
...@@ -20,37 +19,24 @@ __all__ = [ ...@@ -20,37 +19,24 @@ __all__ = [
] ]
class OrgQuerySet(models.QuerySet):
pass
class OrgManager(models.Manager): class OrgManager(models.Manager):
def get_queryset(self):
queryset = super().get_queryset()
kwargs = get_org_filters()
if kwargs:
return queryset.filter(**kwargs)
return queryset
def set_current_org(self, org): def get_queryset(self):
if isinstance(org, str): queryset = super(OrgManager, self).get_queryset()
org = Organization.get_instance(org) return filter_org_queryset(queryset)
set_current_org(org)
return self
def all(self): def all(self):
# print("Call all: {}".format(current_org))
#
# lines = traceback.format_stack()
# print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>")
# for line in lines[-10:-1]:
# print(line)
# print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<")
if not current_org: if not current_org:
msg = 'You can `objects.set_current_org(org).all()` then run it' msg = 'You can `objects.set_current_org(org).all()` then run it'
return self return self
else: else:
return super().all() return super(OrgManager, self).all()
def set_current_org(self, org):
if isinstance(org, str):
org = Organization.get_instance(org)
set_current_org(org)
return self
class OrgModelMixin(models.Model): class OrgModelMixin(models.Model):
......
...@@ -4,7 +4,7 @@ from django.conf import settings ...@@ -4,7 +4,7 @@ from django.conf import settings
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.utils import is_uuid from common.utils import is_uuid, lazyproperty
class Organization(models.Model): class Organization(models.Model):
...@@ -72,7 +72,8 @@ class Organization(models.Model): ...@@ -72,7 +72,8 @@ class Organization(models.Model):
org = cls.default() if default else None org = cls.default() if default else None
return org return org
def get_org_users(self): @lazyproperty
def org_users(self):
from users.models import User from users.models import User
if self.is_real(): if self.is_real():
return self.users.all() return self.users.all()
...@@ -81,18 +82,29 @@ class Organization(models.Model): ...@@ -81,18 +82,29 @@ class Organization(models.Model):
users = users.filter(related_user_orgs__isnull=True) users = users.filter(related_user_orgs__isnull=True)
return users return users
def get_org_admins(self): def get_org_users(self):
return self.org_users
@lazyproperty
def org_admins(self):
from users.models import User from users.models import User
if self.is_real(): if self.is_real():
return self.admins.all() return self.admins.all()
return User.objects.filter(role=User.ROLE_ADMIN) return User.objects.filter(role=User.ROLE_ADMIN)
def get_org_auditors(self): def get_org_admins(self):
return self.org_admins
@lazyproperty
def org_auditors(self):
from users.models import User from users.models import User
if self.is_real(): if self.is_real():
return self.auditors.all() return self.auditors.all()
return User.objects.filter(role=User.ROLE_AUDITOR) return User.objects.filter(role=User.ROLE_AUDITOR)
def get_org_auditors(self):
return self.org_auditors
def get_org_members(self, exclude=()): def get_org_members(self, exclude=()):
from users.models import User from users.models import User
members = User.objects.none() members = User.objects.none()
......
...@@ -115,8 +115,8 @@ $(document).ready(function () { ...@@ -115,8 +115,8 @@ $(document).ready(function () {
closeOnSelect: false closeOnSelect: false
}); });
usersSelect2Init('.users-select2'); usersSelect2Init('.users-select2');
$('#date_start').daterangepicker(dateOptions); initDateRangePicker('#date_start');
$('#date_expired').daterangepicker(dateOptions); initDateRangePicker('#date_expired');
}) })
.on("submit", "form", function (evt) { .on("submit", "form", function (evt) {
evt.preventDefault(); evt.preventDefault();
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment