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' %}
{% load i18n static %}
{% 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' %}
<b><a href="https://github.com/jumpserver/Jmservisor/releases" target="view_window" >{% trans 'Download application loader' %}</a></b>
</div>
{% endblock %}
{% block table_search %}{% endblock %}
{% 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']
class GatheredUser(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset"))
username = models.CharField(max_length=32, blank=True, db_index=True,
verbose_name=_('Username'))
username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username'))
present = models.BooleanField(default=True, verbose_name=_("Present"))
date_created = models.DateTimeField(auto_now_add=True,
verbose_name=_("Date created"))
date_updated = models.DateTimeField(auto_now=True,
verbose_name=_("Date updated"))
date_last_login = models.DateTimeField(null=True, verbose_name=_("Date last login"))
ip_last_login = models.CharField(max_length=39, default='', verbose_name=_("IP last login"))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
@property
def hostname(self):
......
......@@ -12,6 +12,7 @@ class GatheredUserSerializer(OrgResourceModelSerializerMixin):
model = GatheredUser
fields = [
'id', 'asset', 'hostname', 'ip', 'username',
'date_last_login', 'ip_last_login',
'present', 'date_created', 'date_updated'
]
read_only_fields = fields
......
......@@ -94,6 +94,13 @@ GATHER_ASSET_USERS_TASKS = [
"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 = [
......
......@@ -2,9 +2,10 @@
import re
from collections import defaultdict
from celery import shared_task
from celery import shared_task
from django.utils.translation import ugettext as _
from django.utils import timezone
from orgs.utils import tmp_to_org
from common.utils import get_logger
......@@ -19,19 +20,25 @@ ignore_login_shell = re.compile(r'nologin$|sync$|shutdown$|halt$')
def parse_linux_result_to_users(result):
task_result = {}
for task_name, raw in result.items():
res = raw.get('ansible_facts', {}).get('getent_passwd')
if res:
task_result = res
break
if not task_result or not isinstance(task_result, dict):
return []
users = []
for username, attr in task_result.items():
users = defaultdict(dict)
users_result = result.get('gather host users', {})\
.get('ansible_facts', {})\
.get('getent_passwd')
if not isinstance(users_result, dict):
users_result = {}
for username, attr in users_result.items():
if ignore_login_shell.search(attr[-1]):
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
......@@ -45,7 +52,7 @@ def parse_windows_result_to_users(result):
if not task_result:
return []
users = []
users = {}
for i in range(4):
task_result.pop(0)
......@@ -55,7 +62,7 @@ def parse_windows_result_to_users(result):
for line in task_result:
user = space.split(line)
if user[0]:
users.append(user[0])
users[user[0]] = {}
return users
......@@ -82,8 +89,12 @@ def add_asset_users(assets, results):
with tmp_to_org(asset.org_id):
GatheredUser.objects.filter(asset=asset, present=True)\
.update(present=False)
for username in users:
for username, data in users.items():
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(
defaults=defaults, asset=asset, username=username,
)
......
......@@ -31,7 +31,7 @@
<div class="form-group">
<div class="col-sm-9 col-lg-9 col-sm-offset-2">
<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>
......
......@@ -135,7 +135,8 @@ function initAssetModalTable() {
],
lengthMenu: [[10, 25, 50], [10, 25, 50]],
pageLength: 10,
select_style: assetModalOption.selectStyle
select_style: assetModalOption.selectStyle,
paging_numbers_length: 3
};
assetModalTable = jumpserver.initServerSideDataTable(options);
if (assetModalOption.onModalTableDone) {
......
......@@ -303,9 +303,24 @@ function defaultCallback(action) {
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 () {
$('.treebox').css('height', window.innerHeight - 180);
$('.treebox').css('height', window.innerHeight - 60);
})
.on('click', '.btn-show-current-asset', function(){
hideRMenu();
......@@ -320,6 +335,9 @@ $(document).ready(function () {
$('#show_current_asset').css('display', 'inline-block');
setCookie('show_current_asset', '');
location.reload();
}).on('click', '.tree-toggle-btn', function (e) {
e.preventDefault();
toggle();
})
</script>
{% extends '_base_list.html' %}
{% load i18n static %}
{% 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 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%}
</div>
{% endblock %}
{% block table_search %}
<div class="" style="float: right">
......
......@@ -3,16 +3,16 @@
{% load i18n %}
{% 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' %}
</div>
{# </div>#}
{% endblock %}
{% block custom_head_css_js %}
<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>
{# <link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">#}
{# <script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>#}
<script src="{% static 'js/jquery.form.min.js' %}"></script>
<style type="text/css">
div#rMenu {
......@@ -48,12 +48,12 @@
{% block content %}
<div class="wrapper wrapper-content">
<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' %}
</div>
<div class="col-lg-9 animated fadeInRight" id="split-right">
<div class="tree-toggle">
<div class="btn btn-sm btn-primary tree-toggle-btn" onclick="toggle()">
<div class="tree-toggle" style="z-index: 9999">
<div class="btn btn-sm btn-primary tree-toggle-btn">
<i class="fa fa-angle-left fa-x" id="toggle-icon"></i>
</div>
</div>
......@@ -151,9 +151,9 @@ function initTable() {
}},
{targets: 4, createdCell: function (td, cellData, rowData) {
var innerHtml = "";
if (cellData.status == 1) {
if (cellData.status === 1) {
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>'
} else {
innerHtml = '<i class="fa fa-circle text-warning"></i>'
......@@ -386,6 +386,10 @@ $(document).ready(function(){
setTimeout( function () {window.location.reload();}, 300);
}
function reloadTable() {
asset_table.ajax.reload();
}
function doDeactive() {
var data = [];
$.each(id_list, function(index, object_id) {
......@@ -396,7 +400,7 @@ $(document).ready(function(){
url: the_url,
method: 'PATCH',
body: JSON.stringify(data),
success: refreshPage
success: reloadTable
});
}
function doActive() {
......@@ -409,7 +413,7 @@ $(document).ready(function(){
url: the_url,
method: 'PATCH',
body: JSON.stringify(data),
success: refreshPage
success: reloadTable
});
}
function doDelete() {
......@@ -431,7 +435,7 @@ $(document).ready(function(){
success: function () {
var msg = "{% trans 'Asset Deleted.' %}";
swal("{% trans 'Asset Delete' %}", msg, "success");
refreshPage();
reloadTable();
},
flash_message: false,
});
......@@ -478,16 +482,12 @@ $(document).ready(function(){
'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);
requestApi({
'url': url,
'method': 'PUT',
'body': JSON.stringify(data),
'success': success
'success': reloadTable
})
}
......
......@@ -2,14 +2,12 @@
{% load i18n static %}
{% block table_search %}{% endblock %}
{% block help_message %}
<div class="alert alert-info help-message">
{% 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 '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 'else if action is deny, then command with be deny,' %}
{% trans 'else match next rule, if none matched, allowed' %}
</div>
{% endblock %}
{% block table_container %}
<div class="uc pull-left m-r-5">
......
......@@ -3,13 +3,9 @@
{% block table_search %}{% endblock %}
{% 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.' %}
<br>
{% trans 'JMS => Domain gateway => Target assets' %}
</div>
{% endblock %}
{% block table_container %}
......
......@@ -2,11 +2,9 @@
{% load i18n %}
{% 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 '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.' %}
</div>
{% endblock %}
{% block table_search %}
......
......@@ -7,7 +7,7 @@ from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _
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.utils import get_object_or_none
from ..models import Domain, Gateway
......
......@@ -110,5 +110,14 @@ class UserLoginLog(models.Model):
login_logs = login_logs.filter(username__in=username_list)
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:
ordering = ['-datetime', 'username']
......@@ -4,15 +4,18 @@
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.db import transaction
from django.utils import timezone
from rest_framework.renderers import JSONRenderer
from rest_framework.request import Request
from jumpserver.utils import current_request
from common.utils import get_request_ip, get_logger, get_syslogger
from users.models import User
from authentication.signals import post_auth_failed, post_auth_success
from terminal.models import Session, Command
from terminal.backends.command.serializers import SessionCommandSerializer
from . import models
from . import serializers
from . import models, serializers
from .tasks import write_login_log_async
logger = get_logger(__name__)
sys_logger = get_syslogger("audits")
......@@ -99,3 +102,39 @@ def on_audits_log_create(sender, instance=None, **kwargs):
data = json_render.render(s.data).decode(errors='ignore')
msg = "{} - {}".format(category, data)
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
from ops.celery.decorator import register_as_period_task
from .models import UserLoginLog
from .utils import write_login_log
@register_as_period_task(interval=3600*24)
......@@ -19,3 +20,8 @@ def clean_login_log_period():
days = 90
expired_day = now - datetime.timedelta(days=days)
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 @@
<td class="text-center">{{ login_log.ip }}</td>
<td class="text-center">{{ login_log.city }}</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.datetime }}</td>
</tr>
......
import csv
import codecs
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):
......@@ -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]
writer.writerow(data)
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 *
from .token import *
from .mfa import *
from .access_key import *
from .login_confirm import *
# -*- coding: utf-8 -*-
#
import uuid
import time
from django.core.cache import cache
from django.urls import reverse
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from rest_framework.views import APIView
from common.utils import get_logger, get_request_ip
from common.permissions import IsOrgAdminOrAppUser, IsValidUser
from common.utils import get_logger
from common.permissions import IsOrgAdminOrAppUser
from orgs.mixins.api import RootOrgViewMixin
from users.serializers import UserSerializer
from users.models import User
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__)
__all__ = [
'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi',
'UserOtpVerifyApi',
'UserConnectionTokenApi',
]
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):
permission_classes = (IsOrgAdminOrAppUser,)
......@@ -153,59 +61,5 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView):
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 -*-
#
import time
from rest_framework.permissions import AllowAny
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 errors
from ..mixins import AuthMixin
class MFAChallengeApi(CreateAPIView):
__all__ = ['MFAChallengeApi', 'UserOtpVerifyApi']
class MFAChallengeApi(AuthMixin, CreateAPIView):
permission_classes = (AllowAny,)
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 -*-
#
import uuid
from django.core.cache import cache
from django.utils.translation import ugettext as _
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from drf_yasg.utils import swagger_auto_schema
from common.utils import get_request_ip, get_logger
from users.utils import (
check_otp_code, increase_login_failed_count,
is_block_login, clean_failed_count
)
from ..utils import check_user_valid
from ..signals import post_auth_success, post_auth_failed
from .. import serializers
from common.utils import get_logger
from .. import serializers, errors
from ..mixins import AuthMixin
logger = get_logger(__name__)
......@@ -25,71 +16,26 @@ logger = get_logger(__name__)
__all__ = ['TokenCreateApi']
class AuthFailedError(Exception):
def __init__(self, msg, reason=None):
self.msg = msg
self.reason = reason
class MFARequiredError(Exception):
pass
class TokenCreateApi(CreateAPIView):
class TokenCreateApi(AuthMixin, CreateAPIView):
permission_classes = (AllowAny,)
serializer_class = serializers.BearerTokenSerializer
@staticmethod
def check_is_block(username, ip):
if is_block_login(username, ip):
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_session_if_need(self):
if self.request.session.is_empty():
self.request.session.create()
def create(self, request, *args, **kwargs):
username = self.request.data.get('username')
ip = self.request.data.get('remote_addr', None)
ip = ip or get_request_ip(self.request)
user = None
self.create_session_if_need()
# 如果认证没有过,检查账号密码
try:
self.check_is_block(username, ip)
user = self.check_user_valid()
if user.otp_enabled:
raise MFARequiredError()
user = self.check_user_auth_if_need()
self.check_user_mfa_if_need(user)
self.check_user_login_confirm_if_need(user)
self.send_auth_signal(success=True, user=user)
clean_failed_count(username, ip)
return super().create(request, *args, **kwargs)
except AuthFailedError as e:
increase_login_failed_count(username, ip)
self.send_auth_signal(success=False, user=user, username=username, reason=str(e))
return Response({'msg': str(e)}, status=401)
except MFARequiredError:
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
)
self.clear_auth_mark()
resp = super().create(request, *args, **kwargs)
return resp
except errors.AuthFailedError as e:
return Response(e.as_data(), status=400)
except errors.NeedMoreInfoError as e:
return Response(e.as_data(), status=200)
# -*- 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
from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend
from django.conf import settings
from pyrad.packet import AccessRequest
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
from users.utils import get_login_failed_count
class UserLoginForm(AuthenticationForm):
class UserLoginForm(forms.Form):
username = forms.CharField(label=_('Username'), max_length=100)
password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput,
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):
if not user.is_staff:
raise forms.ValidationError(
self.error_messages['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,
code='inactive',
)
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):
......
# 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
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 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):
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True,
......@@ -33,3 +37,42 @@ class PrivateToken(Token):
class Meta:
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 -*-
#
from django.core.cache import cache
from rest_framework import serializers
from common.utils import get_object_or_none
from users.models import User
from .models import AccessKey
from users.serializers import UserProfileSerializer
from .models import AccessKey, LoginConfirmSetting
__all__ = [
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
'MFAChallengeSerializer',
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer',
]
class AccessKeySerializer(serializers.ModelSerializer):
class Meta:
model = AccessKey
fields = ['id', 'secret', 'is_active', 'date_created']
......@@ -25,65 +25,54 @@ class OtpVerifySerializer(serializers.Serializer):
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)
keyword = serializers.SerializerMethodField()
date_expired = serializers.DateTimeField(read_only=True)
user = UserProfileSerializer(read_only=True)
@staticmethod
def get_keyword(obj):
return 'Bearer'
def create_response(self, username):
request = self.context.get("request")
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise serializers.ValidationError("username %s not exist" % username)
def create(self, validated_data):
request = self.context.get('request')
if request.user and not request.user.is_anonymous:
user = request.user
else:
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)
instance = {
"username": username,
"token": token,
"date_expired": date_expired,
"user": user
}
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):
username = validated_data.get("username")
return self.create_response(username)
class MFAChallengeSerializer(BearerTokenMixin, serializers.Serializer):
req = serializers.CharField(write_only=True)
auth_type = serializers.CharField(write_only=True)
class MFAChallengeSerializer(serializers.Serializer):
type = serializers.CharField(write_only=True, required=False, allow_blank=True)
code = serializers.CharField(write_only=True)
def validate_req(self, attr):
username = cache.get(attr)
if not username:
raise serializers.ValidationError("Not valid, may be expired")
self.context["username"] = username
def create(self, validated_data):
pass
def validate_code(self, code):
username = self.context["username"]
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 update(self, instance, validated_data):
pass
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.conf import settings
from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_out
from django.utils import timezone
from django_auth_ldap.backend import populate_user
from common.utils import get_request_ip
from .backends.openid import new_client
from .backends.openid.signals import (
post_create_openid_user, post_openid_login_success
)
from .tasks import write_login_log_async
from .signals import post_auth_success, post_auth_failed
from .signals import post_auth_success
@receiver(user_logged_out)
......@@ -52,35 +48,4 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs):
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
from django.contrib.sessions.models import Session
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)
@shared_task
def clean_django_sessions():
Session.objects.filter(expire_date__lt=timezone.now()).delete()
......@@ -37,7 +37,6 @@
<p>
{% trans "Changes the world, starting with a little bit." %}
</p>
</div>
<div class="col-md-6">
<div class="ibox-content">
......@@ -47,25 +46,29 @@
</div>
<form class="m-t" role="form" method="post" action="">
{% csrf_token %}
{% if block_login %}
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
{% elif password_expired %}
<p class="red-fonts">{% trans 'The user password has expired' %}</p>
{% elif form.errors %}
{% if 'captcha' in form.errors %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
{% else %}
{% if form.non_field_errors %}
<div style="line-height: 17px;">
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
{% endif %}
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
</div>
{% elif form.errors.captcha %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
{% endif %}
<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 %}">
{% if form.errors.username %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.username.as_text }}</p>
</div>
{% endif %}
</div>
<div class="form-group">
<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>
{{ 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 @@
float: right;
}
.red-fonts {
color: red;
}
.field-error {
text-align: left;
}
</style>
</head>
......@@ -69,30 +76,34 @@
<div style="margin-bottom: 10px">
<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">
{% csrf_token %}
{% if form.non_field_errors %}
<div style="height: 70px;color: red;line-height: 17px;">
{% if block_login %}
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
<p class="red-fonts">{{ form.errors.password.as_text }}</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">{{ form.non_field_errors.as_text }}</p>
</div>
{% elif form.errors.captcha %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
{% else %}
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
<div style="height: 50px"></div>
{% endif %}
<p class="red-fonts">{{ form.errors.password.as_text }}</p>
{% endif %}
</div>
<div class="form-group">
<input type="text" class="form-control" name="{{ form.username.html_name }}" placeholder="{% trans 'Username' %}" required="" value="{% if form.username.value %}{{ form.username.value }}{% endif %}" style="height: 35px">
{% if form.errors.username %}
<div class="help-block field-error">
<p class="red-fonts">{{ form.errors.username.as_text }}</p>
</div>
{% endif %}
</div>
<div class="form-group">
<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 class="form-group" style="height: 50px;margin-bottom: 0;font-size: 13px">
{{ form.captcha }}
......
# coding:utf-8
#
from __future__ import absolute_import
from django.urls import path
from rest_framework.routers import DefaultRouter
from .. import api
app_name = 'authentication'
router = DefaultRouter()
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
app_name = 'authentication'
urlpatterns = [
# 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('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
path('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('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
......
......@@ -16,5 +16,7 @@ urlpatterns = [
# login
path('login/', views.UserLoginView.as_view(), name='login'),
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'),
]
......@@ -3,22 +3,11 @@
from django.utils.translation import ugettext as _
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 . import const
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)
from . import errors
def check_user_valid(**kwargs):
......@@ -26,6 +15,7 @@ def check_user_valid(**kwargs):
public_key = kwargs.pop('public_key', None)
email = kwargs.pop('email', None)
username = kwargs.pop('username', None)
request = kwargs.get('request')
if username:
user = get_object_or_none(User, username=username)
......@@ -35,21 +25,17 @@ def check_user_valid(**kwargs):
user = None
if user is None:
return None, const.user_not_exist
elif not user.is_valid:
return None, const.user_invalid
return None, errors.reason_user_not_exist
elif user.is_expired:
return None, errors.reason_user_inactive
elif not user.is_active:
return None, errors.reason_user_inactive
elif user.password_has_expired:
return None, const.password_expired
return None, errors.reason_password_expired
if password and authenticate(username=username, password=password):
return user, ''
if public_key and user.public_key:
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]:
if password or public_key:
user = authenticate(request, username=username,
password=password, public_key=public_key)
if user:
return user, ''
return None, const.password_failed
return None, errors.reason_password_failed
# -*- coding: utf-8 -*-
#
from .login import *
from .mfa import *
......@@ -3,6 +3,7 @@
from __future__ import unicode_literals
import os
import datetime
from django.core.cache import cache
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.http import HttpResponse
......@@ -12,36 +13,32 @@ from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView
from django.views.generic.base import TemplateView, RedirectView
from django.views.generic.edit import FormView
from django.conf import settings
from django.urls import reverse_lazy
from common.utils import get_request_ip
from users.models import User
from audits.models import UserLoginLog as LoginLog
from common.utils import get_request_ip, get_object_or_none
from users.utils import (
check_otp_code, is_block_login, clean_failed_count, get_user_or_tmp_user,
set_tmp_user_to_cache, increase_login_failed_count,
redirect_user_first_login_or_index,
redirect_user_first_login_or_index
)
from ..signals import post_auth_success, post_auth_failed
from .. import forms
from .. import const
from .. import forms, mixins, errors
__all__ = [
'UserLoginView', 'UserLoginOtpView', 'UserLogoutView',
'UserLoginView', 'UserLogoutView',
'UserLoginGuardView', 'UserLoginWaitConfirmView',
]
@method_decorator(sensitive_post_parameters(), name='dispatch')
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(never_cache, name='dispatch')
class UserLoginView(FormView):
class UserLoginView(mixins.AuthMixin, FormView):
form_class = forms.UserLoginForm
form_class_captcha = forms.UserLoginCaptchaForm
redirect_field_name = 'next'
key_prefix_captcha = "_LOGIN_INVALID_{}"
redirect_field_name = 'next'
def get_template_names(self):
template_name = 'authentication/login.html'
......@@ -52,7 +49,7 @@ class UserLoginView(FormView):
if not License.has_valid_license():
return template_name
template_name = 'authentication/new_login.html'
template_name = 'authentication/xpack_login.html'
return template_name
def get(self, request, *args, **kwargs):
......@@ -68,48 +65,27 @@ class UserLoginView(FormView):
request.session.set_test_cookie()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
# limit login authentication
ip = get_request_ip(request)
username = self.request.POST.get('username')
if is_block_login(username, ip):
return self.render_to_response(self.get_context_data(block_login=True))
return super().post(request, *args, **kwargs)
def form_valid(self, form):
if not self.request.session.test_cookie_worked():
return HttpResponse(_("Please enable cookies and try again."))
user = form.get_user()
# user password expired
if user.password_has_expired:
reason = const.password_expired
self.send_auth_signal(success=False, username=user.username, reason=reason)
return self.render_to_response(self.get_context_data(password_expired=True))
set_tmp_user_to_cache(self.request, user)
username = form.cleaned_data.get('username')
ip = get_request_ip(self.request)
# 登陆成功,清除缓存计数
clean_failed_count(username, ip)
return redirect(self.get_success_url())
def form_invalid(self, form):
# write login failed log
username = form.cleaned_data.get('username')
exist = User.objects.filter(username=username).first()
reason = const.password_failed if exist else const.user_not_exist
# limit user login failed count
ip = get_request_ip(self.request)
increase_login_failed_count(username, ip)
form.add_limit_login_error(username, ip)
# show captcha
try:
self.check_user_auth()
except errors.AuthFailedError as e:
form.add_error(None, e.msg)
ip = self.get_request_ip()
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
self.send_auth_signal(success=False, username=username, reason=reason)
old_form = form
form = self.form_class_captcha(data=form.data)
form._errors = old_form.errors
return super().form_invalid(form)
new_form = self.form_class_captcha(data=form.data)
new_form._errors = form.errors
context = self.get_context_data(form=new_form)
return self.render_to_response(context)
return self.redirect_to_guard_view()
def redirect_to_guard_view(self):
guard_url = reverse('authentication:login-guard')
args = self.request.META.get('QUERY_STRING', '')
if args:
guard_url = "%s?%s" % (guard_url, args)
return redirect(guard_url)
def get_form_class(self):
ip = get_request_ip(self.request)
......@@ -118,21 +94,6 @@ class UserLoginView(FormView):
else:
return self.form_class
def get_success_url(self):
user = get_user_or_tmp_user(self.request)
if user.otp_enabled and user.otp_secret_key:
# 1,2,mfa_setting & T
return reverse('authentication:login-otp')
elif user.otp_enabled and not user.otp_secret_key:
# 1,2,mfa_setting & F
return reverse('users:user-otp-enable-authentication')
elif not user.otp_enabled:
# 0 & T,F
auth_login(self.request, user)
self.send_auth_signal(success=True, user=user)
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
def get_context_data(self, **kwargs):
context = {
'demo_mode': os.environ.get("DEMO_MODE"),
......@@ -141,51 +102,70 @@ class UserLoginView(FormView):
kwargs.update(context)
return super().get_context_data(**kwargs)
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 UserLoginOtpView(FormView):
template_name = 'authentication/login_otp.html'
form_class = forms.UserCheckOtpCodeForm
class UserLoginGuardView(mixins.AuthMixin, RedirectView):
redirect_field_name = 'next'
def form_valid(self, form):
user = get_user_or_tmp_user(self.request)
otp_code = form.cleaned_data.get('otp_code')
otp_secret_key = user.otp_secret_key
if check_otp_code(otp_secret_key, otp_code):
login_url = reverse_lazy('authentication:login')
login_otp_url = reverse_lazy('authentication:login-otp')
login_confirm_url = reverse_lazy('authentication:login-wait-confirm')
def format_redirect_url(self, url):
args = self.request.META.get('QUERY_STRING', '')
if args and self.query_string:
url = "%s?%s" % (url, args)
return url
def get_redirect_url(self, *args, **kwargs):
try:
user = self.check_user_auth_if_need()
self.check_user_mfa_if_need(user)
self.check_user_login_confirm_if_need(user)
except errors.CredentialError:
return self.format_redirect_url(self.login_url)
except errors.MFARequiredError:
return self.format_redirect_url(self.login_otp_url)
except errors.LoginConfirmBaseError:
return self.format_redirect_url(self.login_confirm_url)
else:
auth_login(self.request, user)
self.send_auth_signal(success=True, user=user)
return redirect(self.get_success_url())
else:
self.send_auth_signal(
success=False, username=user.username,
reason=const.mfa_failed
)
form.add_error(
'otp_code', _('MFA code invalid, or ntp sync server time')
self.clear_auth_mark()
# 启用但是没有设置otp, 排除radius
if user.mfa_enabled_but_not_set():
# 1,2,mfa_setting & F
return reverse('users:user-otp-enable-authentication')
url = redirect_user_first_login_or_index(
self.request, self.redirect_field_name
)
return super().form_invalid(form)
return url
def get_success_url(self):
return redirect_user_first_login_or_index(self.request, self.redirect_field_name)
class UserLoginWaitConfirmView(TemplateView):
template_name = 'authentication/login_wait_confirm.html'
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)
def get_context_data(self, **kwargs):
from tickets.models import Ticket
ticket_id = self.request.session.get("auth_ticket_id")
if not ticket_id:
ticket = None
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
ticket = get_object_or_none(Ticket, pk=ticket_id)
context = super().get_context_data(**kwargs)
if ticket:
timestamp_created = datetime.datetime.timestamp(ticket.date_created)
ticket_detail_url = reverse('tickets:ticket-detail', kwargs={'pk': ticket_id})
msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>
Don't close this page""").format(ticket.assignees_display)
else:
timestamp_created = 0
ticket_detail_url = ''
msg = _("No ticket found")
context.update({
"msg": msg,
"timestamp": timestamp_created,
"ticket_detail_url": ticket_detail_url
})
return context
@method_decorator(never_cache, name='dispatch')
......
# -*- 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:
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:
default_added_filters = [CustomFilter, IDSpmFilter]
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
......@@ -44,5 +53,5 @@ class ExtraFilterFieldsMixin:
return queryset
class CommonApiMixin(ExtraFilterFieldsMixin):
class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin):
pass
......@@ -53,3 +53,15 @@ class CommonModelMixin(models.Model):
class Meta:
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):
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):
try:
ipaddress.ip_address(ip)
......
......@@ -375,6 +375,7 @@ defaults = {
'RADIUS_SERVER': 'localhost',
'RADIUS_PORT': 1812,
'RADIUS_SECRET': '',
'RADIUS_ENCRYPT_PASSWORD': True,
'AUTH_LDAP_SEARCH_PAGED_SIZE': 1000,
'AUTH_LDAP_SYNC_IS_PERIODIC': False,
'AUTH_LDAP_SYNC_INTERVAL': None,
......@@ -394,8 +395,11 @@ defaults = {
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
'FLOWER_URL': "127.0.0.1:5555",
'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,
'OTP_IN_RADIUS': False,
}
......
......@@ -18,7 +18,9 @@ def jumpserver_processor(request):
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2019',
'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION,
'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,
'LOGIN_CONFIRM_ENABLE': settings.CONFIG.LOGIN_CONFIRM_ENABLE,
}
return context
......
......@@ -71,6 +71,7 @@ INSTALLED_APPS = [
'audits.apps.AuditsConfig',
'authentication.apps.AuthenticationConfig', # authentication
'applications.apps.ApplicationsConfig',
'tickets.apps.TicketsConfig',
'rest_framework',
'rest_framework_swagger',
'drf_yasg',
......@@ -331,7 +332,7 @@ LOCALE_PATHS = [
# Static files (CSS, JavaScript, Images)
# 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_DIR = os.path.join(BASE_DIR, "static")
......@@ -410,6 +411,7 @@ REST_FRAMEWORK = {
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'authentication.backends.pubkey.PublicKeyAuthBackend',
]
# Custom User Auth model
......@@ -655,3 +657,4 @@ CHANNEL_LAYERS = {
# Enable internal period task
PERIOD_TASK_ENABLED = CONFIG.PERIOD_TASK_ENABLED
FORCE_SCRIPT_NAME = CONFIG.FORCE_SCRIPT_NAME
......@@ -24,6 +24,7 @@ api_v1 = [
path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')),
path('common/', include('common.urls.api_urls', namespace='api-common')),
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
]
api_v2 = [
......@@ -42,6 +43,7 @@ app_view_patterns = [
path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
path('auth/', include('authentication.urls.view_urls'), name='auth'),
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'),
]
......@@ -65,7 +67,8 @@ urlpatterns = [
path('api/v2/', include(api_v2)),
re_path('api/(?P<app>\w+)/(?P<version>v\d)/.*', views.redirect_format_api),
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'),
path('i18n/<str:lang>/', views.I18NView.as_view(), name='i18n-switch'),
path('settings/', include('settings.urls.view_urls', namespace='settings')),
......
......@@ -234,3 +234,10 @@ class WsView(APIView):
.format(self.ws_port))
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)
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-13 16:38+0800\n"
"POT-Creation-Date: 2019-11-20 12:02+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
......@@ -73,31 +73,31 @@ msgstr "运行参数"
#: applications/forms/remote_app.py:100 applications/models/remote_app.py:23
#: applications/templates/applications/remote_app_detail.html:57
#: applications/templates/applications/remote_app_list.html:22
#: applications/templates/applications/remote_app_list.html:20
#: applications/templates/applications/user_remote_app_list.html:18
#: assets/forms/domain.py:15 assets/forms/label.py:13
#: assets/models/asset.py:295 assets/models/authbook.py:24
#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:32
#: assets/serializers/asset_user.py:82 assets/serializers/system_user.py:37
#: assets/templates/assets/admin_user_list.html:46
#: assets/templates/assets/admin_user_list.html:44
#: assets/templates/assets/domain_detail.html:60
#: assets/templates/assets/domain_list.html:26
#: assets/templates/assets/domain_list.html:22
#: assets/templates/assets/label_list.html:16
#: assets/templates/assets/system_user_list.html:51 audits/models.py:20
#: assets/templates/assets/system_user_list.html:49 audits/models.py:20
#: audits/templates/audits/ftp_log_list.html:44
#: audits/templates/audits/ftp_log_list.html:74
#: perms/forms/asset_permission.py:84 perms/models/asset_permission.py:80
#: perms/templates/perms/asset_permission_create_update.html:45
#: perms/templates/perms/asset_permission_list.html:52
#: perms/templates/perms/asset_permission_list.html:121
#: perms/templates/perms/asset_permission_list.html:112
#: terminal/backends/command/models.py:13 terminal/models.py:157
#: terminal/templates/terminal/command_list.html:30
#: terminal/templates/terminal/command_list.html:66
#: terminal/templates/terminal/session_list.html:28
#: terminal/templates/terminal/session_list.html:72
#: xpack/plugins/change_auth_plan/forms.py:73
#: xpack/plugins/change_auth_plan/models.py:412
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:46
#: xpack/plugins/change_auth_plan/models.py:419
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:44
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:13
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:14
......@@ -110,27 +110,27 @@ msgstr "资产"
#: applications/models/remote_app.py:21
#: applications/templates/applications/remote_app_detail.html:53
#: applications/templates/applications/remote_app_list.html:20
#: applications/templates/applications/remote_app_list.html:18
#: applications/templates/applications/user_remote_app_list.html:16
#: assets/forms/asset.py:21 assets/forms/domain.py:77 assets/forms/user.py:75
#: assets/forms/user.py:95 assets/models/base.py:28 assets/models/cluster.py:18
#: assets/models/cmd_filter.py:21 assets/models/domain.py:20
#: assets/models/group.py:20 assets/models/label.py:18
#: assets/templates/assets/admin_user_detail.html:56
#: assets/templates/assets/admin_user_list.html:44
#: assets/templates/assets/admin_user_list.html:42
#: assets/templates/assets/cmd_filter_detail.html:61
#: assets/templates/assets/cmd_filter_list.html:24
#: assets/templates/assets/cmd_filter_list.html:22
#: assets/templates/assets/domain_detail.html:56
#: assets/templates/assets/domain_gateway_list.html:67
#: assets/templates/assets/domain_list.html:25
#: assets/templates/assets/domain_list.html:21
#: assets/templates/assets/label_list.html:14
#: assets/templates/assets/system_user_detail.html:58
#: assets/templates/assets/system_user_list.html:47 ops/models/adhoc.py:37
#: assets/templates/assets/system_user_list.html:45 ops/models/adhoc.py:37
#: ops/templates/ops/task_detail.html:60 ops/templates/ops/task_list.html:11
#: orgs/models.py:12 perms/models/base.py:48
#: perms/templates/perms/asset_permission_detail.html:62
#: perms/templates/perms/asset_permission_list.html:49
#: perms/templates/perms/asset_permission_list.html:68
#: perms/templates/perms/asset_permission_list.html:208
#: perms/templates/perms/asset_permission_user.html:54
#: perms/templates/perms/remote_app_permission_detail.html:62
#: perms/templates/perms/remote_app_permission_list.html:14
......@@ -143,8 +143,9 @@ msgstr "资产"
#: settings/templates/settings/terminal_setting.html:83
#: settings/templates/settings/terminal_setting.html:105 terminal/models.py:23
#: terminal/models.py:260 terminal/templates/terminal/terminal_detail.html:43
#: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14
#: users/models/user.py:373 users/templates/users/_select_user_modal.html:13
#: terminal/templates/terminal/terminal_list.html:29 users/forms.py:162
#: users/models/group.py:14 users/models/user.py:429
#: users/templates/users/_select_user_modal.html:13
#: users/templates/users/user_detail.html:63
#: users/templates/users/user_group_detail.html:55
#: users/templates/users/user_group_list.html:35
......@@ -152,7 +153,7 @@ msgstr "资产"
#: users/templates/users/user_profile.html:51
#: users/templates/users/user_pubkey_update.html:57
#: xpack/plugins/change_auth_plan/forms.py:56
#: xpack/plugins/change_auth_plan/models.py:63
#: xpack/plugins/change_auth_plan/models.py:64
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:61
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:12
#: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:144
......@@ -169,7 +170,7 @@ msgstr "名称"
#: applications/models/remote_app.py:28
#: applications/templates/applications/remote_app_detail.html:61
#: applications/templates/applications/remote_app_list.html:21
#: applications/templates/applications/remote_app_list.html:19
#: applications/templates/applications/user_remote_app_list.html:17
msgid "App type"
msgstr "应用类型"
......@@ -197,9 +198,9 @@ msgstr "参数"
#: orgs/models.py:16 perms/models/base.py:54
#: perms/templates/perms/asset_permission_detail.html:98
#: perms/templates/perms/remote_app_permission_detail.html:90
#: users/models/user.py:414 users/serializers/group.py:32
#: users/models/user.py:470 users/serializers/group.py:32
#: users/templates/users/user_detail.html:111
#: xpack/plugins/change_auth_plan/models.py:108
#: xpack/plugins/change_auth_plan/models.py:109
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113
#: xpack/plugins/cloud/models.py:80 xpack/plugins/cloud/models.py:179
#: xpack/plugins/gathered_user/models.py:46
......@@ -222,7 +223,8 @@ msgstr "创建者"
#: orgs/models.py:17 perms/models/base.py:55
#: perms/templates/perms/asset_permission_detail.html:94
#: perms/templates/perms/remote_app_permission_detail.html:86
#: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17
#: terminal/templates/terminal/terminal_detail.html:59
#: tickets/templates/tickets/ticket_detail.html:52 users/models/group.py:17
#: users/templates/users/user_group_detail.html:63
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:105
#: xpack/plugins/cloud/models.py:83 xpack/plugins/cloud/models.py:182
......@@ -236,33 +238,34 @@ msgstr "创建日期"
# msgstr "创建日期"
#: applications/models/remote_app.py:45
#: applications/templates/applications/remote_app_detail.html:77
#: applications/templates/applications/remote_app_list.html:23
#: applications/templates/applications/remote_app_list.html:21
#: applications/templates/applications/user_remote_app_list.html:19
#: assets/models/asset.py:176 assets/models/base.py:33
#: assets/models/cluster.py:29 assets/models/cmd_filter.py:23
#: assets/models/cmd_filter.py:56 assets/models/domain.py:21
#: assets/models/domain.py:53 assets/models/group.py:23
#: assets/models/label.py:23 assets/templates/assets/admin_user_detail.html:72
#: assets/templates/assets/admin_user_list.html:50
#: assets/templates/assets/admin_user_list.html:48
#: assets/templates/assets/asset_detail.html:130
#: assets/templates/assets/cmd_filter_detail.html:65
#: assets/templates/assets/cmd_filter_list.html:27
#: assets/templates/assets/cmd_filter_list.html:25
#: assets/templates/assets/cmd_filter_rule_list.html:62
#: assets/templates/assets/domain_detail.html:76
#: assets/templates/assets/domain_gateway_list.html:72
#: assets/templates/assets/domain_list.html:28
#: assets/templates/assets/domain_list.html:24
#: assets/templates/assets/system_user_detail.html:104
#: assets/templates/assets/system_user_list.html:55 ops/models/adhoc.py:43
#: assets/templates/assets/system_user_list.html:53 ops/models/adhoc.py:43
#: orgs/models.py:18 perms/models/base.py:56
#: perms/templates/perms/asset_permission_detail.html:102
#: perms/templates/perms/remote_app_permission_detail.html:94
#: settings/models.py:34 terminal/models.py:33
#: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15
#: users/models/user.py:406 users/templates/users/user_detail.html:129
#: terminal/templates/terminal/terminal_detail.html:63
#: tickets/templates/tickets/ticket_detail.html:104 users/models/group.py:15
#: users/models/user.py:462 users/templates/users/user_detail.html:129
#: users/templates/users/user_group_detail.html:67
#: users/templates/users/user_group_list.html:37
#: users/templates/users/user_profile.html:138
#: xpack/plugins/change_auth_plan/models.py:104
#: xpack/plugins/change_auth_plan/models.py:105
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:117
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:19
#: xpack/plugins/cloud/models.py:77 xpack/plugins/cloud/models.py:173
......@@ -310,14 +313,14 @@ msgstr "远程应用"
#: terminal/templates/terminal/terminal_update.html:45
#: users/templates/users/_user.html:51
#: users/templates/users/user_bulk_update.html:23
#: users/templates/users/user_detail.html:178
#: users/templates/users/user_detail.html:180
#: users/templates/users/user_group_create_update.html:31
#: users/templates/users/user_password_update.html:75
#: users/templates/users/user_profile.html:209
#: users/templates/users/user_profile_update.html:67
#: users/templates/users/user_pubkey_update.html:74
#: users/templates/users/user_pubkey_update.html:80
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:71
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:69
#: xpack/plugins/cloud/templates/cloud/account_create_update.html:33
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:53
#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:44
......@@ -359,7 +362,7 @@ msgstr "重置"
#: users/templates/users/user_password_update.html:76
#: users/templates/users/user_profile_update.html:68
#: users/templates/users/user_pubkey_update.html:81
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:72
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:70
#: xpack/plugins/interface/templates/interface/interface.html:74
#: xpack/plugins/vault/templates/vault/vault_create.html:46
msgid "Submit"
......@@ -392,27 +395,27 @@ msgid "Detail"
msgstr "详情"
#: applications/templates/applications/remote_app_detail.html:21
#: applications/templates/applications/remote_app_list.html:54
#: applications/templates/applications/remote_app_list.html:52
#: assets/templates/assets/_asset_user_list.html:75
#: assets/templates/assets/admin_user_detail.html:24
#: assets/templates/assets/admin_user_list.html:26
#: assets/templates/assets/admin_user_list.html:74
#: assets/templates/assets/admin_user_list.html:24
#: assets/templates/assets/admin_user_list.html:72
#: assets/templates/assets/asset_detail.html:26
#: assets/templates/assets/asset_list.html:78
#: assets/templates/assets/asset_list.html:167
#: assets/templates/assets/cmd_filter_detail.html:29
#: assets/templates/assets/cmd_filter_list.html:58
#: assets/templates/assets/cmd_filter_list.html:56
#: assets/templates/assets/cmd_filter_rule_list.html:86
#: assets/templates/assets/domain_detail.html:24
#: assets/templates/assets/domain_detail.html:103
#: assets/templates/assets/domain_gateway_list.html:97
#: assets/templates/assets/domain_list.html:54
#: assets/templates/assets/domain_list.html:50
#: assets/templates/assets/label_list.html:39
#: assets/templates/assets/system_user_detail.html:26
#: assets/templates/assets/system_user_list.html:29
#: assets/templates/assets/system_user_list.html:81 audits/models.py:34
#: assets/templates/assets/system_user_list.html:27
#: assets/templates/assets/system_user_list.html:79 audits/models.py:34
#: perms/templates/perms/asset_permission_detail.html:30
#: perms/templates/perms/asset_permission_list.html:178
#: perms/templates/perms/asset_permission_list.html:169
#: perms/templates/perms/remote_app_permission_detail.html:30
#: perms/templates/perms/remote_app_permission_list.html:64
#: terminal/templates/terminal/terminal_detail.html:16
......@@ -440,25 +443,25 @@ msgid "Update"
msgstr "更新"
#: applications/templates/applications/remote_app_detail.html:25
#: applications/templates/applications/remote_app_list.html:55
#: applications/templates/applications/remote_app_list.html:53
#: assets/templates/assets/admin_user_detail.html:28
#: assets/templates/assets/admin_user_list.html:75
#: assets/templates/assets/admin_user_list.html:73
#: assets/templates/assets/asset_detail.html:30
#: assets/templates/assets/asset_list.html:168
#: assets/templates/assets/cmd_filter_detail.html:33
#: assets/templates/assets/cmd_filter_list.html:59
#: assets/templates/assets/cmd_filter_list.html:57
#: assets/templates/assets/cmd_filter_rule_list.html:87
#: assets/templates/assets/domain_detail.html:28
#: assets/templates/assets/domain_detail.html:104
#: assets/templates/assets/domain_gateway_list.html:98
#: assets/templates/assets/domain_list.html:55
#: assets/templates/assets/domain_list.html:51
#: assets/templates/assets/label_list.html:40
#: assets/templates/assets/system_user_detail.html:30
#: assets/templates/assets/system_user_list.html:82 audits/models.py:35
#: assets/templates/assets/system_user_list.html:80 audits/models.py:35
#: authentication/templates/authentication/_access_key_modal.html:65
#: ops/templates/ops/task_list.html:69
#: perms/templates/perms/asset_permission_detail.html:34
#: perms/templates/perms/asset_permission_list.html:179
#: perms/templates/perms/asset_permission_list.html:170
#: perms/templates/perms/remote_app_permission_detail.html:34
#: perms/templates/perms/remote_app_permission_list.html:65
#: settings/templates/settings/terminal_setting.html:93
......@@ -481,7 +484,7 @@ msgstr "更新"
msgid "Delete"
msgstr "删除"
#: applications/templates/applications/remote_app_list.html:5
#: applications/templates/applications/remote_app_list.html:4
msgid ""
"Before using this feature, make sure that the application loader has been "
"uploaded to the application server and successfully published as a RemoteApp "
......@@ -490,27 +493,27 @@ msgstr ""
"使用此功能前,请确保已将应用加载器上传到应用服务器并成功发布为一个 RemoteApp "
"应用"
#: applications/templates/applications/remote_app_list.html:6
#: applications/templates/applications/remote_app_list.html:5
msgid "Download application loader"
msgstr "下载应用加载器"
#: applications/templates/applications/remote_app_list.html:12
#: applications/templates/applications/remote_app_list.html:10
#: applications/views/remote_app.py:48
msgid "Create RemoteApp"
msgstr "创建远程应用"
#: applications/templates/applications/remote_app_list.html:24
#: applications/templates/applications/remote_app_list.html:22
#: applications/templates/applications/user_remote_app_list.html:20
#: assets/models/cmd_filter.py:55
#: assets/templates/assets/_asset_user_list.html:25
#: assets/templates/assets/admin_user_list.html:51
#: assets/templates/assets/admin_user_list.html:49
#: assets/templates/assets/asset_list.html:100
#: assets/templates/assets/cmd_filter_list.html:28
#: assets/templates/assets/cmd_filter_list.html:26
#: assets/templates/assets/cmd_filter_rule_list.html:63
#: assets/templates/assets/domain_gateway_list.html:73
#: assets/templates/assets/domain_list.html:29
#: assets/templates/assets/domain_list.html:25
#: assets/templates/assets/label_list.html:17
#: assets/templates/assets/system_user_list.html:56 audits/models.py:39
#: assets/templates/assets/system_user_list.html:54 audits/models.py:39
#: audits/templates/audits/operate_log_list.html:47
#: audits/templates/audits/operate_log_list.html:73
#: authentication/templates/authentication/_access_key_modal.html:34
......@@ -519,12 +522,13 @@ msgstr "创建远程应用"
#: perms/forms/asset_permission.py:21
#: perms/templates/perms/asset_permission_create_update.html:50
#: perms/templates/perms/asset_permission_list.html:56
#: perms/templates/perms/asset_permission_list.html:130
#: perms/templates/perms/asset_permission_list.html:121
#: perms/templates/perms/remote_app_permission_list.html:20
#: settings/templates/settings/terminal_setting.html:85
#: settings/templates/settings/terminal_setting.html:107
#: terminal/templates/terminal/session_list.html:36
#: terminal/templates/terminal/terminal_list.html:36
#: tickets/templates/tickets/ticket_list.html:105
#: users/templates/users/_granted_assets.html:34
#: users/templates/users/user_group_list.html:38
#: users/templates/users/user_list.html:41
......@@ -602,7 +606,7 @@ msgstr "端口"
#: assets/templates/assets/asset_detail.html:196
#: assets/templates/assets/system_user_assets.html:83
#: perms/models/asset_permission.py:81
#: xpack/plugins/change_auth_plan/models.py:74
#: xpack/plugins/change_auth_plan/models.py:75
#: xpack/plugins/gathered_user/models.py:31
#: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:17
msgid "Nodes"
......@@ -639,8 +643,8 @@ msgstr "网域"
#: assets/templates/assets/asset_create.html:42
#: perms/forms/asset_permission.py:87 perms/forms/asset_permission.py:94
#: perms/templates/perms/asset_permission_list.html:53
#: perms/templates/perms/asset_permission_list.html:74
#: perms/templates/perms/asset_permission_list.html:124
#: perms/templates/perms/asset_permission_list.html:115
#: perms/templates/perms/asset_permission_list.html:214
#: xpack/plugins/change_auth_plan/forms.py:74
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:55
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:15
......@@ -681,41 +685,42 @@ msgstr "内容不能包含: {}"
#: assets/forms/domain.py:55 assets/models/domain.py:67
msgid "Password should not contain special characters"
msgstr "密码不能包含特殊字符"
msgstr "不能包含特殊字符"
#: assets/forms/domain.py:74
msgid "SSH gateway support proxy SSH,RDP,VNC"
msgstr "SSH网关,支持代理SSH,RDP和VNC"
#: assets/forms/domain.py:78 assets/forms/user.py:76 assets/forms/user.py:96
#: assets/models/base.py:29 assets/models/gathered_user.py:16
#: assets/models/base.py:29 assets/models/gathered_user.py:15
#: assets/templates/assets/_asset_user_auth_update_modal.html:15
#: assets/templates/assets/_asset_user_auth_view_modal.html:21
#: assets/templates/assets/_asset_user_list.html:21
#: assets/templates/assets/admin_user_detail.html:60
#: assets/templates/assets/admin_user_list.html:45
#: assets/templates/assets/admin_user_list.html:43
#: assets/templates/assets/domain_gateway_list.html:71
#: assets/templates/assets/system_user_detail.html:62
#: assets/templates/assets/system_user_list.html:48 audits/models.py:81
#: assets/templates/assets/system_user_list.html:46 audits/models.py:81
#: audits/templates/audits/login_log_list.html:57 authentication/forms.py:13
#: authentication/templates/authentication/login.html:65
#: authentication/templates/authentication/new_login.html:92
#: ops/models/adhoc.py:189 perms/templates/perms/asset_permission_list.html:70
#: authentication/templates/authentication/login.html:58
#: authentication/templates/authentication/xpack_login.html:93
#: ops/models/adhoc.py:189 perms/templates/perms/asset_permission_list.html:210
#: perms/templates/perms/asset_permission_user.html:55
#: perms/templates/perms/remote_app_permission_user.html:54
#: settings/templates/settings/_ldap_list_users_modal.html:31 users/forms.py:14
#: users/models/user.py:371 users/templates/users/_select_user_modal.html:14
#: users/forms.py:161 users/models/user.py:427
#: users/templates/users/_select_user_modal.html:14
#: users/templates/users/user_detail.html:67
#: users/templates/users/user_list.html:36
#: users/templates/users/user_profile.html:47
#: xpack/plugins/change_auth_plan/forms.py:58
#: xpack/plugins/change_auth_plan/models.py:65
#: xpack/plugins/change_auth_plan/models.py:408
#: xpack/plugins/change_auth_plan/models.py:66
#: xpack/plugins/change_auth_plan/models.py:415
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:65
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:53
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:12
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:13
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:74
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:64
msgid "Username"
msgstr "用户名"
......@@ -728,8 +733,8 @@ msgstr "密码或密钥密码"
#: assets/templates/assets/_asset_user_auth_update_modal.html:21
#: assets/templates/assets/_asset_user_auth_view_modal.html:27
#: authentication/forms.py:15
#: authentication/templates/authentication/login.html:68
#: authentication/templates/authentication/new_login.html:95
#: authentication/templates/authentication/login.html:66
#: authentication/templates/authentication/xpack_login.html:101
#: settings/forms.py:114 users/forms.py:16 users/forms.py:42
#: users/templates/users/reset_password.html:53
#: users/templates/users/user_password_authentication.html:18
......@@ -737,14 +742,14 @@ msgstr "密码或密钥密码"
#: users/templates/users/user_profile_update.html:41
#: users/templates/users/user_pubkey_update.html:41
#: users/templates/users/user_update.html:20
#: xpack/plugins/change_auth_plan/models.py:95
#: xpack/plugins/change_auth_plan/models.py:263
#: xpack/plugins/change_auth_plan/models.py:96
#: xpack/plugins/change_auth_plan/models.py:264
msgid "Password"
msgstr "密码"
#: assets/forms/user.py:30 assets/serializers/asset_user.py:71
#: assets/templates/assets/_asset_user_auth_update_modal.html:27
#: users/models/user.py:400
#: users/models/user.py:456
msgid "Private key"
msgstr "ssh私钥"
......@@ -793,15 +798,16 @@ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig"
#: assets/templates/assets/domain_gateway_list.html:68
#: assets/templates/assets/user_asset_list.html:76
#: audits/templates/audits/login_log_list.html:60
#: perms/templates/perms/asset_permission_asset.html:58 settings/forms.py:144
#: perms/templates/perms/asset_permission_asset.html:58
#: perms/templates/perms/asset_permission_list.html:212 settings/forms.py:144
#: users/templates/users/_granted_assets.html:31
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:54
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:73
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:63
msgid "IP"
msgstr "IP"
#: assets/models/asset.py:136 assets/serializers/asset_user.py:27
#: assets/serializers/gathered_user.py:19
#: assets/serializers/gathered_user.py:20
#: assets/templates/assets/_asset_list_modal.html:46
#: assets/templates/assets/_asset_user_auth_update_modal.html:9
#: assets/templates/assets/_asset_user_auth_view_modal.html:15
......@@ -810,10 +816,10 @@ msgstr "IP"
#: assets/templates/assets/asset_list.html:96
#: assets/templates/assets/user_asset_list.html:75
#: perms/templates/perms/asset_permission_asset.html:57
#: perms/templates/perms/asset_permission_list.html:73 settings/forms.py:143
#: perms/templates/perms/asset_permission_list.html:213 settings/forms.py:143
#: users/templates/users/_granted_assets.html:30
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:53
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:72
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:62
msgid "Hostname"
msgstr "主机名"
......@@ -821,7 +827,7 @@ msgstr "主机名"
#: assets/models/user.py:113 assets/templates/assets/asset_detail.html:70
#: assets/templates/assets/domain_gateway_list.html:70
#: assets/templates/assets/system_user_detail.html:70
#: assets/templates/assets/system_user_list.html:49
#: assets/templates/assets/system_user_list.html:47
#: terminal/templates/terminal/session_list.html:31
#: terminal/templates/terminal/session_list.html:75
msgid "Protocol"
......@@ -842,6 +848,7 @@ msgstr "系统平台"
#: assets/models/asset.py:146 assets/models/authbook.py:27
#: assets/models/cmd_filter.py:22 assets/models/domain.py:54
#: assets/models/label.py:22 assets/templates/assets/asset_detail.html:110
#: authentication/models.py:45
msgid "Is active"
msgstr "激活"
......@@ -931,21 +938,21 @@ msgstr "版本"
msgid "AuthBook"
msgstr ""
#: assets/models/base.py:31 xpack/plugins/change_auth_plan/models.py:99
#: xpack/plugins/change_auth_plan/models.py:270
#: assets/models/base.py:31 xpack/plugins/change_auth_plan/models.py:100
#: xpack/plugins/change_auth_plan/models.py:271
msgid "SSH private key"
msgstr "ssh密钥"
#: assets/models/base.py:32 xpack/plugins/change_auth_plan/models.py:102
#: xpack/plugins/change_auth_plan/models.py:266
#: assets/models/base.py:32 xpack/plugins/change_auth_plan/models.py:103
#: xpack/plugins/change_auth_plan/models.py:267
msgid "SSH public key"
msgstr "ssh公钥"
#: assets/models/base.py:35 assets/models/gathered_user.py:21
#: assets/models/base.py:35 assets/models/gathered_user.py:20
#: assets/templates/assets/cmd_filter_detail.html:73 common/mixins/models.py:52
#: ops/models/adhoc.py:46
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:109
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:76
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:68
msgid "Date updated"
msgstr "更新日期"
......@@ -957,7 +964,7 @@ msgstr "带宽"
msgid "Contact"
msgstr "联系人"
#: assets/models/cluster.py:22 users/models/user.py:392
#: assets/models/cluster.py:22 users/models/user.py:448
#: users/templates/users/user_detail.html:76
msgid "Phone"
msgstr "手机"
......@@ -983,7 +990,7 @@ msgid "Default"
msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14
#: users/models/user.py:512
#: users/models/user.py:568
msgid "System"
msgstr "系统"
......@@ -1011,7 +1018,7 @@ msgstr "BGP全网通"
msgid "Regex"
msgstr "正则表达式"
#: assets/models/cmd_filter.py:40 ops/models/command.py:21
#: assets/models/cmd_filter.py:40 ops/models/command.py:22
#: ops/templates/ops/command_execution_list.html:64 terminal/models.py:163
#: terminal/templates/terminal/command_list.html:28
#: terminal/templates/terminal/command_list.html:68
......@@ -1040,6 +1047,8 @@ msgstr "过滤器"
#: settings/templates/settings/replay_storage_create.html:31
#: settings/templates/settings/terminal_setting.html:84
#: settings/templates/settings/terminal_setting.html:106
#: tickets/models/ticket.py:43 tickets/templates/tickets/ticket_detail.html:33
#: tickets/templates/tickets/ticket_list.html:35
msgid "Type"
msgstr "类型"
......@@ -1069,16 +1078,26 @@ msgstr "命令过滤规则"
#: assets/models/domain.py:61 assets/templates/assets/domain_detail.html:21
#: assets/templates/assets/domain_detail.html:64
#: assets/templates/assets/domain_gateway_list.html:26
#: assets/templates/assets/domain_list.html:27
#: assets/templates/assets/domain_list.html:23
msgid "Gateway"
msgstr "网关"
#: assets/models/gathered_user.py:17
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:75
#: assets/models/gathered_user.py:16
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:67
msgid "Present"
msgstr "存在"
#: assets/models/gathered_user.py:32
#: assets/models/gathered_user.py:17
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:65
msgid "Date last login"
msgstr "最后登录日期"
#: assets/models/gathered_user.py:18
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:66
msgid "IP last login"
msgstr "最后登录IP"
#: assets/models/gathered_user.py:31
msgid "GatherUser"
msgstr "收集用户"
......@@ -1097,21 +1116,24 @@ msgstr "默认资产组"
#: audits/templates/audits/operate_log_list.html:72
#: audits/templates/audits/password_change_log_list.html:39
#: audits/templates/audits/password_change_log_list.html:56
#: ops/templates/ops/command_execution_list.html:38
#: authentication/models.py:43 ops/templates/ops/command_execution_list.html:38
#: ops/templates/ops/command_execution_list.html:63
#: perms/forms/asset_permission.py:78 perms/forms/remote_app_permission.py:34
#: perms/models/base.py:49
#: perms/templates/perms/asset_permission_create_update.html:41
#: perms/templates/perms/asset_permission_list.html:50
#: perms/templates/perms/asset_permission_list.html:115
#: perms/templates/perms/asset_permission_list.html:106
#: perms/templates/perms/remote_app_permission_create_update.html:43
#: perms/templates/perms/remote_app_permission_list.html:15
#: templates/index.html:87 terminal/backends/command/models.py:12
#: terminal/models.py:156 terminal/templates/terminal/command_list.html:29
#: terminal/templates/terminal/command_list.html:65
#: terminal/templates/terminal/session_list.html:27
#: terminal/templates/terminal/session_list.html:71 users/forms.py:339
#: users/models/user.py:127 users/models/user.py:143 users/models/user.py:500
#: terminal/templates/terminal/session_list.html:71 tickets/models/ticket.py:33
#: tickets/models/ticket.py:128 tickets/templates/tickets/ticket_detail.html:32
#: tickets/templates/tickets/ticket_list.html:34
#: tickets/templates/tickets/ticket_list.html:100 users/forms.py:339
#: users/models/user.py:148 users/models/user.py:164 users/models/user.py:556
#: users/serializers/group.py:21
#: users/templates/users/user_group_detail.html:78
#: users/templates/users/user_group_list.html:36 users/views/user.py:250
......@@ -1177,7 +1199,7 @@ msgstr "手动登录"
#: assets/views/label.py:27 assets/views/label.py:45 assets/views/label.py:73
#: assets/views/system_user.py:29 assets/views/system_user.py:46
#: assets/views/system_user.py:63 assets/views/system_user.py:79
#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:70
#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:71
msgid "Assets"
msgstr "资产管理"
......@@ -1196,7 +1218,7 @@ msgid "Shell"
msgstr "Shell"
#: assets/models/user.py:117 assets/templates/assets/system_user_detail.html:66
#: assets/templates/assets/system_user_list.html:50
#: assets/templates/assets/system_user_list.html:48
msgid "Login mode"
msgstr "登录模式"
......@@ -1207,8 +1229,8 @@ msgstr "登录模式"
#: perms/models/asset_permission.py:82 perms/models/remote_app_permission.py:16
#: perms/templates/perms/asset_permission_detail.html:140
#: perms/templates/perms/asset_permission_list.html:54
#: perms/templates/perms/asset_permission_list.html:75
#: perms/templates/perms/asset_permission_list.html:127
#: perms/templates/perms/asset_permission_list.html:118
#: perms/templates/perms/asset_permission_list.html:215
#: perms/templates/perms/remote_app_permission_detail.html:131
#: perms/templates/perms/remote_app_permission_list.html:18
#: templates/_nav.html:45 terminal/backends/command/models.py:14
......@@ -1235,8 +1257,8 @@ msgstr "不可达"
msgid "Reachable"
msgstr "可连接"
#: assets/models/utils.py:45 assets/tasks/const.py:86
#: authentication/utils.py:13 xpack/plugins/license/models.py:78
#: assets/models/utils.py:45 assets/tasks/const.py:86 audits/utils.py:30
#: xpack/plugins/license/models.py:78
msgid "Unknown"
msgstr "未知"
......@@ -1267,7 +1289,7 @@ msgid "Backend"
msgstr "后端"
#: assets/serializers/asset_user.py:67 users/forms.py:282
#: users/models/user.py:403 users/templates/users/first_login.html:42
#: users/models/user.py:459 users/templates/users/first_login.html:42
#: users/templates/users/user_password_update.html:49
#: users/templates/users/user_profile.html:69
#: users/templates/users/user_profile_update.html:46
......@@ -1322,7 +1344,7 @@ msgstr "测试资产可连接性: {}"
#: assets/tasks/asset_user_connectivity.py:27
#: assets/tasks/push_system_user.py:130
#: xpack/plugins/change_auth_plan/models.py:521
#: xpack/plugins/change_auth_plan/models.py:528
msgid "The asset {} system platform {} does not support run Ansible tasks"
msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务"
......@@ -1342,7 +1364,7 @@ msgstr "更新资产硬件信息"
msgid "Update asset hardware info: {}"
msgstr "更新资产硬件信息: {}"
#: assets/tasks/gather_asset_users.py:96
#: assets/tasks/gather_asset_users.py:107
msgid "Gather assets users"
msgstr "收集资产上的用户"
......@@ -1415,7 +1437,7 @@ msgstr "选择资产"
#: assets/templates/assets/_asset_group_bulk_update_modal.html:21
#: assets/templates/assets/cmd_filter_detail.html:89
#: assets/templates/assets/cmd_filter_list.html:26
#: assets/templates/assets/cmd_filter_list.html:24
msgid "System users"
msgstr "系统用户"
......@@ -1461,8 +1483,8 @@ msgstr "请输入密码"
#: assets/templates/assets/_asset_user_auth_update_modal.html:68
#: assets/templates/assets/asset_detail.html:302
#: users/templates/users/user_detail.html:313
#: users/templates/users/user_detail.html:340
#: users/templates/users/user_detail.html:367
#: users/templates/users/user_detail.html:394
#: xpack/plugins/interface/views.py:35
msgid "Update successfully!"
msgstr "更新成功"
......@@ -1472,6 +1494,7 @@ msgid "Asset user auth"
msgstr "资产用户信息"
#: assets/templates/assets/_asset_user_auth_view_modal.html:54
#: authentication/templates/authentication/login_wait_confirm.html:114
msgid "Copy success"
msgstr "复制成功"
......@@ -1484,7 +1507,8 @@ msgstr "获取认证信息错误"
#: authentication/templates/authentication/_access_key_modal.html:142
#: authentication/templates/authentication/_mfa_confirm_modal.html:53
#: settings/templates/settings/_ldap_list_users_modal.html:171
#: templates/_modal.html:22
#: templates/_modal.html:22 tickets/models/ticket.py:68
#: tickets/templates/tickets/ticket_detail.html:103
msgid "Close"
msgstr "关闭"
......@@ -1494,6 +1518,7 @@ msgstr "关闭"
#: ops/templates/ops/task_adhoc.html:63
#: terminal/templates/terminal/command_list.html:33
#: terminal/templates/terminal/session_detail.html:50
#: tickets/templates/tickets/ticket_list.html:37
msgid "Datetime"
msgstr "日期"
......@@ -1566,7 +1591,7 @@ msgstr "重命名成功"
#: assets/templates/assets/gateway_create_update.html:37
#: perms/templates/perms/asset_permission_create_update.html:38
#: perms/templates/perms/remote_app_permission_create_update.html:39
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:43
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:41
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:27
#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:27
msgid "Basic"
......@@ -1589,7 +1614,7 @@ msgstr "自动生成密钥"
#: perms/templates/perms/asset_permission_create_update.html:53
#: perms/templates/perms/remote_app_permission_create_update.html:53
#: terminal/templates/terminal/terminal_update.html:40
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:67
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:65
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:48
#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:39
msgid "Other"
......@@ -1649,18 +1674,19 @@ msgstr "选择节点"
#: assets/templates/assets/admin_user_detail.html:100
#: assets/templates/assets/asset_detail.html:202
#: assets/templates/assets/asset_list.html:423
#: assets/templates/assets/asset_list.html:427
#: assets/templates/assets/cmd_filter_detail.html:106
#: assets/templates/assets/system_user_assets.html:97
#: assets/templates/assets/system_user_detail.html:182
#: assets/templates/assets/system_user_list.html:135
#: assets/templates/assets/system_user_list.html:133
#: authentication/templates/authentication/_mfa_confirm_modal.html:20
#: settings/templates/settings/terminal_setting.html:168
#: templates/_modal.html:23 terminal/templates/terminal/session_detail.html:112
#: users/templates/users/user_detail.html:394
#: users/templates/users/user_detail.html:420
#: users/templates/users/user_detail.html:443
#: users/templates/users/user_detail.html:488
#: users/templates/users/user_detail.html:272
#: users/templates/users/user_detail.html:448
#: users/templates/users/user_detail.html:474
#: users/templates/users/user_detail.html:497
#: users/templates/users/user_detail.html:542
#: users/templates/users/user_group_create_update.html:32
#: users/templates/users/user_group_list.html:120
#: users/templates/users/user_list.html:256
......@@ -1672,57 +1698,57 @@ msgstr "选择节点"
msgid "Confirm"
msgstr "确认"
#: assets/templates/assets/admin_user_list.html:5
#: assets/templates/assets/admin_user_list.html:4
msgid ""
"Admin users are asset (charged server) on the root, or have NOPASSWD: ALL "
"sudo permissions users, "
msgstr ""
"管理用户是资产(被控服务器)上的 root,或拥有 NOPASSWD: ALL sudo 权限的用户,"
#: assets/templates/assets/admin_user_list.html:6
#: assets/templates/assets/admin_user_list.html:5
msgid ""
"Jumpserver users of the system using the user to `push system user`, `get "
"assets hardware information`, etc. "
msgstr "Jumpserver 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。"
#: assets/templates/assets/admin_user_list.html:16
#: assets/templates/assets/admin_user_list.html:14
#: assets/templates/assets/asset_list.html:68
#: assets/templates/assets/system_user_list.html:19
#: assets/templates/assets/system_user_list.html:17
#: audits/templates/audits/login_log_list.html:91
#: users/templates/users/user_group_list.html:10
#: users/templates/users/user_list.html:10
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:59
#: xpack/plugins/vault/templates/vault/vault.html:55
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:49
#: xpack/plugins/vault/templates/vault/vault.html:47
msgid "Export"
msgstr "导出"
#: assets/templates/assets/admin_user_list.html:21
#: assets/templates/assets/admin_user_list.html:19
#: assets/templates/assets/asset_list.html:73
#: assets/templates/assets/system_user_list.html:24
#: assets/templates/assets/system_user_list.html:22
#: settings/templates/settings/_ldap_list_users_modal.html:172
#: users/templates/users/user_group_list.html:15
#: users/templates/users/user_list.html:15
#: xpack/plugins/license/templates/license/license_detail.html:110
#: xpack/plugins/vault/templates/vault/vault.html:60
#: xpack/plugins/vault/templates/vault/vault.html:52
msgid "Import"
msgstr "导入"
#: assets/templates/assets/admin_user_list.html:36
#: assets/templates/assets/admin_user_list.html:34
#: assets/views/admin_user.py:50
msgid "Create admin user"
msgstr "创建管理用户"
#: assets/templates/assets/admin_user_list.html:125
#: assets/templates/assets/admin_user_list.html:156
#: assets/templates/assets/admin_user_list.html:123
#: assets/templates/assets/admin_user_list.html:154
#: assets/templates/assets/asset_list.html:304
#: assets/templates/assets/asset_list.html:341
#: assets/templates/assets/system_user_list.html:188
#: assets/templates/assets/system_user_list.html:219
#: assets/templates/assets/system_user_list.html:186
#: assets/templates/assets/system_user_list.html:217
#: users/templates/users/user_group_list.html:164
#: users/templates/users/user_group_list.html:195
#: users/templates/users/user_list.html:165
#: users/templates/users/user_list.html:197
#: xpack/plugins/vault/templates/vault/vault.html:224
#: xpack/plugins/vault/templates/vault/vault.html:200
msgid "Please select file"
msgstr "选择文件"
......@@ -1770,7 +1796,7 @@ msgstr "硬盘"
msgid "Date joined"
msgstr "创建日期"
#: assets/templates/assets/asset_detail.html:148 authentication/models.py:15
#: assets/templates/assets/asset_detail.html:148 authentication/models.py:19
#: authentication/templates/authentication/_access_key_modal.html:32
#: perms/models/base.py:51
#: perms/templates/perms/asset_permission_create_update.html:55
......@@ -1789,10 +1815,11 @@ msgid "Refresh hardware"
msgstr "更新硬件信息"
#: assets/templates/assets/asset_detail.html:168
#: authentication/templates/authentication/login_wait_confirm.html:42
msgid "Refresh"
msgstr "刷新"
#: assets/templates/assets/asset_list.html:8
#: assets/templates/assets/asset_list.html:9
msgid ""
"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 "
......@@ -1857,43 +1884,43 @@ msgstr "仅显示当前节点资产"
msgid "Displays all child node assets"
msgstr "显示所有子节点资产"
#: assets/templates/assets/asset_list.html:417
#: assets/templates/assets/system_user_list.html:129
#: users/templates/users/user_detail.html:388
#: users/templates/users/user_detail.html:414
#: users/templates/users/user_detail.html:482
#: assets/templates/assets/asset_list.html:421
#: assets/templates/assets/system_user_list.html:127
#: users/templates/users/user_detail.html:442
#: users/templates/users/user_detail.html:468
#: users/templates/users/user_detail.html:536
#: users/templates/users/user_group_list.html:114
#: users/templates/users/user_list.html:250
#: xpack/plugins/interface/templates/interface/interface.html:97
msgid "Are you sure?"
msgstr "你确认吗?"
#: assets/templates/assets/asset_list.html:418
#: assets/templates/assets/asset_list.html:422
msgid "This will delete the selected assets !!!"
msgstr "删除选择资产"
#: assets/templates/assets/asset_list.html:421
#: assets/templates/assets/system_user_list.html:133
#: assets/templates/assets/asset_list.html:425
#: assets/templates/assets/system_user_list.html:131
#: settings/templates/settings/terminal_setting.html:166
#: users/templates/users/user_detail.html:392
#: users/templates/users/user_detail.html:418
#: users/templates/users/user_detail.html:486
#: users/templates/users/user_detail.html:446
#: users/templates/users/user_detail.html:472
#: users/templates/users/user_detail.html:540
#: users/templates/users/user_group_list.html:118
#: users/templates/users/user_list.html:254
#: xpack/plugins/interface/templates/interface/interface.html:101
msgid "Cancel"
msgstr "取消"
#: assets/templates/assets/asset_list.html:432
#: assets/templates/assets/asset_list.html:436
msgid "Asset Deleted."
msgstr "已被删除"
#: assets/templates/assets/asset_list.html:433
#: assets/templates/assets/asset_list.html:441
#: assets/templates/assets/asset_list.html:437
#: assets/templates/assets/asset_list.html:445
msgid "Asset Delete"
msgstr "删除"
#: assets/templates/assets/asset_list.html:440
#: assets/templates/assets/asset_list.html:444
msgid "Asset Deleting failed."
msgstr "删除失败"
......@@ -1902,7 +1929,7 @@ msgid "Configuration"
msgstr "配置"
#: assets/templates/assets/cmd_filter_detail.html:25
#: assets/templates/assets/cmd_filter_list.html:25
#: assets/templates/assets/cmd_filter_list.html:23
#: assets/templates/assets/cmd_filter_rule_list.html:23
msgid "Rules"
msgstr "规则"
......@@ -1911,33 +1938,33 @@ msgstr "规则"
msgid "Binding to system user"
msgstr "绑定到系统用户"
#: assets/templates/assets/cmd_filter_list.html:6
#: assets/templates/assets/cmd_filter_list.html:5
msgid ""
"System user bound some command filter, each command filter has some rules,"
msgstr "系统用户可以绑定一些命令过滤器,一个过滤器可以定义一些规则"
#: assets/templates/assets/cmd_filter_list.html:7
#: assets/templates/assets/cmd_filter_list.html:6
msgid "When user login asset with this system user, then run a command,"
msgstr "当用户使用这个系统用户登录资产,然后执行一个命令"
#: assets/templates/assets/cmd_filter_list.html:8
#: assets/templates/assets/cmd_filter_list.html:7
msgid "The command will be filter by rules, higher priority rule run first,"
msgstr "这个命令需要被绑定过滤器的所有规则匹配,高优先级先被匹配,"
#: assets/templates/assets/cmd_filter_list.html:9
#: assets/templates/assets/cmd_filter_list.html:8
msgid ""
"When a rule matched, if rule action is allow, then allow command execute,"
msgstr "当一个规则匹配到了,如果规则的动作是允许,这个命令会被放行,"
#: assets/templates/assets/cmd_filter_list.html:10
#: assets/templates/assets/cmd_filter_list.html:9
msgid "else if action is deny, then command with be deny,"
msgstr "如果规则的动作是禁止,命令将会被禁止执行,"
#: assets/templates/assets/cmd_filter_list.html:11
#: assets/templates/assets/cmd_filter_list.html:10
msgid "else match next rule, if none matched, allowed"
msgstr "否则就匹配下一个规则,如果最后没有匹配到规则,则允许执行"
#: assets/templates/assets/cmd_filter_list.html:16
#: assets/templates/assets/cmd_filter_list.html:14
#: assets/views/cmd_filter.py:49
msgid "Create command filter"
msgstr "创建命令过滤器"
......@@ -1985,7 +2012,7 @@ msgstr "测试连接"
msgid "Can be connected"
msgstr "可连接"
#: assets/templates/assets/domain_list.html:9
#: assets/templates/assets/domain_list.html:6
msgid ""
"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 "
......@@ -1994,11 +2021,11 @@ msgstr ""
"网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过"
"网关服务器进行跳转登录。"
#: assets/templates/assets/domain_list.html:11
#: assets/templates/assets/domain_list.html:8
msgid "JMS => Domain gateway => Target assets"
msgstr "JMS => 网域网关 => 目标资产"
#: assets/templates/assets/domain_list.html:17 assets/views/domain.py:48
#: assets/templates/assets/domain_list.html:13 assets/views/domain.py:48
msgid "Create domain"
msgstr "创建网域"
......@@ -2036,7 +2063,7 @@ msgstr "Uid"
msgid "Binding command filters"
msgstr "绑定命令过滤器"
#: assets/templates/assets/system_user_list.html:6
#: assets/templates/assets/system_user_list.html:5
msgid ""
"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 "
......@@ -2047,7 +2074,7 @@ msgstr ""
"web,sa,dba(`ssh web@some-host`),而不是使用某个用户的用户名跳转登录服务器"
"(`ssh xiaoming@some-host`);"
#: assets/templates/assets/system_user_list.html:7
#: assets/templates/assets/system_user_list.html:6
msgid ""
"In simple terms, users log into Jumpserver using their own username, and "
"Jumpserver uses system users to log into assets. "
......@@ -2055,7 +2082,7 @@ msgstr ""
"简单来说是用户使用自己的用户名登录 Jumpserver,Jumpserver 使用系统用户登录资"
"产。"
#: assets/templates/assets/system_user_list.html:8
#: assets/templates/assets/system_user_list.html:7
msgid ""
"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 "
......@@ -2064,25 +2091,25 @@ msgstr ""
"系统用户创建时,如果选择了自动推送,Jumpserver 会使用 Ansible 自动推送系统用"
"户到资产中,如果资产(交换机)不支持 Ansible,请手动填写账号密码。"
#: assets/templates/assets/system_user_list.html:39
#: assets/templates/assets/system_user_list.html:37
#: assets/views/system_user.py:47
msgid "Create system user"
msgstr "创建系统用户"
#: assets/templates/assets/system_user_list.html:130
#: assets/templates/assets/system_user_list.html:128
msgid "This will delete the selected System Users !!!"
msgstr "删除选择系统用户"
#: assets/templates/assets/system_user_list.html:139
#: assets/templates/assets/system_user_list.html:137
msgid "System Users Deleted."
msgstr "已被删除"
#: assets/templates/assets/system_user_list.html:140
#: assets/templates/assets/system_user_list.html:145
#: assets/templates/assets/system_user_list.html:138
#: assets/templates/assets/system_user_list.html:143
msgid "System Users Delete"
msgstr "删除系统用户"
#: assets/templates/assets/system_user_list.html:144
#: assets/templates/assets/system_user_list.html:142
msgid "System Users Deleting failed."
msgstr "系统用户删除失败"
......@@ -2201,7 +2228,7 @@ msgstr "文件名"
#: audits/templates/audits/ftp_log_list.html:79
#: ops/templates/ops/command_execution_list.html:68
#: ops/templates/ops/task_list.html:15
#: users/templates/users/user_detail.html:464
#: users/templates/users/user_detail.html:518
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:14
#: xpack/plugins/cloud/api.py:61
msgid "Success"
......@@ -2209,7 +2236,7 @@ msgstr "成功"
#: audits/models.py:33
#: authentication/templates/authentication/_access_key_modal.html:22
#: xpack/plugins/vault/templates/vault/vault.html:46
#: xpack/plugins/vault/templates/vault/vault.html:38
msgid "Create"
msgstr "创建"
......@@ -2262,13 +2289,13 @@ msgstr "Agent"
#: audits/models.py:86 audits/templates/audits/login_log_list.html:62
#: authentication/templates/authentication/_mfa_confirm_modal.html:14
#: users/forms.py:194 users/models/user.py:395
#: users/forms.py:194 users/models/user.py:451
#: users/templates/users/first_login.html:45
msgid "MFA"
msgstr "MFA"
#: audits/models.py:87 audits/templates/audits/login_log_list.html:63
#: xpack/plugins/change_auth_plan/models.py:416
#: xpack/plugins/change_auth_plan/models.py:423
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15
#: xpack/plugins/cloud/models.py:278
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:69
......@@ -2276,6 +2303,9 @@ msgid "Reason"
msgstr "原因"
#: audits/models.py:88 audits/templates/audits/login_log_list.html:64
#: tickets/templates/tickets/ticket_detail.html:34
#: tickets/templates/tickets/ticket_list.html:36
#: tickets/templates/tickets/ticket_list.html:101
#: xpack/plugins/cloud/models.py:275 xpack/plugins/cloud/models.py:310
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:65
......@@ -2294,11 +2324,11 @@ msgstr "登录日期"
#: perms/templates/perms/asset_permission_detail.html:86
#: perms/templates/perms/remote_app_permission_detail.html:78
#: terminal/models.py:167 terminal/templates/terminal/session_list.html:34
#: xpack/plugins/change_auth_plan/models.py:249
#: xpack/plugins/change_auth_plan/models.py:419
#: xpack/plugins/change_auth_plan/models.py:250
#: xpack/plugins/change_auth_plan/models.py:426
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:59
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:17
#: xpack/plugins/gathered_user/models.py:143
#: xpack/plugins/gathered_user/models.py:140
msgid "Date start"
msgstr "开始日期"
......@@ -2314,7 +2344,7 @@ msgstr "选择用户"
#: audits/templates/audits/password_change_log_list.html:48
#: ops/templates/ops/command_execution_list.html:46
#: ops/templates/ops/command_execution_list.html:51
#: templates/_base_list.html:41
#: templates/_base_list.html:41 templates/_user_profile.html:23
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:52
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:48
msgid "Search"
......@@ -2335,7 +2365,7 @@ msgstr "ID"
msgid "UA"
msgstr "Agent"
#: audits/templates/audits/login_log_list.html:61
#: audits/templates/audits/login_log_list.html:61 authentication/models.py:63
msgid "City"
msgstr "城市"
......@@ -2346,23 +2376,23 @@ msgid "Date"
msgstr "日期"
#: audits/views.py:86 audits/views.py:130 audits/views.py:167
#: audits/views.py:212 audits/views.py:244 templates/_nav.html:129
#: audits/views.py:212 audits/views.py:244 templates/_nav.html:137
msgid "Audits"
msgstr "日志审计"
#: audits/views.py:87 templates/_nav.html:133
#: audits/views.py:87 templates/_nav.html:141
msgid "FTP log"
msgstr "FTP日志"
#: audits/views.py:131 templates/_nav.html:134
#: audits/views.py:131 templates/_nav.html:142
msgid "Operate log"
msgstr "操作日志"
#: audits/views.py:168 templates/_nav.html:135
#: audits/views.py:168 templates/_nav.html:143
msgid "Password change log"
msgstr "改密日志"
#: audits/views.py:213 templates/_nav.html:132
#: audits/views.py:213 templates/_nav.html:140
msgid "Login log"
msgstr "登录日志"
......@@ -2370,28 +2400,6 @@ msgstr "登录日志"
msgid "Command execution log"
msgstr "命令执行"
#: authentication/api/auth.py:61 authentication/api/token.py:45
#: authentication/templates/authentication/login.html:52
#: authentication/templates/authentication/new_login.html:77
msgid "Log in frequently and try again later"
msgstr "登录频繁, 稍后重试"
#: authentication/api/auth.py:86
msgid "Please carry seed value and conduct MFA secondary certification"
msgstr "请携带seed值, 进行MFA二次认证"
#: authentication/api/auth.py:176
msgid "Please verify the user name and password first"
msgstr "请先进行用户名和密码验证"
#: authentication/api/auth.py:181
msgid "MFA certification failed"
msgstr "MFA认证失败"
#: authentication/api/token.py:80
msgid "MFA required"
msgstr ""
#: authentication/backends/api.py:53
msgid "Invalid signature header. No credentials provided."
msgstr ""
......@@ -2443,56 +2451,87 @@ msgstr ""
msgid "Invalid token or cache refreshed."
msgstr ""
#: authentication/const.py:6
#: authentication/errors.py:20
msgid "Username/password check failed"
msgstr "用户名/密码 校验失败"
#: authentication/const.py:7
#: authentication/errors.py:21
msgid "MFA authentication failed"
msgstr "MFA 认证失败"
#: authentication/const.py:8
#: authentication/errors.py:22
msgid "Username does not exist"
msgstr "用户名不存在"
#: authentication/const.py:9
#: authentication/errors.py:23
msgid "Password expired"
msgstr "密码过期"
msgstr "密码过期"
#: authentication/const.py:10
#: authentication/errors.py:24
msgid "Disabled or expired"
msgstr "禁用或失效"
#: authentication/forms.py:21
msgid ""
"The username or password you entered is incorrect, please enter it again."
msgstr "您输入的用户名或密码不正确,请重新输入。"
#: authentication/forms.py:24
#: authentication/errors.py:25
msgid "This account is inactive."
msgstr "此账户无效"
msgstr "此账户已禁用"
#: authentication/errors.py:35
msgid "No session found, check your cookie"
msgstr "会话已变更,刷新页面"
#: authentication/forms.py:26
#: authentication/errors.py:37
#, python-brace-format
msgid ""
"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)"
msgstr "您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)"
msgstr ""
"您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将"
"被临时 锁定 {block_time} 分钟)"
#: authentication/forms.py:30
#: authentication/errors.py:43
msgid ""
"The account has been locked (please contact admin to unlock it or try again "
"after {} minutes)"
msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
#: authentication/forms.py:66 users/forms.py:22
#: authentication/errors.py:46 users/views/user.py:398 users/views/user.py:423
msgid "MFA code invalid, or ntp sync server time"
msgstr "MFA验证码不正确,或者服务器端时间不对"
#: authentication/errors.py:48
msgid "MFA required"
msgstr ""
#: authentication/errors.py:49
msgid "Login confirm required"
msgstr "需要登录复核"
#: authentication/errors.py:50
msgid "Wait login confirm ticket for accept"
msgstr "等待登录复核处理"
#: authentication/errors.py:51
msgid "Login confirm ticket was {}"
msgstr "登录复核 {}"
#: authentication/forms.py:32 users/forms.py:22
msgid "MFA code"
msgstr "MFA 验证码"
#: authentication/models.py:35
#: authentication/models.py:39
msgid "Private Token"
msgstr "ssh密钥"
#: authentication/models.py:44 users/templates/users/user_detail.html:266
msgid "Reviewers"
msgstr "审批人"
#: authentication/models.py:53 tickets/models/ticket.py:25
#: users/templates/users/user_detail.html:258
msgid "Login confirm"
msgstr "登录复核"
#: authentication/templates/authentication/_access_key_modal.html:6
msgid "API key list"
msgstr "API Key列表"
......@@ -2515,14 +2554,14 @@ msgid "Show"
msgstr "显示"
#: authentication/templates/authentication/_access_key_modal.html:66
#: users/models/user.py:330 users/templates/users/user_profile.html:94
#: users/models/user.py:351 users/templates/users/user_profile.html:94
#: users/templates/users/user_profile.html:163
#: users/templates/users/user_profile.html:166
msgid "Disable"
msgstr "禁用"
#: authentication/templates/authentication/_access_key_modal.html:67
#: users/models/user.py:331 users/templates/users/user_profile.html:92
#: users/models/user.py:352 users/templates/users/user_profile.html:92
#: users/templates/users/user_profile.html:170
msgid "Enable"
msgstr "启用"
......@@ -2582,35 +2621,30 @@ msgstr ""
msgid "Changes the world, starting with a little bit."
msgstr "改变世界,从一点点开始。"
#: authentication/templates/authentication/login.html:46
#: authentication/templates/authentication/login.html:73
#: authentication/templates/authentication/new_login.html:101
#: authentication/templates/authentication/login.html:45
#: authentication/templates/authentication/login.html:76
#: authentication/templates/authentication/xpack_login.html:112
#: templates/_header_bar.html:83
msgid "Login"
msgstr "登录"
#: authentication/templates/authentication/login.html:54
#: authentication/templates/authentication/new_login.html:80
msgid "The user password has expired"
msgstr "用户密码已过期"
#: authentication/templates/authentication/login.html:57
#: authentication/templates/authentication/new_login.html:83
#: authentication/templates/authentication/xpack_login.html:87
msgid "Captcha invalid"
msgstr "验证码错误"
#: authentication/templates/authentication/login.html:84
#: authentication/templates/authentication/new_login.html:105
#: authentication/templates/authentication/login.html:87
#: authentication/templates/authentication/xpack_login.html:116
#: users/templates/users/forgot_password.html:10
#: users/templates/users/forgot_password.html:25
msgid "Forgot password"
msgstr "忘记密码"
#: authentication/templates/authentication/login.html:91
#: authentication/templates/authentication/login.html:94
msgid "More login options"
msgstr "更多登录方式"
#: authentication/templates/authentication/login.html:95
#: authentication/templates/authentication/login.html:98
msgid "Keycloak"
msgstr ""
......@@ -2651,24 +2685,40 @@ msgstr "下一步"
msgid "Can't provide security? Please contact the administrator!"
msgstr "如果不能提供MFA验证码,请联系管理员!"
#: authentication/templates/authentication/new_login.html:67
#: authentication/templates/authentication/login_wait_confirm.html:47
msgid "Copy link"
msgstr "复制链接"
#: authentication/templates/authentication/login_wait_confirm.html:52
#: templates/flash_message_standalone.html:47
msgid "Return"
msgstr "返回"
#: authentication/templates/authentication/xpack_login.html:74
msgid "Welcome back, please enter username and password to login"
msgstr "欢迎回来,请输入用户名和密码登录"
#: authentication/views/login.py:81
#: authentication/views/login.py:70
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
#: authentication/views/login.py:174 users/views/user.py:393
#: users/views/user.py:418
msgid "MFA code invalid, or ntp sync server time"
msgstr "MFA验证码不正确,或者服务器端时间不对"
#: authentication/views/login.py:157
msgid ""
"Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
" Don't close this page"
msgstr ""
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n"
" 不要关闭本页面"
#: authentication/views/login.py:205
#: authentication/views/login.py:162
msgid "No ticket found"
msgstr "没有发现工单"
#: authentication/views/login.py:185
msgid "Logout success"
msgstr "退出登录成功"
#: authentication/views/login.py:206
#: authentication/views/login.py:186
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
......@@ -2753,7 +2803,17 @@ msgstr ""
#: jumpserver/views.py:233
msgid "Websocket server run on port: {}, you should proxy it on nginx"
msgstr "Websocket 服务运行在端口: {}, 请检查nginx是否代理是否设置"
#: jumpserver/views.py:241
msgid ""
"<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>"
msgstr ""
"<div>Koko是单独部署的一个程序,你需要部署Koko, 并确保nginx配置转发, </"
"div><div>如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运</"
"div>"
#: ops/api/celery.py:54
msgid "Waiting task start"
......@@ -2761,7 +2821,7 @@ msgstr "等待任务开始"
#: ops/api/command.py:35
msgid "Not has host {} permission"
msgstr ""
msgstr "没有该主机 {} 权限"
#: ops/models/adhoc.py:38
msgid "Interval"
......@@ -2824,67 +2884,67 @@ msgstr "Become"
msgid "Create by"
msgstr "创建者"
#: ops/models/adhoc.py:252
#: ops/models/adhoc.py:254
msgid "{} Start task: {}"
msgstr "{} 任务开始: {}"
#: ops/models/adhoc.py:264
#: ops/models/adhoc.py:263
msgid "{} Task finish"
msgstr "{} 任务结束"
#: ops/models/adhoc.py:356
#: ops/models/adhoc.py:360
msgid "Start time"
msgstr "开始时间"
#: ops/models/adhoc.py:357
#: ops/models/adhoc.py:361
msgid "End time"
msgstr "完成时间"
#: ops/models/adhoc.py:358 ops/templates/ops/adhoc_history.html:57
#: ops/models/adhoc.py:362 ops/templates/ops/adhoc_history.html:57
#: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:17
#: xpack/plugins/change_auth_plan/models.py:252
#: xpack/plugins/change_auth_plan/models.py:422
#: xpack/plugins/change_auth_plan/models.py:253
#: xpack/plugins/change_auth_plan/models.py:429
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:58
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:16
#: xpack/plugins/gathered_user/models.py:146
#: xpack/plugins/gathered_user/models.py:143
msgid "Time"
msgstr "时间"
#: ops/models/adhoc.py:359 ops/templates/ops/adhoc_detail.html:106
#: ops/models/adhoc.py:363 ops/templates/ops/adhoc_detail.html:106
#: ops/templates/ops/adhoc_history.html:55
#: ops/templates/ops/adhoc_history_detail.html:69
#: ops/templates/ops/task_detail.html:84 ops/templates/ops/task_history.html:61
msgid "Is finished"
msgstr "是否完成"
#: ops/models/adhoc.py:360 ops/templates/ops/adhoc_history.html:56
#: ops/models/adhoc.py:364 ops/templates/ops/adhoc_history.html:56
#: ops/templates/ops/task_history.html:62
msgid "Is success"
msgstr "是否成功"
#: ops/models/adhoc.py:361
#: ops/models/adhoc.py:365
msgid "Adhoc raw result"
msgstr "结果"
#: ops/models/adhoc.py:362
#: ops/models/adhoc.py:366
msgid "Adhoc result summary"
msgstr "汇总"
#: ops/models/command.py:22
#: ops/models/command.py:23
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:56
#: xpack/plugins/cloud/models.py:273
msgid "Result"
msgstr "结果"
#: ops/models/command.py:57
#: ops/models/command.py:58
msgid "Task start"
msgstr "任务开始"
#: ops/models/command.py:71
#: ops/models/command.py:75
msgid "Command `{}` is forbidden ........"
msgstr "命令 `{}` 不允许被执行 ......."
#: ops/models/command.py:77
#: ops/models/command.py:81
msgid "Task end"
msgstr "任务结束"
......@@ -3110,7 +3170,7 @@ msgstr "命令执行列表"
msgid "Command execution"
msgstr "命令执行"
#: orgs/mixins/models.py:58 orgs/mixins/serializers.py:26 orgs/models.py:31
#: orgs/mixins/models.py:44 orgs/mixins/serializers.py:26 orgs/models.py:31
msgid "Organization"
msgstr "组织"
......@@ -3130,12 +3190,12 @@ msgstr "提示:RDP 协议不支持单独控制上传或下载文件"
#: perms/forms/asset_permission.py:81 perms/forms/remote_app_permission.py:37
#: perms/models/base.py:50 perms/templates/perms/asset_permission_list.html:51
#: perms/templates/perms/asset_permission_list.html:71
#: perms/templates/perms/asset_permission_list.html:118
#: perms/templates/perms/asset_permission_list.html:109
#: perms/templates/perms/asset_permission_list.html:211
#: perms/templates/perms/remote_app_permission_list.html:16
#: templates/_nav.html:21 users/forms.py:313 users/models/group.py:26
#: users/models/user.py:379 users/templates/users/_select_user_modal.html:16
#: users/templates/users/user_detail.html:218
#: users/models/user.py:435 users/templates/users/_select_user_modal.html:16
#: users/templates/users/user_detail.html:219
#: users/templates/users/user_list.html:38
#: xpack/plugins/orgs/templates/orgs/org_list.html:16
msgid "User group"
......@@ -3170,13 +3230,14 @@ msgid "Actions"
msgstr "动作"
#: perms/models/asset_permission.py:87 templates/_nav.html:72
#: tickets/templates/tickets/ticket_list.html:22
msgid "Asset permission"
msgstr "资产授权"
#: perms/models/base.py:53
#: perms/templates/perms/asset_permission_detail.html:90
#: perms/templates/perms/remote_app_permission_detail.html:82
#: users/models/user.py:411 users/templates/users/user_detail.html:107
#: users/models/user.py:467 users/templates/users/user_detail.html:107
#: users/templates/users/user_profile.html:120
msgid "Date expired"
msgstr "失效日期"
......@@ -3229,7 +3290,7 @@ msgid "Add node to this permission"
msgstr "添加节点"
#: perms/templates/perms/asset_permission_asset.html:109
#: users/templates/users/user_detail.html:235
#: users/templates/users/user_detail.html:236
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:105
msgid "Join"
msgstr "加入"
......@@ -3278,7 +3339,7 @@ msgid "Refresh permission cache"
msgstr "刷新授权缓存"
#: perms/templates/perms/asset_permission_list.html:55
#: perms/templates/perms/asset_permission_list.html:69
#: perms/templates/perms/asset_permission_list.html:209
#: perms/templates/perms/remote_app_permission_list.html:19
#: users/templates/users/user_list.html:40 xpack/plugins/cloud/models.py:74
#: xpack/plugins/cloud/templates/cloud/account_detail.html:58
......@@ -3286,7 +3347,7 @@ msgstr "刷新授权缓存"
msgid "Validity"
msgstr "有效"
#: perms/templates/perms/asset_permission_list.html:244
#: perms/templates/perms/asset_permission_list.html:232
msgid "Refresh success"
msgstr "刷新成功"
......@@ -3403,31 +3464,31 @@ msgstr "连接LDAP成功"
#: settings/api.py:107
msgid "LDAP attr map not valid"
msgstr "LDAP 属性映射无效"
msgstr ""
#: settings/api.py:116
msgid "Match {} s users"
msgstr "匹配 {} 个用户"
#: settings/api.py:224
#: settings/api.py:226
msgid "Get ldap users is None"
msgstr "获取 LDAP 用户为 None"
#: settings/api.py:231
#: settings/api.py:233
msgid "Imported {} users successfully"
msgstr "导入 {} 个用户成功"
#: settings/api.py:262 settings/api.py:298
#: settings/api.py:264 settings/api.py:300
msgid ""
"Error: Account invalid (Please make sure the information such as Access key "
"or Secret key is correct)"
msgstr "错误:账户无效 (请确保 Access key 或 Secret key 等信息正确)"
#: settings/api.py:268 settings/api.py:304
#: settings/api.py:270 settings/api.py:306
msgid "Create succeed"
msgstr "创建成功"
#: settings/api.py:286 settings/api.py:324
#: settings/api.py:288 settings/api.py:326
#: settings/templates/settings/terminal_setting.html:154
msgid "Delete succeed"
msgstr "删除成功"
......@@ -3635,12 +3696,11 @@ msgstr ""
#: settings/forms.py:220
msgid "Connection max idle time"
msgstr "SSH最大空闲时间"
msgstr "连接最大空闲时间"
#: settings/forms.py:222
msgid ""
"If idle time more than it, disconnect connection(only ssh now) Unit: minute"
msgstr "提示:(单位:分)如果超过该配置没有操作,连接会被断开(仅ssh)"
msgid "If idle time more than it, disconnect connection Unit: minute"
msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)"
#: settings/forms.py:228
msgid "Password expiration time"
......@@ -3752,7 +3812,7 @@ msgid "Refresh cache"
msgstr "刷新缓存"
#: settings/templates/settings/_ldap_list_users_modal.html:33
#: users/models/user.py:375 users/templates/users/user_detail.html:71
#: users/models/user.py:431 users/templates/users/user_detail.html:71
#: users/templates/users/user_profile.html:59
msgid "Email"
msgstr "邮件"
......@@ -3952,7 +4012,7 @@ msgstr "在ou:{}中没有匹配条目"
#: settings/views.py:20 settings/views.py:47 settings/views.py:74
#: settings/views.py:105 settings/views.py:133 settings/views.py:146
#: settings/views.py:160 settings/views.py:187 templates/_nav.html:170
#: settings/views.py:160 settings/views.py:187 templates/_nav.html:178
msgid "Settings"
msgstr "系统设置"
......@@ -4145,7 +4205,7 @@ msgstr "终端管理"
msgid "Job Center"
msgstr "作业中心"
#: templates/_nav.html:116 templates/_nav.html:136
#: templates/_nav.html:116 templates/_nav.html:144
msgid "Batch command"
msgstr "批量命令"
......@@ -4153,15 +4213,19 @@ msgstr "批量命令"
msgid "Task monitor"
msgstr "任务监控"
#: templates/_nav.html:146
#: templates/_nav.html:128 tickets/views.py:19 tickets/views.py:37
msgid "Tickets"
msgstr "工单管理"
#: templates/_nav.html:154
msgid "XPack"
msgstr ""
#: templates/_nav.html:154 xpack/plugins/cloud/views.py:28
#: templates/_nav.html:162 xpack/plugins/cloud/views.py:28
msgid "Account list"
msgstr "账户列表"
#: templates/_nav.html:155
#: templates/_nav.html:163
msgid "Sync instance"
msgstr "同步实例"
......@@ -4190,10 +4254,6 @@ msgstr "语言播放验证码"
msgid "Captcha"
msgstr "验证码"
#: templates/flash_message_standalone.html:47
msgid "Return"
msgstr "返回"
#: templates/index.html:11
msgid "Total users"
msgstr "用户总数"
......@@ -4507,6 +4567,8 @@ msgid "Accept"
msgstr "接受"
#: terminal/templates/terminal/terminal_list.html:80
#: tickets/models/ticket.py:31 tickets/templates/tickets/ticket_detail.html:101
#: tickets/templates/tickets/ticket_list.html:107
msgid "Reject"
msgstr "拒绝"
......@@ -4543,11 +4605,159 @@ msgid ""
"You should use your ssh client tools connect terminal: {} <br /> <br />{}"
msgstr "你可以使用ssh客户端工具连接终端"
#: users/api/user.py:173
#: tickets/models/ticket.py:18 tickets/models/ticket.py:70
#: tickets/templates/tickets/ticket_list.html:102
msgid "Open"
msgstr "开启"
#: tickets/models/ticket.py:19 tickets/templates/tickets/ticket_list.html:103
msgid "Closed"
msgstr "关闭"
#: tickets/models/ticket.py:24
msgid "General"
msgstr "一般"
#: tickets/models/ticket.py:30 tickets/templates/tickets/ticket_detail.html:100
#: tickets/templates/tickets/ticket_list.html:106
msgid "Approve"
msgstr "同意"
#: tickets/models/ticket.py:34 tickets/models/ticket.py:129
msgid "User display name"
msgstr "用户显示名称"
#: tickets/models/ticket.py:36 tickets/templates/tickets/ticket_list.html:33
#: tickets/templates/tickets/ticket_list.html:99
msgid "Title"
msgstr "标题"
#: tickets/models/ticket.py:37 tickets/models/ticket.py:130
msgid "Body"
msgstr "内容"
#: tickets/models/ticket.py:38
msgid "Meta"
msgstr ""
#: tickets/models/ticket.py:39 tickets/templates/tickets/ticket_detail.html:51
msgid "Assignee"
msgstr "处理人"
#: tickets/models/ticket.py:40
msgid "Assignee display name"
msgstr "处理人名称"
#: tickets/models/ticket.py:41 tickets/templates/tickets/ticket_detail.html:50
msgid "Assignees"
msgstr "待处理人"
#: tickets/models/ticket.py:42
msgid "Assignees display name"
msgstr "待处理人名称"
#: tickets/models/ticket.py:71
msgid "{} {} this ticket"
msgstr "{} {} 这个工单"
#: tickets/models/ticket.py:82
msgid "this ticket"
msgstr "这个工单"
#: tickets/templates/tickets/ticket_detail.html:66
#: tickets/templates/tickets/ticket_detail.html:80
msgid "ago"
msgstr "前"
#: tickets/templates/tickets/ticket_list.html:9
msgid "My tickets"
msgstr "我的工单"
#: tickets/templates/tickets/ticket_list.html:10
msgid "Assigned me"
msgstr "待处理"
#: tickets/templates/tickets/ticket_list.html:19
msgid "Create ticket"
msgstr "提交工单"
#: tickets/utils.py:18
msgid "New ticket"
msgstr "新工单"
#: tickets/utils.py:21
#, python-brace-format
msgid ""
"\n"
" <div>\n"
" <p>Your has a new ticket</p>\n"
" <div>\n"
" {body}\n"
" <br/>\n"
" <a href={url}>click here to review</a> \n"
" </div>\n"
" </div>\n"
" "
msgstr ""
"\n"
" <div>\n"
" <p>你有一个新工单</p>\n"
" <div>\n"
" {body}\n"
" <br/>\n"
" <a href={url}>点击我查看</a> \n"
" </div>\n"
" </div>\n"
" "
#: tickets/utils.py:40
msgid "Ticket has been reply"
msgstr "工单已被回复"
#: tickets/utils.py:41
#, python-brace-format
msgid ""
"\n"
" <div>\n"
" <p>Your ticket has been replay</p>\n"
" <div>\n"
" <b>Title:</b> {ticket.title}\n"
" <br/>\n"
" <b>Assignee:</b> {ticket.assignee_display}\n"
" <br/>\n"
" <b>Status:</b> {ticket.status_display}\n"
" <br/>\n"
" </div>\n"
" </div>\n"
" "
msgstr ""
"\n"
" <div>\n"
" <p>您的工单已被回复</p>\n"
" <div>\n"
" <b>标题:</b> {ticket.title}\n"
" <br/>\n"
" <b>处理人:</b> {ticket.assignee_display}\n"
" <br/>\n"
" <b>状态:</b> {ticket.status_display}\n"
" <br/>\n"
" </div>\n"
" </div>\n"
" "
#: tickets/views.py:20
msgid "Ticket list"
msgstr "工单列表"
#: tickets/views.py:38
msgid "Ticket detail"
msgstr "工单详情"
#: users/api/user.py:174
msgid "Could not reset self otp, use profile reset instead"
msgstr "不能再该页面重置MFA, 请去个人信息页面重置"
#: users/forms.py:47 users/models/user.py:383
#: users/forms.py:47 users/models/user.py:439
#: users/templates/users/_select_user_modal.html:15
#: users/templates/users/user_detail.html:87
#: users/templates/users/user_list.html:37
......@@ -4555,7 +4765,7 @@ msgstr "不能再该页面重置MFA, 请去个人信息页面重置"
msgid "Role"
msgstr "角色"
#: users/forms.py:51 users/models/user.py:418
#: users/forms.py:51 users/models/user.py:474
#: users/templates/users/user_detail.html:103
#: users/templates/users/user_list.html:39
#: users/templates/users/user_profile.html:102
......@@ -4575,7 +4785,7 @@ msgstr ""
msgid "Paste user id_rsa.pub here."
msgstr "复制用户公钥到这里"
#: users/forms.py:71 users/templates/users/user_detail.html:226
#: users/forms.py:71 users/templates/users/user_detail.html:227
msgid "Join user groups"
msgstr "添加到用户组"
......@@ -4583,7 +4793,7 @@ msgstr "添加到用户组"
msgid "Public key should not be the same as your old one."
msgstr "不能和原来的密钥相同"
#: users/forms.py:110 users/forms.py:271 users/serializers/user.py:110
#: users/forms.py:110 users/forms.py:271 users/serializers/user.py:122
msgid "Not a valid ssh public key"
msgstr "ssh密钥不合法"
......@@ -4599,8 +4809,8 @@ msgstr "生成重置密码链接,通过邮件发送给用户"
msgid "Set password"
msgstr "设置密码"
#: users/forms.py:152 xpack/plugins/change_auth_plan/models.py:88
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:51
#: users/forms.py:152 xpack/plugins/change_auth_plan/models.py:89
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:49
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:69
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:57
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:16
......@@ -4668,49 +4878,53 @@ msgstr "复制你的公钥到这里"
msgid "Select users"
msgstr "选择用户"
#: users/models/user.py:50 users/templates/users/user_update.html:22
#: users/models/user.py:51 users/templates/users/user_update.html:22
#: users/views/login.py:46 users/views/login.py:107
msgid "User auth from {}, go there change password"
msgstr "用户认证源来自 {}, 请去相应系统修改密码"
#: users/models/user.py:126 users/models/user.py:508
#: users/models/user.py:147 users/models/user.py:564
msgid "Administrator"
msgstr "管理员"
#: users/models/user.py:128
#: users/models/user.py:149
msgid "Application"
msgstr "应用程序"
#: users/models/user.py:129 xpack/plugins/orgs/forms.py:30
#: users/models/user.py:150 xpack/plugins/orgs/forms.py:30
#: xpack/plugins/orgs/templates/orgs/org_list.html:14
msgid "Auditor"
msgstr "审计员"
#: users/models/user.py:139
#: users/models/user.py:160
msgid "Org admin"
msgstr "组织管理员"
#: users/models/user.py:141
#: users/models/user.py:162
msgid "Org auditor"
msgstr "组织审计员"
#: users/models/user.py:332 users/templates/users/user_profile.html:90
#: users/models/user.py:353 users/templates/users/user_profile.html:90
msgid "Force enable"
msgstr "强制启用"
#: users/models/user.py:386
#: users/models/user.py:419
msgid "Local"
msgstr "数据库"
#: users/models/user.py:442
msgid "Avatar"
msgstr "头像"
#: users/models/user.py:389 users/templates/users/user_detail.html:82
#: users/models/user.py:445 users/templates/users/user_detail.html:82
msgid "Wechat"
msgstr "微信"
#: users/models/user.py:422
#: users/models/user.py:478
msgid "Date password last updated"
msgstr "最后更新密码日期"
#: users/models/user.py:511
#: users/models/user.py:567
msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员"
......@@ -4718,42 +4932,42 @@ msgstr "Administrator是初始的超级管理员"
msgid "Auditors cannot be join in the user group"
msgstr "审计员不能被加入到用户组"
#: users/serializers/user.py:40
msgid "Groups name"
msgstr "用户组名"
#: users/serializers/user.py:41
msgid "Source name"
msgstr "用户来源名"
#: users/serializers/user.py:42
#: users/serializers/user.py:35
msgid "Is first login"
msgstr "首次登录"
#: users/serializers/user.py:43
msgid "Role name"
msgstr "角色名"
#: users/serializers/user.py:44
#: users/serializers/user.py:36
msgid "Is valid"
msgstr "账户是否有效"
#: users/serializers/user.py:45
#: users/serializers/user.py:37
msgid "Is expired"
msgstr " 是否过期"
#: users/serializers/user.py:46
#: users/serializers/user.py:38
msgid "Avatar url"
msgstr "头像路径"
#: users/serializers/user.py:66
#: users/serializers/user.py:46
msgid "Role limit to {}"
msgstr "角色只能为 {}"
#: users/serializers/user.py:78
#: users/serializers/user.py:58
msgid "Password does not match security rules"
msgstr "密码不满足安全规则"
#: users/serializers/user.py:107
msgid "Groups name"
msgstr "用户组名"
#: users/serializers/user.py:108
msgid "Source name"
msgstr "用户来源名"
#: users/serializers/user.py:109
msgid "Role name"
msgstr "角色名"
#: users/serializers_v2/user.py:36
msgid "name not unique"
msgstr "名称重复"
......@@ -4882,7 +5096,7 @@ msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry"
msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry"
#: users/templates/users/reset_password.html:46
#: users/templates/users/user_detail.html:379 users/utils.py:83
#: users/templates/users/user_detail.html:433 users/utils.py:83
msgid "Reset password"
msgstr "重置密码"
......@@ -4981,72 +5195,72 @@ msgstr "最后更新密码"
msgid "Force enabled MFA"
msgstr "强制启用MFA"
#: users/templates/users/user_detail.html:175
#: users/templates/users/user_detail.html:177
msgid "Reset MFA"
msgstr "重置MFA"
#: users/templates/users/user_detail.html:184
#: users/templates/users/user_detail.html:186
msgid "Send reset password mail"
msgstr "发送重置密码邮件"
#: users/templates/users/user_detail.html:187
#: users/templates/users/user_detail.html:197
#: users/templates/users/user_detail.html:189
#: users/templates/users/user_detail.html:199
msgid "Send"
msgstr "发送"
#: users/templates/users/user_detail.html:194
#: users/templates/users/user_detail.html:196
msgid "Send reset ssh key mail"
msgstr "发送重置密钥邮件"
#: users/templates/users/user_detail.html:203
#: users/templates/users/user_detail.html:467
#: users/templates/users/user_detail.html:205
#: users/templates/users/user_detail.html:521
msgid "Unblock user"
msgstr "解除登录限制"
#: users/templates/users/user_detail.html:206
#: users/templates/users/user_detail.html:208
msgid "Unblock"
msgstr "解除"
#: users/templates/users/user_detail.html:322
#: users/templates/users/user_detail.html:376
msgid "Goto profile page enable MFA"
msgstr "请去个人信息页面启用自己的MFA"
#: users/templates/users/user_detail.html:378
#: users/templates/users/user_detail.html:432
msgid "An e-mail has been sent to the user`s mailbox."
msgstr "已发送邮件到用户邮箱"
#: users/templates/users/user_detail.html:389
#: users/templates/users/user_detail.html:443
msgid "This will reset the user password and send a reset mail"
msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱"
#: users/templates/users/user_detail.html:404
#: users/templates/users/user_detail.html:458
msgid ""
"The reset-ssh-public-key E-mail has been sent successfully. Please inform "
"the user to update his new ssh public key."
msgstr "重设密钥邮件将会发送到用户邮箱"
#: users/templates/users/user_detail.html:405
#: users/templates/users/user_detail.html:459
msgid "Reset SSH public key"
msgstr "重置SSH密钥"
#: users/templates/users/user_detail.html:415
#: users/templates/users/user_detail.html:469
msgid "This will reset the user public key and send a reset mail"
msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱"
#: users/templates/users/user_detail.html:433
#: users/templates/users/user_detail.html:487
msgid "Successfully updated the SSH public key."
msgstr "更新ssh密钥成功"
#: users/templates/users/user_detail.html:434
#: users/templates/users/user_detail.html:438
#: users/templates/users/user_detail.html:488
#: users/templates/users/user_detail.html:492
msgid "User SSH public key update"
msgstr "ssh密钥"
#: users/templates/users/user_detail.html:483
#: users/templates/users/user_detail.html:537
msgid "After unlocking the user, the user can log in normally."
msgstr "解除用户登录限制后,此用户即可正常登录"
#: users/templates/users/user_detail.html:497
#: users/templates/users/user_detail.html:551
msgid "Reset user MFA success"
msgstr "重置用户MFA成功"
......@@ -5161,13 +5375,13 @@ msgid "Set MFA"
msgstr "设置MFA"
#: users/templates/users/user_profile.html:178
msgid "Update password"
msgstr "更改密码"
#: users/templates/users/user_profile.html:188
msgid "Update MFA"
msgstr "更改MFA"
#: users/templates/users/user_profile.html:188
msgid "Update password"
msgstr "更改密码"
#: users/templates/users/user_profile.html:198
msgid "Update SSH public key"
msgstr "更改SSH密钥"
......@@ -5469,19 +5683,19 @@ msgstr "密钥更新"
msgid "Password invalid"
msgstr "用户名或密码无效"
#: users/views/user.py:448
#: users/views/user.py:453
msgid "MFA enable success"
msgstr "MFA 绑定成功"
#: users/views/user.py:449
#: users/views/user.py:454
msgid "MFA enable success, return login page"
msgstr "MFA 绑定成功,返回到登录页面"
#: users/views/user.py:451
#: users/views/user.py:456
msgid "MFA disable success"
msgstr "MFA 解绑成功"
#: users/views/user.py:452
#: users/views/user.py:457
msgid "MFA disable success, return login page"
msgstr "MFA 解绑成功,返回登录页面"
......@@ -5490,7 +5704,7 @@ msgid "Password length"
msgstr "密码长度"
#: xpack/plugins/change_auth_plan/forms.py:75
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:60
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:58
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:81
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:17
#: xpack/plugins/cloud/forms.py:33 xpack/plugins/cloud/forms.py:87
......@@ -5529,8 +5743,8 @@ msgstr ""
"具</a>) <br>注意: 如果同时设置了定期执行和周期执行,优先使用定期执行"
#: xpack/plugins/change_auth_plan/meta.py:9
#: xpack/plugins/change_auth_plan/models.py:116
#: xpack/plugins/change_auth_plan/models.py:256
#: xpack/plugins/change_auth_plan/models.py:117
#: xpack/plugins/change_auth_plan/models.py:257
#: xpack/plugins/change_auth_plan/views.py:33
#: xpack/plugins/change_auth_plan/views.py:50
#: xpack/plugins/change_auth_plan/views.py:74
......@@ -5541,20 +5755,20 @@ msgstr ""
msgid "Change auth plan"
msgstr "改密计划"
#: xpack/plugins/change_auth_plan/models.py:57
#: xpack/plugins/change_auth_plan/models.py:58
msgid "Custom password"
msgstr "自定义密码"
#: xpack/plugins/change_auth_plan/models.py:58
#: xpack/plugins/change_auth_plan/models.py:59
msgid "All assets use the same random password"
msgstr "所有资产使用相同的随机密码"
#: xpack/plugins/change_auth_plan/models.py:59
#: xpack/plugins/change_auth_plan/models.py:60
msgid "All assets use different random password"
msgstr "所有资产使用不同的随机密码"
#: xpack/plugins/change_auth_plan/models.py:78
#: xpack/plugins/change_auth_plan/models.py:147
#: xpack/plugins/change_auth_plan/models.py:79
#: xpack/plugins/change_auth_plan/models.py:148
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:100
#: xpack/plugins/cloud/models.py:165 xpack/plugins/cloud/models.py:219
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:91
......@@ -5563,8 +5777,8 @@ msgstr "所有资产使用不同的随机密码"
msgid "Cycle perform"
msgstr "周期执行"
#: xpack/plugins/change_auth_plan/models.py:83
#: xpack/plugins/change_auth_plan/models.py:145
#: xpack/plugins/change_auth_plan/models.py:84
#: xpack/plugins/change_auth_plan/models.py:146
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:92
#: xpack/plugins/cloud/models.py:170 xpack/plugins/cloud/models.py:217
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:83
......@@ -5573,37 +5787,37 @@ msgstr "周期执行"
msgid "Regularly perform"
msgstr "定期执行"
#: xpack/plugins/change_auth_plan/models.py:92
#: xpack/plugins/change_auth_plan/models.py:93
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:74
msgid "Password rules"
msgstr "密码规则"
#: xpack/plugins/change_auth_plan/models.py:212
#: xpack/plugins/change_auth_plan/models.py:213
msgid "* For security, do not change {} user's password"
msgstr "* 为了安全,禁止更改 {} 用户的密码"
#: xpack/plugins/change_auth_plan/models.py:216
#: xpack/plugins/change_auth_plan/models.py:217
msgid "Assets is empty, please add the asset"
msgstr "资产为空,请添加资产"
#: xpack/plugins/change_auth_plan/models.py:260
#: xpack/plugins/change_auth_plan/models.py:261
msgid "Change auth plan snapshot"
msgstr "改密计划快照"
#: xpack/plugins/change_auth_plan/models.py:275
#: xpack/plugins/change_auth_plan/models.py:426
#: xpack/plugins/change_auth_plan/models.py:276
#: xpack/plugins/change_auth_plan/models.py:433
msgid "Change auth plan execution"
msgstr "改密计划执行"
#: xpack/plugins/change_auth_plan/models.py:435
#: xpack/plugins/change_auth_plan/models.py:442
msgid "Change auth plan execution subtask"
msgstr "改密计划执行子任务"
#: xpack/plugins/change_auth_plan/models.py:453
#: xpack/plugins/change_auth_plan/models.py:460
msgid "Authentication failed"
msgstr "认证失败"
#: xpack/plugins/change_auth_plan/models.py:455
#: xpack/plugins/change_auth_plan/models.py:462
msgid "Connection timeout"
msgstr "连接超时"
......@@ -5643,7 +5857,7 @@ msgstr "添加资产"
msgid "Add node to this plan"
msgstr "添加节点"
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:12
#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:11
msgid ""
"When the user password on the asset is changed, the action is performed "
"using the admin user associated with the asset"
......@@ -5953,19 +6167,19 @@ msgid "Periodic"
msgstr "定时执行"
#: xpack/plugins/gathered_user/models.py:57
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:48
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:38
msgid "Gather user task"
msgstr "收集用户任务"
#: xpack/plugins/gathered_user/models.py:140
#: xpack/plugins/gathered_user/models.py:137
msgid "Task"
msgstr "任务"
#: xpack/plugins/gathered_user/models.py:152
#: xpack/plugins/gathered_user/models.py:149
msgid "gather user task execution"
msgstr "收集用户执行"
#: xpack/plugins/gathered_user/models.py:158
#: xpack/plugins/gathered_user/models.py:155
msgid "Assets is empty, please change nodes"
msgstr "资产为空,请更改节点"
......@@ -6213,14 +6427,142 @@ msgstr "密码匣子"
msgid "vault create"
msgstr "创建"
#, fuzzy
#~| msgid "Password should not contain special characters"
#~ msgid "Password has special char"
#~ msgstr "不能包含特殊字符"
#~ msgid "The connection fails"
#~ msgstr "连接失败"
#~ msgid "Assigned ticket"
#~ msgstr "处理人"
#~ msgid "My ticket"
#~ msgstr "我的工单"
#~ msgid "User login confirm: {}"
#~ msgstr "用户登录复核: {}"
#~ msgid ""
#~ "User: {}\n"
#~ "IP: {}\n"
#~ "City: {}\n"
#~ "Date: {}\n"
#~ msgstr ""
#~ "用户: {}\n"
#~ "IP: {}\n"
#~ "城市: {}\n"
#~ "日期: {}\n"
#~ msgid "this order"
#~ msgstr "这个工单"
#~ msgid "Approve selected"
#~ msgstr "同意所选"
#~ msgid "Reject selected"
#~ msgstr "拒绝所选"
#~ msgid ""
#~ "\n"
#~ " <div>\n"
#~ " <p>Your has a new ticket</p>\n"
#~ " <div>\n"
#~ " <b>Title:</b> {ticket.title}\n"
#~ " <br/>\n"
#~ " <b>User:</b> {user}\n"
#~ " <br/>\n"
#~ " <b>Assignees:</b> {ticket.assignees_display}\n"
#~ " <br/>\n"
#~ " <b>City:</b> {ticket.city}\n"
#~ " <br/>\n"
#~ " <b>IP:</b> {ticket.ip}\n"
#~ " <br/>\n"
#~ " <a href={url}>click here to review</a> \n"
#~ " </div>\n"
#~ " </div>\n"
#~ " "
#~ msgstr ""
#~ "\n"
#~ " <div>\n"
#~ " <p>您有一个新工单</p>\n"
#~ " <div>\n"
#~ " <b>标题:</b> {ticket.title}\n"
#~ " <br/>\n"
#~ " <b>用户:</b> {user}\n"
#~ " <br/>\n"
#~ " <b>待处理人:</b> {ticket.assignees_display}\n"
#~ " <br/>\n"
#~ " <b>城市:</b> {ticket.city}\n"
#~ " <br/>\n"
#~ " <b>IP:</b> {ticket.ip}\n"
#~ " <br/>\n"
#~ " <a href={url}>点我查看</a> \n"
#~ " </div>\n"
#~ " </div>\n"
#~ " "
#~ msgid "Login confirm ticket list"
#~ msgstr "登录复核工单列表"
#~ msgid "Login confirm ticket detail"
#~ msgstr "登录复核工单详情"
#, fuzzy
#~| msgid "Login"
#~ msgid "Login IP"
#~ msgstr "登录"
#~ msgid "succeed: {} failed: {} total: {}"
#~ msgstr "成功:{} 失败:{} 总数:{}"
#~ msgid "The user source is not LDAP"
#~ msgstr "用户来源不是LDAP"
#~ msgid "selected"
#~ msgstr "所选"
#~ msgid "not found"
#~ msgstr "没有发现"
#~ msgid "Log in frequently and try again later"
#~ msgstr "登录频繁, 稍后重试"
#~ msgid "Please carry seed value and conduct MFA secondary certification"
#~ msgstr "请携带seed值, 进行MFA二次认证"
#~ msgid "Please verify the user name and password first"
#~ msgstr "请先进行用户名和密码验证"
#~ msgid "MFA certification failed"
#~ msgstr "MFA认证失败"
#~ msgid "Accepted"
#~ msgstr "已接受"
#~ msgid "New order"
#~ msgstr "新工单"
#~ msgid "Orders"
#~ msgstr "工单管理"
#~ msgid ""
#~ "The username or password you entered is incorrect, please enter it again."
#~ msgstr "您输入的用户名或密码不正确,请重新输入。"
#~ msgid ""
#~ "You can also try {times_try} times (The account will be temporarily "
#~ "locked for {block_time} minutes)"
#~ msgstr "您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)"
#~ msgid "No order found or order expired"
#~ msgstr "没有找到工单,或者已过期"
#~ msgid "Order was rejected by {}"
#~ msgstr "工单被拒绝 {}"
#~ msgid "login_confirm_setting"
#~ msgstr "登录复核设置"
#~ msgid "The user password has expired"
#~ msgstr "用户密码已过期"
#~ msgid "Recipient"
#~ msgstr "收件人"
......@@ -6387,9 +6729,6 @@ msgstr "创建"
#~ msgid "Start"
#~ msgstr "开始"
#~ msgid "User login settings"
#~ msgstr "用户登录设置"
#~ msgid "Bit"
#~ msgstr " 位"
......
......@@ -246,30 +246,35 @@ class AdHoc(models.Model):
time_start = time.time()
date_start = timezone.now()
is_success = False
summary = {}
raw = ''
try:
date_start_s = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(_("{} Start task: {}").format(date_start_s, self.task.name))
raw, summary = self._run_only()
is_success = summary.get('success', False)
return raw, summary
except Exception as e:
logger.error(e, exc_info=True)
summary = {}
raw = {"dark": {"all": str(e)}, "contacted": []}
return raw, summary
finally:
date_end = timezone.now()
date_end_s = date_end.strftime('%Y-%m-%d %H:%M:%S')
print(_("{} Task finish").format(date_end_s))
print('.\n\n.')
try:
summary_text = json.dumps(summary)
except json.JSONDecodeError:
summary_text = '{}'
AdHocRunHistory.objects.filter(id=history.id).update(
date_start=date_start,
is_finished=True,
is_success=is_success,
date_finished=timezone.now(),
timedelta=time.time() - time_start
timedelta=time.time() - time_start,
_summary=summary_text
)
return raw, summary
def _run_only(self):
Task.objects.filter(id=self.task.id).update(date_updated=timezone.now())
......@@ -321,10 +326,9 @@ class AdHoc(models.Model):
except AdHocRunHistory.DoesNotExist:
return None
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
super().save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields)
def save(self, **kwargs):
instance = super().save(**kwargs)
return instance
def __str__(self):
return "{} of {}".format(self.task.name, self.short_id)
......@@ -393,7 +397,10 @@ class AdHocRunHistory(models.Model):
@summary.setter
def summary(self, item):
try:
self._summary = json.dumps(item)
except json.JSONDecodeError:
self._summary = json.dumps({})
@property
def success_hosts(self):
......
......@@ -3,6 +3,7 @@
import uuid
import json
from celery.exceptions import SoftTimeLimitExceeded
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
......@@ -64,6 +65,9 @@ class CommandExecution(models.Model):
try:
result = runner.execute(self.command, 'all')
self.result = result.results_command
except SoftTimeLimitExceeded as e:
print("Run timeout than 60s")
self.result = {"error": str(e)}
except Exception as e:
print("Error occur: {}".format(e))
self.result = {"error": str(e)}
......
# -*- coding: utf-8 -*-
#
import traceback
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
......@@ -9,7 +8,7 @@ from django.core.exceptions import ValidationError
from common.utils import get_logger
from ..utils import (
set_current_org, get_current_org, current_org,
get_org_filters
filter_org_queryset
)
from ..models import Organization
......@@ -20,37 +19,24 @@ __all__ = [
]
class OrgQuerySet(models.QuerySet):
pass
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):
if isinstance(org, str):
org = Organization.get_instance(org)
set_current_org(org)
return self
def get_queryset(self):
queryset = super(OrgManager, self).get_queryset()
return filter_org_queryset(queryset)
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:
msg = 'You can `objects.set_current_org(org).all()` then run it'
return self
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):
......
......@@ -4,7 +4,7 @@ from django.conf import settings
from django.db import models
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):
......@@ -72,7 +72,8 @@ class Organization(models.Model):
org = cls.default() if default else None
return org
def get_org_users(self):
@lazyproperty
def org_users(self):
from users.models import User
if self.is_real():
return self.users.all()
......@@ -81,18 +82,29 @@ class Organization(models.Model):
users = users.filter(related_user_orgs__isnull=True)
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
if self.is_real():
return self.admins.all()
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
if self.is_real():
return self.auditors.all()
return User.objects.filter(role=User.ROLE_AUDITOR)
def get_org_auditors(self):
return self.org_auditors
def get_org_members(self, exclude=()):
from users.models import User
members = User.objects.none()
......
......@@ -100,16 +100,6 @@
<link rel="stylesheet" type="text/css" href={% static "css/plugins/daterangepicker/daterangepicker.css" %} />
<script>
var dateOptions = {
singleDatePicker: true,
showDropdowns: true,
timePicker: true,
timePicker24Hour: true,
autoApply: true,
locale: {
format: 'YYYY-MM-DD HH:mm'
}
};
var api_action = "{{ api_action }}";
$(document).ready(function () {
......@@ -119,8 +109,8 @@ $(document).ready(function () {
nodesSelect2Init(".nodes-select2");
usersSelect2Init(".users-select2");
$('#date_start').daterangepicker(dateOptions);
$('#date_expired').daterangepicker(dateOptions);
initDateRangePicker('#date_start');
initDateRangePicker('#date_expired');
$("#id_assets").parent().find(".select2-selection").on('click', function (e) {
if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){
......
......@@ -23,12 +23,12 @@
{% block content %}
<div class="wrapper wrapper-content">
<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' %}
</div>
<div class="col-lg-9 animated fadeInRight" id="split-right">
<div class="tree-toggle">
<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>
</div>
</div>
......@@ -64,16 +64,7 @@
</div>
</div>
<ul class="dropdown-menu search-help">
<li><a class="search-item" data-value="name">{% trans 'Name' %}</a></li>
<li><a class="search-item" data-value="is_valid">{% trans 'Validity' %}</a></li>
<li><a class="search-item" data-value="username">{% trans 'Username' %}</a></li>
<li><a class="search-item" data-value="user_group">{% trans 'User group' %}</a></li>
<li><a class="search-item" data-value="ip">IP</a></li>
<li><a class="search-item" data-value="hostname">{% trans 'Hostname' %}</a></li>
<li><a class="search-item" data-value="node">{% trans 'Node' %}</a></li>
<li><a class="search-item" data-value="system_user">{% trans 'System user' %}</a></li>
</ul>
{% include '_filter_dropdown.html' %}
{% endblock %}
{% block custom_foot_js %}
......@@ -209,24 +200,21 @@ function initTree() {
})
}
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(){
initTable();
initTree();
var filterMenu = [
{title: "{% trans 'Name' %}", value: "name"},
{title: "{% trans 'Validity' %}", value: "is_valid"},
{title: "{% trans 'Username' %}", value: "username"},
{title: "{% trans 'User group' %}", value: "user_group"},
{title: "{% trans 'IP' %}", value: "ip"},
{title: "{% trans 'Hostname' %}", value: "hostname"},
{title: "{% trans 'Node' %}", value: "node"},
{title: "{% trans 'System user' %}", value: "system_user"},
];
initTableFilterDropdown('#permission_list_table_filter input', filterMenu)
})
.on('click', '.btn-del', function () {
var $this = $(this);
......@@ -284,27 +272,8 @@ $(document).ready(function(){
detailRows.push(tr.attr('id'));
}
}
}).on('click', '#permission_list_table_filter input', function (e) {
e.preventDefault();
e.stopPropagation();
var position = $('#permission_list_table_filter input').offset();
var y = position['top'];
var x = position['left'];
x -= 220;
y += 30;
$('.search-help').css({"top":y+"px", "left":x+"px", "position": "absolute"});
$('.dropdown-menu.search-help').show();
}).on('click', '.search-item', function (e) {
e.preventDefault();
e.stopPropagation();
var value = $(this).data('value');
var old_value = $('#permission_list_table_filter input').val();
var new_value = old_value + ' ' + value + ':';
$('#permission_list_table_filter input').val(new_value.trim());
$('.dropdown-menu.search-help').hide();
$('#permission_list_table_filter input').focus()
}).on('click', 'body', function (e) {
})
.on('click', 'body', function (e) {
$('.dropdown-menu.search-help').hide()
})
......
......@@ -115,8 +115,8 @@ $(document).ready(function () {
closeOnSelect: false
});
usersSelect2Init('.users-select2');
$('#date_start').daterangepicker(dateOptions);
$('#date_expired').daterangepicker(dateOptions);
initDateRangePicker('#date_start');
initDateRangePicker('#date_expired');
})
.on("submit", "form", function (evt) {
evt.preventDefault();
......
......@@ -334,7 +334,8 @@ class PublicSettingApi(generics.RetrieveAPIView):
c = settings.CONFIG
instance = {
"data": {
"WINDOWS_SKIP_ALL_MANUAL_PASSWORD": c.WINDOWS_SKIP_ALL_MANUAL_PASSWORD
"WINDOWS_SKIP_ALL_MANUAL_PASSWORD": c.WINDOWS_SKIP_ALL_MANUAL_PASSWORD,
"SECURITY_MAX_IDLE_TIME": c.SECURITY_MAX_IDLE_TIME,
}
}
return instance
......
......@@ -219,7 +219,7 @@ class SecuritySettingForm(BaseForm):
min_value=1, max_value=99999, required=False,
label=_("Connection max idle time"),
help_text=_(
'If idle time more than it, disconnect connection(only ssh now) '
'If idle time more than it, disconnect connection '
'Unit: minute'
),
)
......
......@@ -474,3 +474,87 @@ span.select2-selection__placeholder {
.p-r-5 {
padding-right: 5px;
}
.dropdown-submenu {
position: relative;
}
.dropdown-submenu>.dropdown-menu {
top: 0;
left: 100%;
margin-top: -6px;
margin-left: -1px;
-webkit-border-radius: 0 6px 6px 6px;
-moz-border-radius: 0 6px 6px;
border-radius: 0 6px 6px 6px;
}
.dropdown-submenu:hover>.dropdown-menu {
display: block;
}
.dropdown-submenu>a:after {
display: block;
content: " ";
float: right;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 5px 0 5px 5px;
border-left-color: #ccc;
margin-top: 5px;
margin-right: -10px;
}
.dropdown-submenu:hover>a:after {
border-left-color: #fff;
}
.dropdown-submenu.pull-left {
float: none;
}
.dropdown-submenu.pull-left>.dropdown-menu {
left: -100px;
margin-left: 10px;
-webkit-border-radius: 6px 0 6px 6px;
-moz-border-radius: 6px 0 6px 6px;
border-radius: 6px 0 6px 6px;
}
.bootstrap-tagsinput {
border: 1px solid #e5e6e7;
box-shadow: none;
padding: 4px 6px;
cursor: text;
}
/*.bootstrap-tagsinput {*/
/* background-color: #fff;*/
/* border: 1px solid #ccc;*/
/* box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);*/
/* display: inline-block;*/
/* color: #555;*/
/* vertical-align: middle;*/
/* border-radius: 4px;*/
/* max-width: 100%;*/
/* line-height: 22px;*/
/*}*/
.bootstrap-tagsinput input {
border: none;
box-shadow: none;
outline: none;
background-color: transparent;
padding: 0 6px;
margin: 0;
width: auto;
height: 22px;
max-width: inherit;
}
table.table-striped.table-bordered {
width: 100% !important;
}
......@@ -1588,9 +1588,9 @@ table.dataTable thead .sorting_desc_disabled {
/*.dataTables_length {*/
/*float: left;*/
/*}*/
.dataTables_filter label {
margin-right: 5px;
}
/*.dataTables_filter label {*/
/* margin-right: 5px;*/
/*}*/
.html5buttons {
float: right;
}
......
......@@ -137,14 +137,19 @@ function setAjaxCSRFToken() {
});
}
function activeNav() {
var url_array = document.location.pathname.split("/");
var app = url_array[1];
var resource = url_array[2];
function activeNav(prefix) {
var path = document.location.pathname;
if (prefix) {
path = path.replace(prefix, '');
console.log(path);
}
var urlArray = path.split("/");
var app = urlArray[1];
var resource = urlArray[2];
if (app === '') {
$('#index').addClass('active');
} else if (app === 'xpack' && resource === 'cloud') {
var item = url_array[3];
var item = urlArray[3];
$("#" + app).addClass('active');
$('#' + app + ' #' + resource).addClass('active');
$('#' + app + ' #' + resource + ' #' + item + ' a').css('color', '#ffffff');
......@@ -294,6 +299,8 @@ function requestApi(props) {
msg = jqXHR.responseJSON.error
} else if (jqXHR.responseJSON.msg) {
msg = jqXHR.responseJSON.msg
} else if (jqXHR.responseJSON.detail) {
msg = jqXHR.responseJSON.detail
}
}
if (msg === "") {
......@@ -302,7 +309,7 @@ function requestApi(props) {
toastr.error(msg);
}
if (typeof props.error === 'function') {
return props.error(jqXHR.responseText, jqXHR.status);
return props.error(jqXHR.responseText, jqXHR.responseJSON, jqXHR.status);
}
});
// return true;
......@@ -411,6 +418,9 @@ function makeLabel(data) {
function parseTableFilter(value) {
var cleanValues = [];
if (!value) {
return {}
}
var valuesArray = value.split(':');
for (var i=0; i<valuesArray.length; i++) {
var v = valuesArray[i].trim();
......@@ -472,6 +482,11 @@ jumpserver.language = {
last: "»"
}
};
function setDataTablePagerLength(num) {
$.fn.DataTable.ext.pager.numbers_length = num;
}
jumpserver.initDataTable = function (options) {
// options = {
// ele *: $('#dataTable_id'),
......@@ -486,6 +501,7 @@ jumpserver.initDataTable = function (options) {
// op_html: 'div.btn-group?',
// paging: true
// }
setDataTablePagerLength(5);
var ele = options.ele || $('.dataTable');
var columnDefs = [
{
......@@ -582,8 +598,14 @@ jumpserver.initServerSideDataTable = function (options) {
// columnDefs: [{target: 0, createdCell: ()=>{}}, ...],
// uc_html: '<a>header button</a>',
// op_html: 'div.btn-group?',
// paging: true
// paging: true,
// paging_numbers_length: 5;
// }
var pagingNumbersLength = 5;
if (options.paging_numbers_length){
pagingNumbersLength = options.paging_numbers_length;
}
setDataTablePagerLength(pagingNumbersLength);
var ele = options.ele || $('.dataTable');
var columnDefs = [
{
......@@ -606,16 +628,21 @@ jumpserver.initServerSideDataTable = function (options) {
style: select_style,
selector: 'td:first-child'
};
var dom = '<"#uc.pull-left"> <"pull-right"<"inline"l> <"#fb.inline"> <"inline"f><"#fa.inline">>' +
'tr' +
'<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>';
var table = ele.DataTable({
pageLength: options.pageLength || 15,
// dom: options.dom || '<"#uc.pull-left">fltr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>',
dom: options.dom || '<"#uc.pull-left"><"pull-right"<"inline"l><"#fb.inline"><"inline"f><"#fa.inline">>tr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>',
// dom: options.dom || '<"#uc.pull-left"><"pull-right"<"inline"l><"#fb.inline"><"inline"<"table-filter"f>><"#fa.inline">>tr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>',
dom: dom,
order: options.order || [],
buttons: [],
columnDefs: columnDefs,
serverSide: true,
processing: true,
searchDelay: 800,
oSearch: options.oSearch,
ajax: {
url: options.ajax_url,
error: function (jqXHR, textStatus, errorThrown) {
......@@ -1276,3 +1303,35 @@ function showCeleryTaskLog(taskId) {
var url = '/ops/celery/task/taskId/log/'.replace('taskId', taskId);
window.open(url, '', 'width=900,height=600')
}
function initDateRangePicker(selector, options) {
if (!options) {
options = {}
}
var zhLocale = {
format: 'YYYY-MM-DD HH:mm',
separator: ' ~ ',
applyLabel: "应用",
cancelLabel: "取消",
resetLabel: "重置",
daysOfWeek: ["日", "一", "二", "三", "四", "五", "六"],//汉化处理
monthNames: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
};
var defaultOption = {
singleDatePicker: true,
showDropdowns: true,
timePicker: true,
timePicker24Hour: true,
autoApply: true,
};
var userLang = navigator.language || navigator.userLanguage;;
if (userLang.indexOf('zh') !== -1) {
defaultOption.locale = zhLocale;
}
options = Object.assign(defaultOption, options);
return $(selector).daterangepicker(options);
}
function reloadPage() {
setTimeout( function () {window.location.reload();}, 300);
}
<style>
li.dropdown-submenu {
width: 100%;
}
</style>
<ul class="dropdown-menu multi-level search-help" role="menu" aria-labelledby="dropdownMenu">
</ul>
<script>
function addItem(menuRef, menuItem, parent) {
menuItem.forEach(function (item) {
if (item.submenu) {
var subItemData = "<li class='dropdown-submenu pull-left'>" +
" <a tabindex='-1' class='search-select' href='#'>VALUE</a>" +
"</li>";
var subItem = $(subItemData.replace('VALUE', item.title));
var subMenu = $('<ul class="dropdown-menu"></ul>');
addItem(subMenu, item.submenu, item.value);
subItem.append(subMenu);
menuRef.append(subItem);
} else {
var itemRef = $('<li><a class="search-item" data-value="VALUE">TITLE</a></li>'
.replace('VALUE', item.value)
.replace('TITLE', item.title)
);
if (parent){
itemRef.find('a').data('parent', parent)
}
menuRef.append(itemRef)
}
})
}
function initTableFilterDropdown(selector, menu) {
/*
menu = [
{title: "Title", value: "title"},
{title: "Status", value: "status", submenu: [
{"title": "xxx", value: "xxxx"}
]},
]
*/
var dropdownRef = $(".search-help");
addItem(dropdownRef, menu);
$(selector).on("click", function (e) {
e.preventDefault();
e.stopPropagation();
var offset1 = $(selector).offset();
var x = offset1.left;
var y = offset1.top;
var offset = $(".search-help").parent().offset();
x -= offset.left;
y -= offset.top;
{#x += 18;#}
y += 30;
$('.search-help').css({"top":y+"px", "left":x+"px", "position": "absolute"});
$('.dropdown-menu.search-help').show();
});
$('.search-item').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
var keyword = $(selector);
var value = $(this).data('value');
var oldValue = keyword.val();
var newValue = '';
var parentValue = $(this).data("parent");
var re;
if (parentValue) {
re = new RegExp(parentValue + '\\s*:\\s*\\w+');
oldValue = oldValue.replace(re, '');
newValue = oldValue + ' ' + parentValue + ':' + value;
} else {
re = new RegExp(value + '\\s*:\\s*\\w+');
oldValue = oldValue.replace(re, '');
newValue = oldValue + ' ' + value + ':';
}
keyword.val(newValue.trim());
$('.dropdown-menu.search-help').hide();
keyword.trigger('input');
keyword.focus()
});
$(window).on('click', function (e) {
dropdownRef.hide();
})
}
</script>
......@@ -9,7 +9,7 @@
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
<script src="{% static "js/jumpserver.js" %}?v=5"></script>
<script>
activeNav();
activeNav("{{ FORCE_SCRIPT_NAME }}");
$(document).ready(function(){
setAjaxCSRFToken();
$('textarea').attr('rows', 5);
......
......@@ -121,6 +121,14 @@
</li>
{% endif %}
{% if request.user.can_admin_current_org and LOGIN_CONFIRM_ENABLE and LICENSE_VALID %}
<li id="tickets">
<a href="{% url 'tickets:ticket-list' %}">
<i class="fa fa-check-square-o" style="width: 14px"></i>
<span class="nav-label">{% trans 'Tickets' %}</span>
</a>
</li>
{% endif %}
{# Audits #}
{% if request.user.can_admin_or_audit_current_org %}
......
......@@ -19,7 +19,8 @@
</span>
<span class="fa fa-sort-desc pull-right"></span>
</a>
<ul class="dropdown-menu" style="min-width: 220px">
<ul class="dropdown-menu" style="min-width: 220px;max-width: 400px;max-height: 400px; overflow: auto">
<input type="text" id="left-side-org-filter" placeholder="{% trans 'Search' %}" class="form-control">
{% for org in ADMIN_OR_AUDIT_ORGS %}
<li>
<a class="org-dropdown" href="{% url 'orgs:org-switch' pk=org.id %}" data-id="{{ org.id }}">
......@@ -35,3 +36,28 @@
{% endif %}
{% endif %}
</li>
<script>
var orgsRef;
$(document).ready(function () {
orgsRef = $(".org-dropdown");
}).on('click', '#left-side-org-filter', function (e) {
e.preventDefault();
e.stopPropagation();
}).on('keyup', '#left-side-org-filter', function () {
var input = $("#left-side-org-filter").val();
if (!input) {
orgsRef.show();
return
}
orgsRef.each(function (i, v) {
var itemRef = $(v);
var orgItemText = itemRef.text().trim();
var findIndex = orgItemText.indexOf(input);
if (findIndex === -1) {
itemRef.hide();
} else {
itemRef.show();
}
});
})
</script>
......@@ -11,13 +11,16 @@
<link href="{% static 'css/jumpserver.css' %}" rel="stylesheet">
{% block custom_head_css_js %} {% endblock %}
</head>
<body>
<div id="wrapper">
{% include '_left_side_bar.html' %}
<div id="page-wrapper" class="gray-bg">
{% include '_header_bar.html' %}
{% block help_message %} {% endblock %}
<div class="alert alert-info help-message alert-dismissable page-message" style="display: none">
<button aria-hidden="true" data-dismiss="alert" class="close hide-btn" type="button">×</button>
{% block help_message %}
{% endblock %}
</div>
{% include '_message.html' %}
{% block content %}{% endblock %}
{% include '_footer.html' %}
......@@ -27,4 +30,23 @@
</body>
{% include '_foot_js.html' %}
{% block custom_foot_js %} {% endblock %}
<script>
function getMessagePathKey() {
var path = window.location.pathname;
var key = 'message_' + btoa(path);
return key
}
$(document).ready(function () {
var pathKey = getMessagePathKey();
var hidden = window.localStorage.getItem(pathKey);
var hasMessage = $('.page-message').text().trim().length > 5;
if (!hidden && hasMessage) {
$(".help-message").show();
}
}).on('click', '.hide-btn', function () {
var pathKey = getMessagePathKey();
window.localStorage.setItem(pathKey, '1')
})
</script>
</html>
......@@ -55,9 +55,6 @@
<div class="col-md-6">
{% include '_copyright.html' %}
</div>
<div class="col-md-6 text-right">
<small>2014-2019</small>
</div>
</div>
</div>
</body>
......
from django.contrib import admin
# Register your models here.
# -*- coding: utf-8 -*-
#
from .ticket import *
# -*- coding: utf-8 -*-
#
from rest_framework import viewsets
from django.shortcuts import get_object_or_404
from common.permissions import IsValidUser
from common.utils import lazyproperty
from .. import serializers, models, mixins
class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet):
serializer_class = serializers.TicketSerializer
queryset = models.Ticket.objects.all()
permission_classes = (IsValidUser,)
filter_fields = ['status', 'title', 'action']
search_fields = ['user_display', 'title']
class TicketCommentViewSet(viewsets.ModelViewSet):
serializer_class = serializers.CommentSerializer
http_method_names = ['get', 'post']
def check_permissions(self, request):
ticket = self.ticket
if request.user == ticket.user or \
request.user in ticket.assignees.all():
return True
return False
def get_serializer_context(self):
context = super().get_serializer_context()
context['ticket'] = self.ticket
return context
@lazyproperty
def ticket(self):
ticket_id = self.kwargs.get('ticket_id')
ticket = get_object_or_404(models.Ticket, pk=ticket_id)
return ticket
def get_queryset(self):
queryset = self.ticket.comments.all()
return queryset
from django.apps import AppConfig
class TicketsConfig(AppConfig):
name = 'tickets'
def ready(self):
from . import signals_handler
return super().ready()
# Generated by Django 2.2.5 on 2019-11-15 06:57
import common.fields.model
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Ticket',
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')),
('user_display', models.CharField(max_length=128, verbose_name='User display name')),
('title', models.CharField(max_length=256, verbose_name='Title')),
('body', models.TextField(verbose_name='Body')),
('meta', common.fields.model.JsonDictTextField(default='{}', verbose_name='Meta')),
('assignee_display', models.CharField(blank=True, max_length=128, null=True, verbose_name='Assignee display name')),
('assignees_display', models.CharField(blank=True, max_length=128, verbose_name='Assignees display name')),
('type', models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm')], default='general', max_length=16, verbose_name='Type')),
('status', models.CharField(choices=[('open', 'Open'), ('closed', 'Closed')], default='open', max_length=16)),
('action', models.CharField(blank=True, choices=[('approve', 'Approve'), ('reject', 'Reject')], default='', max_length=16)),
('assignee', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_handled', to=settings.AUTH_USER_MODEL, verbose_name='Assignee')),
('assignees', models.ManyToManyField(related_name='ticket_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Assignees')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_requested', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'ordering': ('-date_created',),
},
),
migrations.CreateModel(
name='Comment',
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')),
('user_display', models.CharField(max_length=128, verbose_name='User display name')),
('body', models.TextField(verbose_name='Body')),
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='tickets.Ticket')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comments', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'ordering': ('date_created',),
},
),
]
# -*- coding: utf-8 -*-
#
from django.db.models import Q
from .models import Ticket
class TicketMixin:
def get_queryset(self):
assign = self.request.GET.get('assign', None)
if assign is None:
queryset = Ticket.get_related_tickets(self.request.user)
elif assign in ['1']:
queryset = Ticket.get_assigned_tickets(self.request.user)
else:
queryset = Ticket.get_my_tickets(self.request.user)
return queryset
# -*- coding: utf-8 -*-
#
from .ticket import *
# -*- coding: utf-8 -*-
#
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from common.mixins.models import CommonModelMixin
from common.fields.model import JsonDictTextField
__all__ = ['Ticket', 'Comment']
class Ticket(CommonModelMixin):
STATUS_OPEN = 'open'
STATUS_CLOSED = 'closed'
STATUS_CHOICES = (
(STATUS_OPEN, _("Open")),
(STATUS_CLOSED, _("Closed"))
)
TYPE_GENERAL = 'general'
TYPE_LOGIN_CONFIRM = 'login_confirm'
TYPE_CHOICES = (
(TYPE_GENERAL, _("General")),
(TYPE_LOGIN_CONFIRM, _("Login confirm"))
)
ACTION_APPROVE = 'approve'
ACTION_REJECT = 'reject'
ACTION_CHOICES = (
(ACTION_APPROVE, _('Approve')),
(ACTION_REJECT, _('Reject')),
)
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_requested', verbose_name=_("User"))
user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
title = models.CharField(max_length=256, verbose_name=_("Title"))
body = models.TextField(verbose_name=_("Body"))
meta = JsonDictTextField(verbose_name=_("Meta"), default='{}')
assignee = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_handled', verbose_name=_("Assignee"))
assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name"))
assignees = models.ManyToManyField('users.User', related_name='%(class)s_assigned', verbose_name=_("Assignees"))
assignees_display = models.CharField(max_length=128, verbose_name=_("Assignees display name"), blank=True)
type = models.CharField(max_length=16, choices=TYPE_CHOICES, default=TYPE_GENERAL, verbose_name=_("Type"))
status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='open')
action = models.CharField(choices=ACTION_CHOICES, max_length=16, default='', blank=True)
def __str__(self):
return '{}: {}'.format(self.user_display, self.title)
@property
def body_as_html(self):
return self.body.replace('\n', '<br/>')
@property
def status_display(self):
return self.get_status_display()
@property
def type_display(self):
return self.get_type_display()
@property
def action_display(self):
return self.get_action_display()
def create_status_comment(self, status, user):
if status == self.STATUS_CLOSED:
action = _("Close")
else:
action = _("Open")
body = _('{} {} this ticket').format(self.user, action)
self.comments.create(user=user, body=body)
def perform_status(self, status, user):
if self.status == status:
return
self.status = status
self.save()
def create_action_comment(self, action, user):
action_display = dict(self.ACTION_CHOICES).get(action)
body = '{} {} {}'.format(user, action_display, _("this ticket"))
self.comments.create(body=body, user=user, user_display=str(user))
def perform_action(self, action, user):
self.create_action_comment(action, user)
self.action = action
self.status = self.STATUS_CLOSED
self.assignee = user
self.assignees_display = str(user)
self.save()
def is_assignee(self, user):
return self.assignees.filter(id=user.id).exists()
def is_user(self, user):
return self.user == user
@classmethod
def get_related_tickets(cls, user, queryset=None):
if queryset is None:
queryset = cls.objects.all()
queryset = queryset.filter(
Q(assignees=user) | Q(user=user)
).distinct()
return queryset
@classmethod
def get_assigned_tickets(cls, user, queryset=None):
if queryset is None:
queryset = cls.objects.all()
queryset = queryset.filter(assignees=user)
return queryset
@classmethod
def get_my_tickets(cls, user, queryset=None):
if queryset is None:
queryset = cls.objects.all()
queryset = queryset.filter(user=user)
return queryset
class Meta:
ordering = ('-date_created',)
class Comment(CommonModelMixin):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name='comments')
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, verbose_name=_("User"), related_name='comments')
user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
body = models.TextField(verbose_name=_("Body"))
class Meta:
ordering = ('date_created', )
# -*- coding: utf-8 -*-
#
from rest_framework.permissions import BasePermission
# -*- coding: utf-8 -*-
#
from .ticket import *
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from .. import models
__all__ = ['TicketSerializer', 'CommentSerializer']
class TicketSerializer(serializers.ModelSerializer):
class Meta:
model = models.Ticket
fields = [
'id', 'user', 'user_display', 'title', 'body',
'assignees', 'assignees_display',
'status', 'action', 'date_created', 'date_updated',
'type', 'type_display', 'action_display',
]
read_only_fields = [
'user_display', 'assignees_display',
'date_created', 'date_updated',
]
def create(self, validated_data):
validated_data.pop('action')
return super().create(validated_data)
def update(self, instance, validated_data):
action = validated_data.get("action")
user = self.context["request"].user
if action and user not in instance.assignees.all():
error = {"action": "Only assignees can update"}
raise serializers.ValidationError(error)
print(validated_data)
print(instance.status)
if instance.status == instance.STATUS_CLOSED:
validated_data.pop('action')
instance = super().update(instance, validated_data)
if not instance.status == instance.STATUS_CLOSED and action:
instance.perform_action(action, user)
return instance
class CurrentTicket(object):
ticket = None
def set_context(self, serializer_field):
self.ticket = serializer_field.context['ticket']
def __call__(self):
return self.ticket
class CommentSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault(),
)
ticket = serializers.HiddenField(
default=CurrentTicket()
)
class Meta:
model = models.Comment
fields = [
'id', 'ticket', 'body', 'user', 'user_display',
'date_created', 'date_updated'
]
read_only_fields = [
'user_display', 'date_created', 'date_updated'
]
# -*- coding: utf-8 -*-
#
from django.dispatch import receiver
from django.db.models.signals import m2m_changed, post_save, pre_save
from common.utils import get_logger
from .models import Ticket, Comment
from .utils import (
send_new_ticket_mail_to_assignees,
send_ticket_action_mail_to_user
)
logger = get_logger(__name__)
@receiver(m2m_changed, sender=Ticket.assignees.through)
def on_ticket_assignees_set(sender, instance=None, action=None,
reverse=False, model=None,
pk_set=None, **kwargs):
if action == 'post_add':
logger.debug('New ticket create, send mail: {}'.format(instance.id))
assignees = model.objects.filter(pk__in=pk_set)
send_new_ticket_mail_to_assignees(instance, assignees)
if action.startswith('post') and not reverse:
instance.assignees_display = ', '.join([
str(u) for u in instance.assignees.all()
])
instance.save()
@receiver(post_save, sender=Ticket)
def on_ticket_status_change(sender, instance=None, created=False, **kwargs):
if created or instance.status == "open":
return
logger.debug('Ticket changed, send mail: {}'.format(instance.id))
send_ticket_action_mail_to_user(instance)
@receiver(pre_save, sender=Ticket)
def on_ticket_create(sender, instance=None, **kwargs):
instance.user_display = str(instance.user)
if instance.assignee:
instance.assignee_display = str(instance.assignee)
@receiver(pre_save, sender=Comment)
def on_comment_create(sender, instance=None, **kwargs):
instance.user_display = str(instance.user)
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5>
{{ object.title }}
</h5>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<div class="row">
<div class="col-lg-11">
<div class="row">
<div class="col-lg-6">
<dl class="dl-horizontal">
<dt>{% trans 'User' %}:</dt> <dd>{{ object.user_display }}</dd>
<dt>{% trans 'Type' %}:</dt> <dd>{{ object.get_type_display | default_if_none:"" }}</dd>
<dt>{% trans 'Status' %}:</dt>
<dd>
{% if object.status == "open" %}
<span class="label label-primary">
{{ object.get_status_display }}
</span>
{% elif object.status == "closed" %}
<span class="label label-danger">
{{ object.get_status_display }}
</span>
{% endif %}
</dd>
</dl>
</div>
<div class="col-lg-6">
<dl class="dl-horizontal">
<dt>{% trans 'Assignees' %}:</dt> <dd> {{ object.assignees_display }}</dd>
<dt>{% trans 'Assignee' %}:</dt> <dd>{{ object.assignee_display | default_if_none:"" }}</dd>
<dt>{% trans 'Date created' %}:</dt> <dd> {{ object.date_created }}</dd>
</dl>
</div>
</div>
<div class="row m-t-sm">
<div class="col-lg-12">
<div class="panel blank-panel">
<div class="panel-body">
<div class="feed-activity-list">
<div class="feed-element">
<a href="#" class="pull-left">
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" >
</a>
<div class="media-body ">
<strong>{{ object.user_display }}</strong> <small class="text-muted"> {{ object.date_created|timesince}} {% trans 'ago' %}</small>
<br/>
<small class="text-muted">{{ object.date_created }} </small>
<div style="padding-top: 10px">
{{ object.body_as_html | safe }}
</div>
</div>
</div>
{% for comment in object.comments.all %}
<div class="feed-element">
<a href="#" class="pull-left">
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" >
</a>
<div class="media-body ">
<strong>{{ comment.user_display }}</strong> <small class="text-muted"> {{ comment.date_created|timesince}} {% trans 'ago' %}</small>
<br/>
<small class="text-muted">{{ comment.date_created }} </small>
<div style="padding-top: 10px">
{{ comment.body }}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="feed-element">
<a href="" class="pull-left">
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" >
</a>
<div class="media-body">
<textarea class="form-control" placeholder="" id="comment"></textarea>
</div>
</div>
<div class="text-right">
{% if has_action_perm %}
<a class="btn btn-sm btn-primary btn-update btn-action" data-action="approve"><i class="fa fa-check"></i> {% trans 'Approve' %}</a>
<a class="btn btn-sm btn-warning btn-update btn-action" data-action="reject"><i class="fa fa-ban"></i> {% trans 'Reject' %}</a>
{% endif %}
<a class="btn btn-sm btn-danger btn-update btn-status" data-uid="closed"><i class="fa fa-times"></i> {% trans 'Close' %}</a>
<a class="btn btn-sm btn-info btn-update btn-comment" data-uid="comment"><i class="fa fa-pencil"></i> {% trans 'Comment' %}</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-1">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
var ticketId = "{{ object.id }}";
var status = "{{ object.status }}";
var commentUrl = "{% url 'api-tickets:ticket-comment-list' ticket_id=object.id %}";
var ticketDetailUrl = "{% url 'api-tickets:ticket-detail' pk=object.id %}";
function createComment(successCallback) {
var commentText = $("#comment").val();
if (!commentText) {
return
}
var body = {
body: commentText,
ticket: ticketId,
};
var success = function () {
window.location.reload();
};
if (successCallback){
success = successCallback;
}
requestApi({
url: commentUrl,
data: JSON.stringify(body),
method: "POST",
success: success
})
}
$(document).ready(function () {
if (status !== "open") {
$('.btn-update').attr('disabled', '1')
}
})
.on('click', '.btn-comment', function () {
createComment();
})
.on('click', '.btn-action', function () {
createComment(function () {});
var action = $(this).data('action');
var data = {
url: ticketDetailUrl,
body: JSON.stringify({action: action}),
method: "PATCH",
success: reloadPage
};
requestApi(data);
})
.on('click', '.btn-status', function () {
var status = $(this).data('uid');
var data = {
url: ticketDetailUrl,
body: JSON.stringify({status: status}),
method: "PATCH",
success: reloadPage
};
requestApi(data);
})
</script>
{% endblock %}
{% extends 'base.html' %}
{% load i18n static %}
{% block content %}
<div class="wrapper wrapper-content animated fadeIn">
<div class="col-lg-12">
<div class="tabs-container">
<ul class="nav nav-tabs">
<li {% if not assign %} class="active" {% endif %}><a href="{% url 'tickets:ticket-list' %}"> {% trans 'My tickets' %}</a></li>
<li {% if assign %}class="active" {% endif %}><a href="{% url 'tickets:ticket-list' %}?assign=1" >{% trans 'Assigned me' %} <span class="label label-primary">{{ assigned_open_count }}</span></a></li>
</ul>
<div class="tab-content">
<div id="my-tickets" class="tab-pane active">
<div class="panel-body">
{% if False %}
<div class="uc pull-left m-r-5">
<div class="btn-group">
<button data-toggle="dropdown" class="btn btn-primary btn-sm dropdown-toggle" aria-expanded="false">
{% trans 'Create ticket' %} <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="#">{% trans 'Asset permission' %}</a></li>
</ul>
</div>
</div>
{% endif %}
<table class="table table-striped table-bordered table-hover" id="ticket-list-table" >
<thead>
<tr>
<th class="text-center">
<input id="" type="checkbox" class="ipt_check_all">
</th>
<th class="text-center">{% trans 'Title' %}</th>
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Type' %}</th>
<th class="text-center">{% trans 'Status' %}</th>
<th class="text-center">{% trans 'Datetime' %}</th>
</tr>
</thead>
{% include '_filter_dropdown.html' %}
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
var assignedTable, myTable, listUrl;
{% if assign %}
listUrl = '{% url "api-tickets:ticket-list" %}?assign=1';
{% else %}
listUrl = '{% url "api-tickets:ticket-list" %}?assign=0';
{% endif %}
function initTable() {
var options = {
ele: $('#ticket-list-table'),
oSearch: {sSearch: "status:open"},
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detailBtn = '<a href="{% url "tickets:ticket-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detailBtn.replace("{{ DEFAULT_PK }}", rowData.id));
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
$(td).html(rowData.type_display)
}},
{targets: 4, createdCell: function (td, cellData) {
if (cellData === "open") {
$(td).html('<i class="fa fa-check-circle-o text-navy"></i>');
} else {
$(td).html('<i class="fa fa-times-circle-o text-danger"></i>')
}
}},
{targets: 5, createdCell: function (td, cellData) {
var d = toSafeLocalDateStr(cellData);
$(td).html(d)
}}
],
ajax_url: listUrl,
columns: [
{data: "id"}, {data: "title"},
{data: "user_display"}, {data: "type"},
{data: "status", width: "40px"},
{data: "date_created"}
],
op_html: $('#actions').html()
};
myTable = jumpserver.initServerSideDataTable(options);
return myTable
}
$(document).ready(function(){
initTable();
var menu = [
{title: "{% trans 'Title' %}", value: "title"},
{title: "{% trans 'User' %}", value: "user_display"},
{title: "{% trans 'Status' %}", value: "status", submenu: [
{title: "{% trans 'Open' %}", value: "open"},
{title: "{% trans 'Closed' %}", value: "closed"},
]},
{title: "{% trans 'Action' %}", value: "action", submenu: [
{title: "{% trans 'Approve' %}", value: "approve"},
{title: "{% trans 'Reject' %}", value: "reject"},
]},
];
initTableFilterDropdown('#ticket-list-table_filter input', menu)
})
</script>
{% endblock %}
from django.test import TestCase
# Create your tests here.
# -*- coding: utf-8 -*-
#
# -*- coding: utf-8 -*-
#
from rest_framework_bulk.routes import BulkRouter
from .. import api
app_name = 'tickets'
router = BulkRouter()
router.register('tickets', api.TicketViewSet, 'ticket')
router.register('tickets/(?P<ticket_id>[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment')
urlpatterns = [
]
urlpatterns += router.urls
# -*- coding: utf-8 -*-
#
from django.urls import path
from .. import views
app_name = 'tickets'
urlpatterns = [
path('tickets/', views.TicketListView.as_view(), name='ticket-list'),
path('tickets/<uuid:pk>/', views.TicketDetailView.as_view(), name='ticket-detail'),
]
# -*- coding: utf-8 -*-
#
from django.conf import settings
from django.utils.translation import ugettext as _
from common.utils import get_logger, reverse
from common.tasks import send_mail_async
logger = get_logger(__name__)
def send_new_ticket_mail_to_assignees(ticket, assignees):
recipient_list = [user.email for user in assignees]
user = ticket.user
if not recipient_list:
logger.error("Ticket not has assignees: {}".format(ticket.id))
return
subject = '{}: {}'.format(_("New ticket"), ticket.title)
detail_url = reverse('tickets:ticket-detail',
kwargs={'pk': ticket.id}, external=True)
message = _("""
<div>
<p>Your has a new ticket</p>
<div>
{body}
<br/>
<a href={url}>click here to review</a>
</div>
</div>
""").format(body=ticket.body, user=user, url=detail_url)
send_mail_async.delay(subject, message, recipient_list, html_message=message)
def send_ticket_action_mail_to_user(ticket):
if not ticket.user:
logger.error("Ticket not has user: {}".format(ticket.id))
return
user = ticket.user
recipient_list = [user.email]
subject = '{}: {}'.format(_("Ticket has been reply"), ticket.title)
message = _("""
<div>
<p>Your ticket has been replay</p>
<div>
<b>Title:</b> {ticket.title}
<br/>
<b>Assignee:</b> {ticket.assignee_display}
<br/>
<b>Status:</b> {ticket.status_display}
<br/>
</div>
</div>
""").format(ticket=ticket)
send_mail_async.delay(subject, message, recipient_list, html_message=message)
from django.views.generic import TemplateView, DetailView
from django.utils.translation import ugettext as _
from common.permissions import PermissionsMixin, IsValidUser
from .models import Ticket
from . import mixins
class TicketListView(PermissionsMixin, TemplateView):
template_name = 'tickets/ticket_list.html'
permission_classes = (IsValidUser,)
def get_context_data(self, **kwargs):
assign = self.request.GET.get('assign', '0') == '1'
context = super().get_context_data(**kwargs)
assigned_open_count = Ticket.get_assigned_tickets(self.request.user)\
.filter(status=Ticket.STATUS_OPEN).count()
context.update({
'app': _("Tickets"),
'action': _("Ticket list"),
'assign': assign,
'assigned_open_count': assigned_open_count
})
return context
class TicketDetailView(PermissionsMixin, mixins.TicketMixin, DetailView):
template_name = 'tickets/ticket_detail.html'
permission_classes = (IsValidUser,)
queryset = Ticket.objects.all()
def get_context_data(self, **kwargs):
ticket = self.get_object()
has_action_perm = ticket.is_assignee(self.request.user)
context = super().get_context_data(**kwargs)
context.update({
'app': _("Tickets"),
'action': _("Ticket detail"),
'has_action_perm': has_action_perm,
})
return context
......@@ -3,3 +3,4 @@
from .user import *
from .group import *
from .relation import *
# -*- coding: utf-8 -*-
#
from rest_framework_bulk import BulkModelViewSet
from django.db.models import F
from common.permissions import IsOrgAdmin
from .. import serializers
from ..models import User
__all__ = ['UserUserGroupRelationViewSet']
class UserUserGroupRelationViewSet(BulkModelViewSet):
filter_fields = ('user', 'usergroup')
search_fields = filter_fields
serializer_class = serializers.UserUserGroupRelationSerializer
permission_classes = (IsOrgAdmin,)
def get_queryset(self):
queryset = User.groups.through.objects.all()\
.annotate(user_name=F('user__name'))\
.annotate(usergroup_name=F('usergroup__name'))
return queryset
def allow_bulk_destroy(self, qs, filtered):
if filtered.count() != 1:
return False
else:
return True
......@@ -40,6 +40,7 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
filter_fields = ('username', 'email', 'name', 'id')
search_fields = filter_fields
serializer_class = serializers.UserSerializer
serializer_display_class = serializers.UserDisplaySerializer
permission_classes = (IsOrgAdmin, CanUpdateDeleteUser)
def get_queryset(self):
......@@ -172,8 +173,8 @@ class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
if user == request.user:
msg = _("Could not reset self otp, use profile reset instead")
return Response({"error": msg}, status=401)
if user.otp_enabled and user.otp_secret_key:
user.otp_secret_key = ''
if user.mfa_enabled:
user.reset_mfa()
user.save()
logout(request)
return Response({"msg": "success"})
......@@ -61,10 +61,10 @@ class UserCreateUpdateFormMixin(OrgModelForm):
fields = [
'username', 'name', 'email', 'groups', 'wechat',
'source', 'phone', 'role', 'date_expired',
'comment', 'otp_level'
'comment', 'mfa_level'
]
widgets = {
'otp_level': forms.RadioSelect(),
'mfa_level': forms.RadioSelect(),
'groups': forms.SelectMultiple(
attrs={
'class': 'select2',
......@@ -126,13 +126,13 @@ class UserCreateUpdateFormMixin(OrgModelForm):
def save(self, commit=True):
password = self.cleaned_data.get('password')
otp_level = self.cleaned_data.get('otp_level')
mfa_level = self.cleaned_data.get('mfa_level')
public_key = self.cleaned_data.get('public_key')
user = super().save(commit=commit)
if password:
user.reset_password(password)
if otp_level:
user.otp_level = otp_level
if mfa_level:
user.mfa_level = mfa_level
user.save()
if public_key:
user.public_key = public_key
......@@ -158,8 +158,8 @@ class UserUpdateForm(UserCreateUpdateFormMixin):
class UserProfileForm(forms.ModelForm):
username = forms.CharField(disabled=True)
name = forms.CharField(disabled=True)
username = forms.CharField(disabled=True, label=_("Username"))
name = forms.CharField(disabled=True, label=_("Name"))
email = forms.CharField(disabled=True)
class Meta:
......@@ -183,10 +183,10 @@ class UserMFAForm(forms.ModelForm):
class Meta:
model = User
fields = ['otp_level']
widgets = {'otp_level': forms.RadioSelect()}
fields = ['mfa_level']
widgets = {'mfa_level': forms.RadioSelect()}
help_texts = {
'otp_level': _('* Enable MFA authentication '
'mfa_level': _('* Enable MFA authentication '
'to make the account more secure.'),
}
......
# Generated by Django 2.2.5 on 2019-11-18 08:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0023_auto_20190724_1525'),
]
operations = [
migrations.RenameField(
model_name='user',
old_name='otp_level',
new_name='mfa_level',
),
]
......@@ -11,12 +11,13 @@ from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractUser
from django.core.cache import cache
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.shortcuts import reverse
from orgs.utils import current_org
from common.utils import get_signer, date_expired_default, get_logger
from common.utils import get_signer, date_expired_default, get_logger, lazyproperty
from common import fields
......@@ -60,10 +61,6 @@ class AuthMixin:
def can_use_ssh_key_login(self):
return settings.TERMINAL_PUBLIC_KEY_AUTH
def check_otp(self, code):
from ..utils import check_otp_code
return check_otp_code(self.otp_secret_key, code)
def is_public_key_valid(self):
"""
Check if the user's ssh public key is valid.
......@@ -115,6 +112,30 @@ class AuthMixin:
return True
return False
def get_login_confirm_setting(self):
if hasattr(self, 'login_confirm_setting'):
s = self.login_confirm_setting
if s.reviewers.all().count() and s.is_active:
return s
return False
@staticmethod
def get_public_key_body(key):
for i in key.split():
if len(i) > 256:
return i
return key
def check_public_key(self, key):
if not self.public_key:
return False
key = self.get_public_key_body(key)
key_saved = self.get_public_key_body(self.public_key)
if key == key_saved:
return True
else:
return False
class RoleMixin:
ROLE_ADMIN = 'Admin'
......@@ -195,7 +216,7 @@ class RoleMixin:
from orgs.models import Organization
return Organization.get_user_admin_or_audit_orgs(self)
@property
@lazyproperty
def is_org_admin(self):
if self.is_superuser or self.related_admin_orgs.exists():
return True
......@@ -328,35 +349,70 @@ class TokenMixin:
class MFAMixin:
otp_level = 0
mfa_level = 0
otp_secret_key = ''
OTP_LEVEL_CHOICES = (
MFA_LEVEL_CHOICES = (
(0, _('Disable')),
(1, _('Enable')),
(2, _("Force enable")),
)
@property
def otp_enabled(self):
return self.otp_force_enabled or self.otp_level > 0
def mfa_enabled(self):
return self.mfa_force_enabled or self.mfa_level > 0
@property
def otp_force_enabled(self):
def mfa_force_enabled(self):
if settings.SECURITY_MFA_AUTH:
return True
return self.otp_level == 2
return self.mfa_level == 2
def enable_otp(self):
if not self.otp_level == 2:
self.otp_level = 1
def enable_mfa(self):
if not self.mfa_level == 2:
self.mfa_level = 1
def force_enable_otp(self):
self.otp_level = 2
def force_enable_mfa(self):
self.mfa_level = 2
def disable_otp(self):
self.otp_level = 0
def disable_mfa(self):
self.mfa_level = 0
self.otp_secret_key = None
def reset_mfa(self):
if self.mfa_is_otp():
self.otp_secret_key = ''
@staticmethod
def mfa_is_otp():
if settings.CONFIG.OTP_IN_RADIUS:
return False
return True
def check_radius(self, code):
from authentication.backends.radius import RadiusBackend
backend = RadiusBackend()
user = backend.authenticate(None, username=self.username, password=code)
if user:
return True
return False
def check_otp(self, code):
from ..utils import check_otp_code
return check_otp_code(self.otp_secret_key, code)
def check_mfa(self, code):
if settings.CONFIG.OTP_IN_RADIUS:
return self.check_radius(code)
else:
return self.check_otp(code)
def mfa_enabled_but_not_set(self):
if self.mfa_enabled and \
self.mfa_is_otp() and \
not self.otp_secret_key:
return True
return False
class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
SOURCE_LOCAL = 'local'
......@@ -364,7 +420,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
SOURCE_OPENID = 'openid'
SOURCE_RADIUS = 'radius'
SOURCE_CHOICES = (
(SOURCE_LOCAL, 'Local'),
(SOURCE_LOCAL, _('Local')),
(SOURCE_LDAP, 'LDAP/AD'),
(SOURCE_OPENID, 'OpenID'),
(SOURCE_RADIUS, 'Radius'),
......@@ -395,8 +451,8 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
phone = models.CharField(
max_length=20, blank=True, null=True, verbose_name=_('Phone')
)
otp_level = models.SmallIntegerField(
default=0, choices=MFAMixin.OTP_LEVEL_CHOICES, verbose_name=_('MFA')
mfa_level = models.SmallIntegerField(
default=0, choices=MFAMixin.MFA_LEVEL_CHOICES, verbose_name=_('MFA')
)
otp_secret_key = fields.EncryptCharField(max_length=128, blank=True, null=True)
# Todo: Auto generate key, let user download
......
......@@ -2,3 +2,4 @@
#
from .user import *
from .group import *
from .realtion import *
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.fields import StringManyToManyField
......@@ -9,10 +10,9 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import User, UserGroup
from .. import utils
__all__ = [
'UserGroupSerializer', 'UserGroupListSerializer',
'UserGroupUpdateMemberSerializer'
'UserGroupUpdateMemberSerializer',
]
......
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from ..models import User
__all__ = ['UserUserGroupRelationSerializer']
class UserUserGroupRelationSerializer(serializers.ModelSerializer):
user_name = serializers.CharField(read_only=True)
usergroup_name = serializers.CharField(read_only=True)
class Meta:
model = User.groups.through
fields = ['id', 'user', 'user_name', 'usergroup', 'usergroup_name']
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.utils import validate_ssh_public_key
......@@ -14,51 +13,32 @@ from ..models import User, UserGroup
__all__ = [
'UserSerializer', 'UserPKUpdateSerializer', 'UserUpdateGroupSerializer',
'ChangeUserPasswordSerializer', 'ResetOTPSerializer',
'UserProfileSerializer', 'UserDisplaySerializer',
]
class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
can_update = serializers.SerializerMethodField()
can_delete = serializers.SerializerMethodField()
class Meta:
model = User
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'username', 'password', 'email', 'public_key',
'groups', 'groups_display',
'role', 'role_display', 'wechat', 'phone', 'otp_level',
'comment', 'source', 'source_display', 'is_valid', 'is_expired',
'groups', 'role', 'wechat', 'phone', 'mfa_level',
'comment', 'source', 'is_valid', 'is_expired',
'is_active', 'created_by', 'is_first_login',
'date_password_last_updated', 'date_expired', 'avatar_url',
'can_update', 'can_delete',
]
extra_kwargs = {
'password': {'write_only': True, 'required': False, 'allow_null': True, 'allow_blank': True},
'public_key': {'write_only': True},
'groups_display': {'label': _('Groups name')},
'source_display': {'label': _('Source name')},
'is_first_login': {'label': _('Is first login'), 'read_only': True},
'role_display': {'label': _('Role name')},
'is_valid': {'label': _('Is valid')},
'is_expired': {'label': _('Is expired')},
'avatar_url': {'label': _('Avatar url')},
'created_by': {'read_only': True, 'allow_blank': True},
'can_update': {'read_only': True},
'can_delete': {'read_only': True},
}
def get_can_update(self, obj):
return CanUpdateDeleteUser.has_update_object_permission(
self.context['request'], self.context['view'], obj
)
def get_can_delete(self, obj):
return CanUpdateDeleteUser.has_delete_object_permission(
self.context['request'], self.context['view'], obj
)
def validate_role(self, value):
request = self.context.get('request')
if not request.user.is_superuser and value != User.ROLE_USER:
......@@ -99,6 +79,38 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
return attrs
class UserDisplaySerializer(UserSerializer):
can_update = serializers.SerializerMethodField()
can_delete = serializers.SerializerMethodField()
class Meta(UserSerializer.Meta):
fields = UserSerializer.Meta.fields + [
'groups_display', 'role_display', 'source_display',
'can_update', 'can_delete',
]
def get_can_update(self, obj):
return CanUpdateDeleteUser.has_update_object_permission(
self.context['request'], self.context['view'], obj
)
def get_can_delete(self, obj):
return CanUpdateDeleteUser.has_delete_object_permission(
self.context['request'], self.context['view'], obj
)
def get_extra_kwargs(self):
kwargs = super().get_extra_kwargs()
kwargs.update({
'can_update': {'read_only': True},
'can_delete': {'read_only': True},
'groups_display': {'label': _('Groups name')},
'source_display': {'label': _('Source name')},
'role_display': {'label': _('Role name')},
})
return kwargs
class UserPKUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = User
......@@ -130,3 +142,17 @@ class ChangeUserPasswordSerializer(serializers.ModelSerializer):
class ResetOTPSerializer(serializers.Serializer):
msg = serializers.CharField(read_only=True)
def create(self, validated_data):
pass
def update(self, instance, validated_data):
pass
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
'id', 'username', 'name', 'role', 'email'
]
......@@ -20,7 +20,7 @@
<h3>{% trans 'Auth' %}</h3>
{% block password %}{% endblock %}
{% bootstrap_field form.otp_level layout="horizontal" %}
{% bootstrap_field form.mfa_level layout="horizontal" %}
{% bootstrap_field form.source layout="horizontal" %}
<div class="hr-line-dashed"></div>
......@@ -57,6 +57,7 @@
{% endblock %}
{% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.zh-CN.min.js' %}"></script>
<script type="text/javascript" src='{% static "js/plugins/daterangepicker/moment.min.js" %}'></script>
<script type="text/javascript" src='{% static "js/plugins/daterangepicker/daterangepicker.min.js" %}'></script>
<link rel="stylesheet" type="text/css" href={% static "css/plugins/daterangepicker/daterangepicker.css" %} />
......@@ -73,20 +74,10 @@
$(groups_id).closest('.form-group').removeClass('hidden');
}}
var dateOptions = {
singleDatePicker: true,
showDropdowns: true,
timePicker: true,
timePicker24Hour: true,
autoApply: true,
locale: {
format: 'YYYY-MM-DD HH:mm'
}
};
$(document).ready(function () {
$('.select2').select2();
$('#id_date_expired').daterangepicker(dateOptions);
var mfa_radio = $('#id_otp_level');
initDateRangePicker('#id_date_expired');
var mfa_radio = $('#id_mfa_level');
mfa_radio.addClass("form-inline");
mfa_radio.children().css("margin-right","15px");
fieldDisplay()
......
......@@ -90,9 +90,9 @@
<tr>
<td>{% trans 'MFA certification' %}:</td>
<td><b>
{% if user_object.otp_force_enabled %}
{% if user_object.mfa_force_enabled %}
{% trans 'Force enabled' %}
{% elif user_object.otp_enabled%}
{% elif user_object.mfa_enabled%}
{% trans 'Enabled' %}
{% else %}
{% trans 'Disabled' %}
......@@ -158,18 +158,20 @@
</tr>
<tr>
<td>{% trans 'Force enabled MFA' %}:</td>
<td><span class="pull-right">
<td>
<span class="pull-right">
<div class="switch">
<div class="onoffswitch">
<input type="checkbox" class="onoffswitch-checkbox" {% if user_object.otp_force_enabled %} checked {% endif %}
id="force_enable_otp">
<label class="onoffswitch-label" for="force_enable_otp">
<input type="checkbox" class="onoffswitch-checkbox" {% if user_object.mfa_force_enabled %} checked {% endif %}
id="force_enable_mfa">
<label class="onoffswitch-label" for="force_enable_mfa">
<span class="onoffswitch-inner"></span>
<span class="onoffswitch-switch"></span>
</label>
</div>
</div>
</span></td>
</span>
</td>
</tr>
<tr>
<td>{% trans 'Reset MFA' %}:</td>
......@@ -212,6 +214,7 @@
</div>
</div>
{% if request.user.can_admin_current_org %}
{% if user_object.can_user_current_org or user_object.can_admin_current_org %}
<div class="panel panel-info">
<div class="panel-heading">
......@@ -252,6 +255,46 @@
</div>
</div>
{% endif %}
{% if LOGIN_CONFIRM_ENABLE %}
<div class="panel panel-warning">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Login confirm' %}
</div>
<div class="panel-body">
<table class="table">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Reviewers' %}" id="id_assignees" class="users-select2" style="width: 100%" multiple="" tabindex="4">
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-warning btn-small" id="btn_reviewer_confirm">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
{% if user_object.get_login_confirm_setting %}
{% for u in user_object.login_confirm_setting.reviewers.all %}
<tr>
<td >
<b class="bdg_reviewer">{{ u }}</b>
</td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-leave-reviewer" data-uid={{ u.id }} type="button"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% endif %}
</div>
</div>
......@@ -264,6 +307,7 @@
{% block custom_foot_js %}
<script>
jumpserver.nodes_selected = {};
var usersSelect2;
function updateUserGroups(groups) {
var the_url = "{% url 'api-users:user-update-group' pk=user_object.id %}";
......@@ -294,6 +338,19 @@ function updateUserGroups(groups) {
});
}
function updateUserLoginReviewer(reviewers) {
var url = "{% url 'api-auth:login-confirm-setting-update' user_id=user_object.id %}";
var data = {reviewers: reviewers};
requestApi({
url: url,
data: JSON.stringify(data),
method: 'PATCH',
success: function () {
window.location.reload();
}
})
}
$(document).ready(function() {
$('.select2').select2()
.on('select2:select', function(evt) {
......@@ -304,6 +361,7 @@ $(document).ready(function() {
var data = evt.params.data;
delete jumpserver.nodes_selected[data.id];
});
usersSelect2 = usersSelect2Init('.users-select2')
})
.on('click', '#is_active', function() {
var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
......@@ -318,7 +376,7 @@ $(document).ready(function() {
success_message: success
});
})
.on('click', '#force_enable_otp', function() {
.on('click', '#force_enable_mfa', function() {
{% if request.user == user_object %}
toastr.error("{% trans 'Goto profile page enable MFA' %}");
return;
......@@ -326,16 +384,16 @@ $(document).ready(function() {
var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
var checked = $(this).prop('checked');
var otp_level;
var mfa_level;
var otp_secret_key;
if(checked){
otp_level = 2
mfa_level = 2
}else{
otp_level = 0;
mfa_level = 0;
otp_secret_key = '';
}
var body = {
'otp_level': otp_level,
'mfa_level': mfa_level,
'otp_secret_key': otp_secret_key
};
var success = '{% trans "Update successfully!" %}';
......@@ -497,6 +555,32 @@ $(document).ready(function() {
method: "GET",
success_message: "{% trans 'Reset user MFA success' %}"
})
}).on('click', '.btn-leave-reviewer', function () {
var reviewersId = [];
var removeReviewerId = $(this).data('uid');
$('.btn-leave-reviewer').each(function (i, v) {
var reviewerId = $(v).data('uid');
if (reviewerId !== removeReviewerId) {
reviewersId.push(reviewerId);
}
});
updateUserLoginReviewer(reviewersId);
}).on('click', '#btn_reviewer_confirm', function () {
var reviewersId = [];
$('.btn-leave-reviewer').each(function (i, v) {
var reviewerId = $(v).data('uid');
reviewersId.push(reviewerId);
});
var selectedId = usersSelect2.val();
if (selectedId.length === 0) {
return
}
selectedId.forEach(function (i) {
if (reviewersId.indexOf(i) === -1) {
reviewersId.push(i)
}
});
updateUserLoginReviewer(reviewersId);
})
</script>
{% endblock %}
......@@ -26,7 +26,7 @@
{% endblock %}
{% block table_container %}
<div class="uc pull-left m-r-5"><a href="{% url "users:user-create" %}" class="btn btn-sm btn-primary"> {% trans "Create user" %} </a></div>
<table class="table table-striped table-bordered table-hover " id="user_list_table" >
<table class="table table-striped table-bordered table-hover " id="user_list_table">
<thead>
<tr>
<th class="text-center">
......@@ -124,7 +124,7 @@ function initTable() {
{data: "role"},
{data: "groups_display", orderable: false},
{data: "source"},
{data: "is_valid", orderable: false},
{data: "is_valid", orderable: false, width: "50px"},
{data: "id", orderable: false, width: "100px"}
],
op_html: $('#actions').html()
......
......@@ -86,9 +86,9 @@
<tr>
<td class="text-navy">{% trans 'MFA certification' %}</td>
<td>
{% if user.otp_force_enabled %}
{% if user.mfa_force_enabled %}
{% trans 'Force enable' %}
{% elif user.otp_enabled%}
{% elif user.mfa_enabled%}
{% trans 'Enable' %}
{% else %}
{% trans 'Disable' %}
......@@ -158,8 +158,8 @@
<span class="pull-right">
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" id=""
href="
{% if request.user.otp_enabled and request.user.otp_secret_key %}
{% if request.user.otp_force_enabled %}
{% if request.user.mfa_enabled %}
{% if request.user.mfa_force_enabled %}
" disabled >{% trans 'Disable' %}
{% else %}
{% url 'users:user-otp-disable-authentication' %}
......@@ -173,22 +173,22 @@
</span>
</td>
</tr>
{% if request.user.can_update_password %}
<tr class="no-borders">
<td>{% trans 'Update password' %}:</td>
{% if request.user.mfa_enabled %}
<tr>
<td>{% trans 'Update MFA' %}:</td>
<td>
<span class="pull-right">
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" href="{% url 'users:user-password-update' %}">{% trans 'Update' %}</a>
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" href="{% url 'users:user-otp-update' %}">{% trans 'Update' %}</a>
</span>
</td>
</tr>
{% endif %}
{% if request.user.otp_enabled and request.user.otp_secret_key %}
<tr>
<td>{% trans 'Update MFA' %}:</td>
{% if request.user.can_update_password %}
<tr class="no-borders">
<td>{% trans 'Update password' %}:</td>
<td>
<span class="pull-right">
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" href="{% url 'users:user-otp-update' %}">{% trans 'Update' %}</a>
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" href="{% url 'users:user-password-update' %}">{% trans 'Update' %}</a>
</span>
</td>
</tr>
......
......@@ -14,14 +14,12 @@ app_name = 'users'
router = BulkRouter()
router.register(r'users', api.UserViewSet, 'user')
router.register(r'groups', api.UserGroupViewSet, 'user-group')
router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'user-group-relation')
urlpatterns = [
path('connection-token/', auth_api.UserConnectionTokenApi.as_view(),
name='connection-token'),
path('auth/', auth_api.UserAuthApi.as_view(), name='user-auth'),
path('otp/auth/', auth_api.UserOtpAuthApi.as_view(), name='user-otp-auth'),
path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'),
path('users/<uuid:pk>/otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'),
......
......@@ -193,7 +193,6 @@ def send_reset_ssh_key_mail(user):
send_mail_async.delay(subject, message, recipient_list, html_message=message)
def get_user_or_tmp_user(request):
user = request.user
tmp_user = get_tmp_user_from_cache(request)
......@@ -212,16 +211,18 @@ def get_tmp_user_from_cache(request):
return user
def set_tmp_user_to_cache(request, user):
cache.set(request.session.session_key+'user', user, 600)
def set_tmp_user_to_cache(request, user, ttl=3600):
cache.set(request.session.session_key+'user', user, ttl)
def redirect_user_first_login_or_index(request, redirect_field_name):
if request.user.is_first_login:
return reverse('users:user-first-login')
return request.POST.get(
redirect_field_name,
request.GET.get(redirect_field_name, reverse('index')))
url_in_post = request.POST.get(redirect_field_name)
if url_in_post:
return url_in_post
url_in_get = request.GET.get(redirect_field_name, reverse('index'))
return url_in_get
def generate_otp_uri(request, issuer="Jumpserver"):
......
......@@ -170,12 +170,12 @@ class UserFirstLoginView(PermissionsMixin, SessionWizardView):
form.instance = self.request.user
if isinstance(form, forms.UserMFAForm):
choices = form.fields["otp_level"].choices
if self.request.user.otp_force_enabled:
choices = form.fields["mfa_level"].choices
if self.request.user.mfa_force_enabled:
choices = [(k, v) for k, v in choices if k == 2]
else:
choices = [(k, v) for k, v in choices if k in [0, 1]]
form.fields["otp_level"].choices = choices
form.fields["otp_level"].initial = self.request.user.otp_level
form.fields["mfa_level"].choices = choices
form.fields["mfa_level"].initial = self.request.user.mfa_level
return form
......@@ -347,7 +347,12 @@ class UserOtpEnableAuthenticationView(FormView):
if not user:
form.add_error("password", _("Password invalid"))
return self.form_invalid(form)
if user.mfa_is_otp():
return redirect(self.get_success_url())
else:
user.enable_mfa()
user.save()
return redirect('users:user-otp-settings-success')
def get_success_url(self):
return reverse('users:user-otp-enable-install-app')
......@@ -395,7 +400,7 @@ class UserOtpEnableBindView(TemplateView, FormView):
def save_otp(self, otp_secret_key):
user = get_user_or_tmp_user(self.request)
user.enable_otp()
user.enable_mfa()
user.otp_secret_key = otp_secret_key
user.save()
......@@ -411,7 +416,7 @@ class UserOtpDisableAuthenticationView(FormView):
otp_secret_key = user.otp_secret_key
if check_otp_code(otp_secret_key, otp_code):
user.disable_otp()
user.disable_mfa()
user.save()
return super().form_valid(form)
else:
......@@ -447,7 +452,7 @@ class UserOtpSettingsSuccessView(TemplateView):
auth_logout(self.request)
title = _('MFA enable success')
describe = _('MFA enable success, return login page')
if not user.otp_enabled:
if not user.mfa_enabled:
title = _('MFA disable success')
describe = _('MFA disable success, return login page')
......
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