Commit e5953e19 authored by ibuler's avatar ibuler

Merge branch 'dev' of bitbucket.org:jumpserver/core into dev

parents e9247dd5 091cf39e
...@@ -17,7 +17,6 @@ dump.rdb ...@@ -17,7 +17,6 @@ dump.rdb
.idea/ .idea/
db.sqlite3 db.sqlite3
config.py config.py
migrations/
*.log *.log
host_rsa_key host_rsa_key
*.bat *.bat
...@@ -33,3 +32,4 @@ celerybeat-schedule.db ...@@ -33,3 +32,4 @@ celerybeat-schedule.db
data/static data/static
docs/_build/ docs/_build/
xpack xpack
logs/*
FROM registry.fit2cloud.com/public/python:v3
MAINTAINER Jumpserver Team <ibuler@qq.com>
WORKDIR /opt/jumpserver
RUN useradd jumpserver
COPY ./requirements /tmp/requirements
RUN yum -y install epel-release && cd /tmp/requirements && \
yum -y install $(cat rpm_requirements.txt)
RUN cd /tmp/requirements && pip install -r requirements.txt
COPY . /opt/jumpserver
COPY config_docker.py /opt/jumpserver/config.py
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs
ENV LANG=zh_CN.UTF-8
ENV LC_ALL=zh_CN.UTF-8
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]
...@@ -20,7 +20,7 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点 ...@@ -20,7 +20,7 @@ Jumpserver采纳分布式架构,支持多机房跨区域部署,中心节点
### 功能 ### 功能
![Jumpserver功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver13.jpg "Jumpserver功能") ![Jumpserver功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver-14.png "Jumpserver功能")
### 开始使用 ### 开始使用
......
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
__version__ = "1.4.3" __version__ = "1.4.4"
...@@ -17,6 +17,7 @@ from django.db import transaction ...@@ -17,6 +17,7 @@ from django.db import transaction
from rest_framework import generics from rest_framework import generics
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from common.mixins import IDInFilterMixin from common.mixins import IDInFilterMixin
from common.utils import get_logger from common.utils import get_logger
...@@ -37,9 +38,17 @@ class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet): ...@@ -37,9 +38,17 @@ class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet):
""" """
Admin user api set, for add,delete,update,list,retrieve resource Admin user api set, for add,delete,update,list,retrieve resource
""" """
filter_fields = ("name", "username")
search_fields = filter_fields
queryset = AdminUser.objects.all() queryset = AdminUser.objects.all()
serializer_class = serializers.AdminUserSerializer serializer_class = serializers.AdminUserSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
return queryset
class AdminUserAuthApi(generics.UpdateAPIView): class AdminUserAuthApi(generics.UpdateAPIView):
......
...@@ -53,14 +53,14 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): ...@@ -53,14 +53,14 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
if show_current_asset: if show_current_asset:
self.queryset = self.queryset.filter( self.queryset = self.queryset.filter(
Q(nodes=node_id) | Q(nodes__isnull=True) Q(nodes=node_id) | Q(nodes__isnull=True)
).distinct() )
return return
if show_current_asset: if show_current_asset:
self.queryset = self.queryset.filter(nodes=node).distinct() self.queryset = self.queryset.filter(nodes=node)
else: else:
self.queryset = self.queryset.filter( self.queryset = self.queryset.filter(
nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key), nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key),
).distinct() )
def filter_admin_user_id(self): def filter_admin_user_id(self):
admin_user_id = self.request.query_params.get('admin_user_id') admin_user_id = self.request.query_params.get('admin_user_id')
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
# #
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from ..hands import IsOrgAdmin from ..hands import IsOrgAdmin
...@@ -13,14 +14,20 @@ __all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet'] ...@@ -13,14 +14,20 @@ __all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
class CommandFilterViewSet(BulkModelViewSet): class CommandFilterViewSet(BulkModelViewSet):
filter_fields = ("name",)
search_fields = filter_fields
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
queryset = CommandFilter.objects.all() queryset = CommandFilter.objects.all()
serializer_class = serializers.CommandFilterSerializer serializer_class = serializers.CommandFilterSerializer
pagination_class = LimitOffsetPagination
class CommandFilterRuleViewSet(BulkModelViewSet): class CommandFilterRuleViewSet(BulkModelViewSet):
filter_fields = ("content",)
search_fields = filter_fields
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterRuleSerializer serializer_class = serializers.CommandFilterRuleSerializer
pagination_class = LimitOffsetPagination
def get_queryset(self): def get_queryset(self):
fpk = self.kwargs.get('filter_pk') fpk = self.kwargs.get('filter_pk')
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework.views import APIView, Response from rest_framework.views import APIView, Response
from rest_framework.pagination import LimitOffsetPagination
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
...@@ -20,6 +21,11 @@ class DomainViewSet(BulkModelViewSet): ...@@ -20,6 +21,11 @@ class DomainViewSet(BulkModelViewSet):
queryset = Domain.objects.all() queryset = Domain.objects.all()
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.DomainSerializer serializer_class = serializers.DomainSerializer
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
return queryset
def get_serializer_class(self): def get_serializer_class(self):
if self.request.query_params.get('gateway'): if self.request.query_params.get('gateway'):
...@@ -33,11 +39,12 @@ class DomainViewSet(BulkModelViewSet): ...@@ -33,11 +39,12 @@ class DomainViewSet(BulkModelViewSet):
class GatewayViewSet(BulkModelViewSet): class GatewayViewSet(BulkModelViewSet):
filter_fields = ("domain",) filter_fields = ("domain__name", "name", "username", "ip", "domain")
search_fields = filter_fields search_fields = filter_fields
queryset = Gateway.objects.all() queryset = Gateway.objects.all()
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.GatewaySerializer serializer_class = serializers.GatewaySerializer
pagination_class = LimitOffsetPagination
class GatewayTestConnectionApi(SingleObjectMixin, APIView): class GatewayTestConnectionApi(SingleObjectMixin, APIView):
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
# limitations under the License. # limitations under the License.
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from django.db.models import Count from django.db.models import Count
from common.utils import get_logger from common.utils import get_logger
...@@ -27,8 +28,11 @@ __all__ = ['LabelViewSet'] ...@@ -27,8 +28,11 @@ __all__ = ['LabelViewSet']
class LabelViewSet(BulkModelViewSet): class LabelViewSet(BulkModelViewSet):
filter_fields = ("name", "value")
search_fields = filter_fields
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.LabelSerializer serializer_class = serializers.LabelSerializer
pagination_class = LimitOffsetPagination
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
if request.query_params.get("distinct"): if request.query_params.get("distinct"):
......
...@@ -42,9 +42,16 @@ class SystemUserViewSet(BulkModelViewSet): ...@@ -42,9 +42,16 @@ class SystemUserViewSet(BulkModelViewSet):
""" """
System user api set, for add,delete,update,list,retrieve resource System user api set, for add,delete,update,list,retrieve resource
""" """
filter_fields = ("name", "username")
search_fields = filter_fields
queryset = SystemUser.objects.all() queryset = SystemUser.objects.all()
serializer_class = serializers.SystemUserSerializer serializer_class = serializers.SystemUserSerializer
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
return queryset
class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-05 10:07
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='adminuser',
options={'ordering': ['name'], 'verbose_name': 'Admin user'},
),
migrations.AlterModelOptions(
name='asset',
options={'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='assetgroup',
options={'ordering': ['name'], 'verbose_name': 'Asset group'},
),
migrations.AlterModelOptions(
name='cluster',
options={'ordering': ['name'], 'verbose_name': 'Cluster'},
),
migrations.AlterModelOptions(
name='systemuser',
options={'ordering': ['name'], 'verbose_name': 'System user'},
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-09 15:31
from __future__ import unicode_literals
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0002_auto_20180105_1807'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='cluster',
field=models.ForeignKey(default=assets.models.asset.default_cluster, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='assets', to='assets.Cluster', verbose_name='Cluster'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-25 04:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0003_auto_20180109_2331'),
]
operations = [
migrations.AlterField(
model_name='assetgroup',
name='created_by',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-26 08:37
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0004_auto_20180125_1218'),
]
operations = [
migrations.CreateModel(
name='Label',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('value', models.CharField(max_length=128, verbose_name='Value')),
('category', models.CharField(choices=[('S', 'System'), ('U', 'User')], default='U', max_length=128, verbose_name='Category')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
options={
'db_table': 'assets_label',
},
),
migrations.AlterUniqueTogether(
name='label',
unique_together=set([('name', 'value')]),
),
migrations.AddField(
model_name='asset',
name='labels',
field=models.ManyToManyField(blank=True, related_name='assets', to='assets.Label', verbose_name='Labels'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-30 07:02
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0005_auto_20180126_1637'),
]
operations = [
migrations.RemoveField(
model_name='asset',
name='cabinet_no',
),
migrations.RemoveField(
model_name='asset',
name='cabinet_pos',
),
migrations.RemoveField(
model_name='asset',
name='env',
),
migrations.RemoveField(
model_name='asset',
name='remote_card_ip',
),
migrations.RemoveField(
model_name='asset',
name='status',
),
migrations.RemoveField(
model_name='asset',
name='type',
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-02-25 10:15
from __future__ import unicode_literals
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0006_auto_20180130_1502'),
]
operations = [
migrations.CreateModel(
name='Node',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('key', models.CharField(max_length=64, unique=True, verbose_name='Key')),
('value', models.CharField(max_length=128, unique=True, verbose_name='Value')),
('child_mark', models.IntegerField(default=0)),
('date_create', models.DateTimeField(auto_now_add=True)),
],
),
migrations.RemoveField(
model_name='asset',
name='cluster',
),
migrations.RemoveField(
model_name='asset',
name='groups',
),
migrations.RemoveField(
model_name='systemuser',
name='cluster',
),
migrations.AlterField(
model_name='asset',
name='admin_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.AdminUser', verbose_name='Admin user'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AddField(
model_name='asset',
name='nodes',
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
),
migrations.AddField(
model_name='systemuser',
name='nodes',
field=models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-06 10:04
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0007_auto_20180225_1815'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='systemuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-07 04:12
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0008_auto_20180306_1804'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, verbose_name='Value'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-07 09:49
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0009_auto_20180307_1212'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, unique=True, verbose_name='Value'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-26 01:57
from __future__ import unicode_literals
import assets.models.utils
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0010_auto_20180307_1749'),
]
operations = [
migrations.CreateModel(
name='Domain',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
),
migrations.CreateModel(
name='Gateway',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('username', models.CharField(max_length=128, verbose_name='Username')),
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')),
('port', models.IntegerField(default=22, verbose_name='Port')),
('protocol', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol')),
('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Domain', verbose_name='Domain')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='asset',
name='domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Domain', verbose_name='Domain'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-04 05:02
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0011_auto_20180326_0957'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Domain', verbose_name='Domain'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-11 03:35
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0012_auto_20180404_1302'),
]
operations = [
migrations.AddField(
model_name='systemuser',
name='assets',
field=models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets'),
),
migrations.AlterField(
model_name='systemuser',
name='sudo',
field=models.TextField(default='/bin/whoami', verbose_name='Sudo'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-27 04:45
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0013_auto_20180411_1135'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-05-10 04:35
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0014_auto_20180427_1245'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-05-11 04:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0015_auto_20180510_1235'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, verbose_name='Value'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-07-02 06:15
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
def migrate_win_to_ssh_protocol(apps, schema_editor):
asset_model = apps.get_model("assets", "Asset")
db_alias = schema_editor.connection.alias
asset_model.objects.using(db_alias).filter(platform__startswith='Win').update(protocol='rdp')
class Migration(migrations.Migration):
dependencies = [
('assets', '0016_auto_20180511_1203'),
]
operations = [
migrations.AddField(
model_name='asset',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=128, verbose_name='Protocol'),
),
migrations.AddField(
model_name='systemuser',
name='login_mode',
field=models.CharField(choices=[('auto', 'Automatic login'), ('manual', 'Manually login')], default='auto', max_length=10, verbose_name='Login mode'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Windows2016', 'Windows(2016)'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.RunPython(migrate_win_to_ssh_protocol),
]
# Generated by Django 2.0.7 on 2018-08-07 03:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0017_auto_20180702_1415'),
]
operations = [
migrations.AddField(
model_name='adminuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='asset',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='domain',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='gateway',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='label',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='node',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='systemuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='adminuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='asset',
name='hostname',
field=models.CharField(max_length=128, verbose_name='Hostname'),
),
migrations.AlterField(
model_name='gateway',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='systemuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='adminuser',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='asset',
unique_together={('org_id', 'hostname')},
),
migrations.AlterUniqueTogether(
name='gateway',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='systemuser',
unique_together={('name', 'org_id')},
),
]
# Generated by Django 2.0.7 on 2018-08-16 05:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0018_auto_20180807_1116'),
]
operations = [
migrations.AddField(
model_name='asset',
name='cpu_vcpus',
field=models.IntegerField(null=True, verbose_name='CPU vcpus'),
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('name', 'value', 'org_id')},
),
]
# Generated by Django 2.0.7 on 2018-08-16 08:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0019_auto_20180816_1320'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='asset',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='domain',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='gateway',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='label',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='node',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='systemuser',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
]
# Generated by Django 2.1 on 2018-09-03 03:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0020_auto_20180816_1652'),
]
operations = [
migrations.AlterModelOptions(
name='domain',
options={'verbose_name': 'Domain'},
),
migrations.AlterModelOptions(
name='gateway',
options={'verbose_name': 'Gateway'},
),
migrations.AlterModelOptions(
name='node',
options={'verbose_name': 'Node'},
),
]
# Generated by Django 2.1.1 on 2018-10-12 09:17
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0021_auto_20180903_1132'),
]
operations = [
migrations.CreateModel(
name='CommandFilter',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=64, verbose_name='Name')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(blank=True, default='', max_length=128, verbose_name='Created by')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='CommandFilterRule',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('type', models.CharField(choices=[('regex', 'Regex'), ('command', 'Command')], default='command', max_length=16, verbose_name='Type')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
('content', models.TextField(help_text='One line one command', max_length=1024, verbose_name='Content')),
('action', models.IntegerField(choices=[(0, 'Deny'), (1, 'Allow')], default=0, verbose_name='Action')),
('comment', models.CharField(blank=True, default='', max_length=64, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(blank=True, default='', max_length=128, verbose_name='Created by')),
('filter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='assets.CommandFilter', verbose_name='Filter')),
],
options={
'ordering': ('priority', 'action'),
},
),
migrations.AddField(
model_name='systemuser',
name='cmd_filters',
field=models.ManyToManyField(blank=True, related_name='system_users', to='assets.CommandFilter', verbose_name='Command filter'),
),
]
# Generated by Django 2.1.1 on 2018-10-16 08:50
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0022_auto_20181012_1717'),
]
operations = [
migrations.AlterModelOptions(
name='commandfilterrule',
options={'ordering': ('-priority', 'action')},
),
migrations.AlterField(
model_name='commandfilterrule',
name='priority',
field=models.IntegerField(default=50, help_text='1-100, the higher will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'),
),
migrations.AlterField(
model_name='systemuser',
name='priority',
field=models.IntegerField(default=20, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'),
),
]
...@@ -219,6 +219,16 @@ class Asset(OrgModelMixin): ...@@ -219,6 +219,16 @@ class Asset(OrgModelMixin):
'become': self.admin_user.become_info, 'become': self.admin_user.become_info,
} }
def as_node(self):
from .node import Node
fake_node = Node()
fake_node.id = self.id
fake_node.key = self.id
fake_node.value = self.hostname
fake_node.asset = self
fake_node.is_node = False
return fake_node
def _to_secret_json(self): def _to_secret_json(self):
""" """
Ansible use it create inventory, First using asset user, Ansible use it create inventory, First using asset user,
......
...@@ -92,7 +92,7 @@ class Node(OrgModelMixin): ...@@ -92,7 +92,7 @@ class Node(OrgModelMixin):
return child return child
def get_children(self, with_self=False): def get_children(self, with_self=False):
pattern = r'^{0}$|^{}:[0-9]+$' if with_self else r'^{}:[0-9]+$' pattern = r'^{0}$|^{0}:[0-9]+$' if with_self else r'^{0}:[0-9]+$'
return self.__class__.objects.filter( return self.__class__.objects.filter(
key__regex=pattern.format(self.key) key__regex=pattern.format(self.key)
) )
...@@ -121,10 +121,10 @@ class Node(OrgModelMixin): ...@@ -121,10 +121,10 @@ class Node(OrgModelMixin):
def get_assets(self): def get_assets(self):
from .asset import Asset from .asset import Asset
if self.is_default_node(): if self.is_default_node():
assets = Asset.objects.filter(nodes__isnull=True) assets = Asset.objects.filter(Q(nodes__id=self.id) | Q(nodes__isnull=True))
else: else:
assets = Asset.objects.filter(nodes__id=self.id) assets = Asset.objects.filter(nodes__id=self.id)
return assets return assets.distinct()
def get_valid_assets(self): def get_valid_assets(self):
return self.get_assets().valid() return self.get_assets().valid()
......
...@@ -93,7 +93,7 @@ $(document).ready(function(){ ...@@ -93,7 +93,7 @@ $(document).ready(function(){
columns: [{data: function(){return ""}}, {data: "name" }, {data: "username" }, {data: "assets_amount" }, columns: [{data: function(){return ""}}, {data: "name" }, {data: "username" }, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }] {data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }]
}; };
jumpserver.initDataTable(options); jumpserver.initServerSideDataTable(options)
}) })
.on('click', '.btn_admin_user_delete', function () { .on('click', '.btn_admin_user_delete', function () {
......
...@@ -66,7 +66,7 @@ function initTable() { ...@@ -66,7 +66,7 @@ function initTable() {
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
jumpserver.initDataTable(options); jumpserver.initServerSideDataTable(options);
} }
$(document).ready(function(){ $(document).ready(function(){
initTable(); initTable();
......
...@@ -95,7 +95,7 @@ function initTable() { ...@@ -95,7 +95,7 @@ function initTable() {
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
jumpserver.initDataTable(options); jumpserver.initServerSideDataTable(options);
} }
$(document).ready(function(){ $(document).ready(function(){
initTable(); initTable();
......
...@@ -98,7 +98,7 @@ function initTable() { ...@@ -98,7 +98,7 @@ function initTable() {
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
jumpserver.initDataTable(options); jumpserver.initServerSideDataTable(options);
} }
$(document).ready(function(){ $(document).ready(function(){
initTable(); initTable();
......
...@@ -62,7 +62,7 @@ function initTable() { ...@@ -62,7 +62,7 @@ function initTable() {
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
jumpserver.initDataTable(options); jumpserver.initServerSideDataTable(options);
} }
$(document).ready(function(){ $(document).ready(function(){
initTable(); initTable();
......
...@@ -47,7 +47,7 @@ function initTable() { ...@@ -47,7 +47,7 @@ function initTable() {
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
jumpserver.initDataTable(options); jumpserver.initServerSideDataTable(options);
} }
$(document).ready(function(){ $(document).ready(function(){
initTable(); initTable();
......
...@@ -100,7 +100,7 @@ function initTable() { ...@@ -100,7 +100,7 @@ function initTable() {
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
jumpserver.initDataTable(options); jumpserver.initServerSideDataTable(options);
} }
$(document).ready(function(){ $(document).ready(function(){
......
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
var zTree, asset_table; var zTree, asset_table, show=0;
var inited = false; var inited = false;
var url; var url;
function initTable() { function initTable() {
...@@ -102,7 +102,7 @@ function initTable() { ...@@ -102,7 +102,7 @@ function initTable() {
{data: "system_users_granted", orderable: false} {data: "system_users_granted", orderable: false}
] ]
}; };
asset_table = jumpserver.initDataTable(options); asset_table = jumpserver.initServerSideDataTable(options);
return asset_table return asset_table
} }
...@@ -183,6 +183,21 @@ $(document).ready(function () { ...@@ -183,6 +183,21 @@ $(document).ready(function () {
$('#asset_detail_tbody').html(trs) $('#asset_detail_tbody').html(trs)
}); });
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;
}
}
</script> </script>
{% endblock %} {% endblock %}
\ No newline at end of file
...@@ -277,7 +277,8 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView): ...@@ -277,7 +277,8 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
v = '' v = ''
elif k == 'domain': elif k == 'domain':
v = get_object_or_none(Domain, name=v) v = get_object_or_none(Domain, name=v)
elif k == 'platform':
v = v.lower().capitalize()
if v != '': if v != '':
asset_dict[k] = v asset_dict[k] = v
......
...@@ -36,6 +36,7 @@ class LabelCreateView(AdminUserRequiredMixin, CreateView): ...@@ -36,6 +36,7 @@ class LabelCreateView(AdminUserRequiredMixin, CreateView):
form_class = LabelForm form_class = LabelForm
success_url = reverse_lazy('assets:label-list') success_url = reverse_lazy('assets:label-list')
success_message = create_success_msg success_message = create_success_msg
disable_name = ['draw', 'search', 'limit', 'offset', '_']
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
...@@ -45,6 +46,16 @@ class LabelCreateView(AdminUserRequiredMixin, CreateView): ...@@ -45,6 +46,16 @@ class LabelCreateView(AdminUserRequiredMixin, CreateView):
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def form_valid(self, form):
name = form.cleaned_data.get('name')
if name in self.disable_name:
msg = _(
'Tips: Avoid using label names reserved internally: {}'
).format(', '.join(self.disable_name))
form.add_error("name", msg)
return self.form_invalid(form)
return super().form_valid(form)
class LabelUpdateView(AdminUserRequiredMixin, UpdateView): class LabelUpdateView(AdminUserRequiredMixin, UpdateView):
model = Label model = Label
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-06 04:30
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='FTPLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('user', models.CharField(max_length=128, verbose_name='User')),
('remote_addr', models.CharField(blank=True, max_length=15, null=True, verbose_name='Remote addr')),
('asset', models.CharField(max_length=1024, verbose_name='Asset')),
('system_user', models.CharField(max_length=128, verbose_name='System user')),
('operate', models.CharField(max_length=16, verbose_name='Operate')),
('filename', models.CharField(max_length=1024, verbose_name='Filename')),
('is_success', models.BooleanField(default=True, verbose_name='Success')),
('date_start', models.DateTimeField(auto_now_add=True)),
],
),
]
# Generated by Django 2.0.7 on 2018-08-07 03:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='ftplog',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
]
# Generated by Django 2.0.7 on 2018-08-16 08:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0002_ftplog_org_id'),
]
operations = [
migrations.AlterField(
model_name='ftplog',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
]
# Generated by Django 2.1 on 2018-09-03 03:32
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('users', '0014_auto_20180816_1652'),
('audits', '0003_auto_20180816_1652'),
]
operations = [
migrations.CreateModel(
name='OperateLog',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('user', models.CharField(max_length=128, verbose_name='User')),
('action', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action')),
('resource_type', models.CharField(max_length=64, verbose_name='Resource Type')),
('resource', models.CharField(max_length=128, verbose_name='Resource')),
('remote_addr', models.CharField(blank=True, max_length=15, null=True, verbose_name='Remote addr')),
('datetime', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PasswordChangeLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('user', models.CharField(max_length=128, verbose_name='User')),
('change_by', models.CharField(max_length=128, verbose_name='Change by')),
('remote_addr', models.CharField(blank=True, max_length=15, null=True, verbose_name='Remote addr')),
('datetime', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='UserLoginLog',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('users.loginlog',),
),
]
...@@ -160,8 +160,12 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): ...@@ -160,8 +160,12 @@ class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
return users return users
def get_queryset(self): def get_queryset(self):
users = self.get_org_users() if current_org.is_default():
queryset = super().get_queryset().filter(username__in=users) queryset = super().get_queryset()
else:
users = self.get_org_users()
queryset = super().get_queryset().filter(username__in=users)
self.user = self.request.GET.get('user', '') self.user = self.request.GET.get('user', '')
self.keyword = self.request.GET.get("keyword", '') self.keyword = self.request.GET.get("keyword", '')
......
from django.apps import AppConfig
class AuthenticationConfig(AppConfig):
name = 'authentication'
def ready(self):
from . import signals_handlers
super().ready()
# -*- coding: utf-8 -*-
#
from django.conf import settings
from .models import Client
def new_client():
"""
:return: authentication.models.Client
"""
return Client(
server_url=settings.AUTH_OPENID_SERVER_URL,
realm_name=settings.AUTH_OPENID_REALM_NAME,
client_id=settings.AUTH_OPENID_CLIENT_ID,
client_secret=settings.AUTH_OPENID_CLIENT_SECRET
)
client = new_client()
# coding:utf-8
#
from django.contrib.auth import get_user_model
from django.conf import settings
from . import client
from common.utils import get_logger
from authentication.openid.models import OIDT_ACCESS_TOKEN
UserModel = get_user_model()
logger = get_logger(__file__)
BACKEND_OPENID_AUTH_CODE = \
'authentication.openid.backends.OpenIDAuthorizationCodeBackend'
class BaseOpenIDAuthorizationBackend(object):
@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
class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend):
def authenticate(self, request, **kwargs):
logger.info('1.openid code backend')
code = kwargs.get('code')
redirect_uri = kwargs.get('redirect_uri')
if not code or not redirect_uri:
return None
try:
oidt_profile = client.update_or_create_from_code(
code=code,
redirect_uri=redirect_uri
)
except Exception as e:
logger.error(e)
else:
# Check openid user single logout or not with access_token
request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token
user = oidt_profile.user
return user if self.user_can_authenticate(user) else None
class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
logger.info('2.openid password backend')
if not settings.AUTH_OPENID:
return None
elif not username:
return None
try:
oidt_profile = client.update_or_create_from_password(
username=username, password=password
)
except Exception as e:
logger.error(e)
else:
user = oidt_profile.user
return user if self.user_can_authenticate(user) else None
# coding:utf-8
#
from django.conf import settings
from django.contrib.auth import logout
from django.utils.deprecation import MiddlewareMixin
from django.contrib.auth import BACKEND_SESSION_KEY
from . import client
from common.utils import get_logger
from .backends import BACKEND_OPENID_AUTH_CODE
from authentication.openid.models import OIDT_ACCESS_TOKEN
logger = get_logger(__file__)
class OpenIDAuthenticationMiddleware(MiddlewareMixin):
"""
Check openid user single logout (with access_token)
"""
def process_request(self, request):
# Don't need openid auth if AUTH_OPENID is False
if not settings.AUTH_OPENID:
return
# Don't need check single logout if user not authenticated
if not request.user.is_authenticated:
return
elif request.session[BACKEND_SESSION_KEY] != BACKEND_OPENID_AUTH_CODE:
return
# Check openid user single logout or not with access_token
try:
client.openid_connect_client.userinfo(
token=request.session.get(OIDT_ACCESS_TOKEN))
except Exception as e:
logout(request)
logger.error(e)
# -*- coding: utf-8 -*-
#
from django.db import transaction
from django.contrib.auth import get_user_model
from keycloak.realm import KeycloakRealm
from keycloak.keycloak_openid import KeycloakOpenID
from ..signals import post_create_openid_user
OIDT_ACCESS_TOKEN = 'oidt_access_token'
class OpenIDTokenProfile(object):
def __init__(self, user, access_token, refresh_token):
"""
:param user: User object
:param access_token:
:param refresh_token:
"""
self.user = user
self.access_token = access_token
self.refresh_token = refresh_token
def __str__(self):
return "{}'s OpenID token profile".format(self.user.username)
class Client(object):
def __init__(self, server_url, realm_name, client_id, client_secret):
self.server_url = server_url
self.realm_name = realm_name
self.client_id = client_id
self.client_secret = client_secret
self.realm = self.new_realm()
self.openid_client = self.new_openid_client()
self.openid_connect_client = self.new_openid_connect_client()
def new_realm(self):
"""
:param authentication.openid.models.Realm realm:
:return keycloak.realm.Realm:
"""
return KeycloakRealm(
server_url=self.server_url,
realm_name=self.realm_name,
headers={}
)
def new_openid_connect_client(self):
"""
:rtype: keycloak.openid_connect.KeycloakOpenidConnect
"""
openid_connect = self.realm.open_id_connect(
client_id=self.client_id,
client_secret=self.client_secret
)
return openid_connect
def new_openid_client(self):
"""
:rtype: keycloak.keycloak_openid.KeycloakOpenID
"""
return KeycloakOpenID(
server_url='%sauth/' % self.server_url,
realm_name=self.realm_name,
client_id=self.client_id,
client_secret_key=self.client_secret,
)
def update_or_create_from_password(self, username, password):
"""
Update or create an user based on an authentication username and password.
:param str username: authentication username
:param str password: authentication password
:return: authentication.models.OpenIDTokenProfile
"""
token_response = self.openid_client.token(
username=username, password=password
)
return self._update_or_create(token_response=token_response)
def update_or_create_from_code(self, code, redirect_uri):
"""
Update or create an user based on an authentication code.
Response as specified in:
https://tools.ietf.org/html/rfc6749#section-4.1.4
:param str code: authentication code
:param str redirect_uri:
:rtype: authentication.models.OpenIDTokenProfile
"""
token_response = self.openid_connect_client.authorization_code(
code=code, redirect_uri=redirect_uri)
return self._update_or_create(token_response=token_response)
def _update_or_create(self, token_response):
"""
Update or create an user based on a token response.
`token_response` contains the items returned by the OpenIDConnect Token API
end-point:
- id_token
- access_token
- expires_in
- refresh_token
- refresh_expires_in
:param dict token_response:
:rtype: authentication.openid.models.OpenIDTokenProfile
"""
userinfo = self.openid_connect_client.userinfo(
token=token_response['access_token'])
with transaction.atomic():
user, _ = get_user_model().objects.update_or_create(
username=userinfo.get('preferred_username', ''),
defaults={
'email': userinfo.get('email', ''),
'first_name': userinfo.get('given_name', ''),
'last_name': userinfo.get('family_name', '')
}
)
oidt_profile = OpenIDTokenProfile(
user=user,
access_token=token_response['access_token'],
refresh_token=token_response['refresh_token'],
)
if user:
post_create_openid_user.send(sender=user.__class__, user=user)
return oidt_profile
def __str__(self):
return self.client_id
class Nonce(object):
"""
The openid-login is stored in cache as a temporary object, recording the
user's redirect_uri and next_pat
"""
def __init__(self, redirect_uri, next_path):
import uuid
self.state = uuid.uuid4()
self.redirect_uri = redirect_uri
self.next_path = next_path
# -*- coding: utf-8 -*-
#
import logging
from django.urls import reverse
from django.conf import settings
from django.core.cache import cache
from django.views.generic.base import RedirectView
from django.contrib.auth import authenticate, login
from django.http.response import (
HttpResponseBadRequest,
HttpResponseServerError,
HttpResponseRedirect
)
from . import client
from .models import Nonce
from users.models import LoginLog
from users.tasks import write_login_log_async
from common.utils import get_request_ip
logger = logging.getLogger(__name__)
def get_base_site_url():
return settings.BASE_SITE_URL
class LoginView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
nonce = Nonce(
redirect_uri=get_base_site_url() + reverse(
"authentication:openid-login-complete"),
next_path=self.request.GET.get('next')
)
cache.set(str(nonce.state), nonce, 24*3600)
self.request.session['openid_state'] = str(nonce.state)
authorization_url = client.openid_connect_client.\
authorization_url(
redirect_uri=nonce.redirect_uri, scope='code',
state=str(nonce.state)
)
return authorization_url
class LoginCompleteView(RedirectView):
def get(self, request, *args, **kwargs):
if 'error' in request.GET:
return HttpResponseServerError(self.request.GET['error'])
if 'code' not in self.request.GET and 'state' not in self.request.GET:
return HttpResponseBadRequest()
if self.request.GET['state'] != self.request.session['openid_state']:
return HttpResponseBadRequest()
nonce = cache.get(self.request.GET['state'])
if not nonce:
return HttpResponseBadRequest()
user = authenticate(
request=self.request,
code=self.request.GET['code'],
redirect_uri=nonce.redirect_uri
)
cache.delete(str(nonce.state))
if not user:
return HttpResponseBadRequest()
login(self.request, user)
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(data)
return HttpResponseRedirect(nonce.next_path or '/')
def write_login_log(self, data):
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
'type': 'W',
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
from django.dispatch import Signal
post_create_openid_user = Signal(providing_args=('user',))
from django.http.request import QueryDict
from django.contrib.auth.signals import user_logged_out
from django.dispatch import receiver
from django.conf import settings
from .openid import client
from .signals import post_create_openid_user
@receiver(user_logged_out)
def on_user_logged_out(sender, request, user, **kwargs):
if not settings.AUTH_OPENID:
return
query = QueryDict('', mutable=True)
query.update({
'redirect_uri': settings.BASE_SITE_URL
})
openid_logout_url = "%s?%s" % (
client.openid_connect_client.get_url(
name='end_session_endpoint'),
query.urlencode()
)
request.COOKIES['next'] = openid_logout_url
@receiver(post_create_openid_user)
def on_post_create_openid_user(sender, user=None, **kwargs):
if user and user.username != 'admin':
user.source = user.SOURCE_OPENID
user.save()
# coding:utf-8
#
from django.urls import path
from authentication.openid import views
app_name = 'authentication'
urlpatterns = [
# openid
path('openid/login/', views.LoginView.as_view(), name='openid-login'),
path('openid/login/complete/', views.LoginCompleteView.as_view(),
name='openid-login-complete'),
# other
]
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import os
import json import json
import jms_storage
from rest_framework.views import Response, APIView from rest_framework.views import Response, APIView
from ldap3 import Server, Connection from ldap3 import Server, Connection
...@@ -8,8 +11,9 @@ from django.core.mail import get_connection, send_mail ...@@ -8,8 +11,9 @@ from django.core.mail import get_connection, send_mail
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from .permissions import IsOrgAdmin from .permissions import IsOrgAdmin, IsSuperUser
from .serializers import MailTestSerializer, LDAPTestSerializer from .serializers import MailTestSerializer, LDAPTestSerializer
from .models import Setting
class MailTestingAPI(APIView): class MailTestingAPI(APIView):
...@@ -85,19 +89,95 @@ class LDAPTestingAPI(APIView): ...@@ -85,19 +89,95 @@ class LDAPTestingAPI(APIView):
return Response({"error": str(serializer.errors)}, status=401) return Response({"error": str(serializer.errors)}, status=401)
class ReplayStorageCreateAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_data = request.data
if storage_data.get('TYPE') == 'ceph':
port = storage_data.get('PORT')
if port.isdigit():
storage_data['PORT'] = int(storage_data.get('PORT'))
storage_name = storage_data.pop('NAME')
data = {storage_name: storage_data}
if not self.is_valid(storage_data):
return Response({"error": _("Error: Account invalid")}, status=401)
Setting.save_storage('TERMINAL_REPLAY_STORAGE', data)
return Response({"msg": _('Create succeed')}, status=200)
@staticmethod
def is_valid(storage_data):
if storage_data.get('TYPE') == 'server':
return True
storage = jms_storage.get_object_storage(storage_data)
target = 'tests.py'
src = os.path.join(settings.BASE_DIR, 'common', target)
return storage.is_valid(src, target)
class ReplayStorageDeleteAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_name = str(request.data.get('name'))
Setting.delete_storage('TERMINAL_REPLAY_STORAGE', storage_name)
return Response({"msg": _('Delete succeed')}, status=200)
class CommandStorageCreateAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_data = request.data
storage_name = storage_data.pop('NAME')
data = {storage_name: storage_data}
if not self.is_valid(storage_data):
return Response({"error": _("Error: Account invalid")}, status=401)
Setting.save_storage('TERMINAL_COMMAND_STORAGE', data)
return Response({"msg": _('Create succeed')}, status=200)
@staticmethod
def is_valid(storage_data):
if storage_data.get('TYPE') == 'server':
return True
try:
storage = jms_storage.get_log_storage(storage_data)
except Exception:
return False
return storage.ping()
class CommandStorageDeleteAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_name = str(request.data.get('name'))
Setting.delete_storage('TERMINAL_COMMAND_STORAGE', storage_name)
return Response({"msg": _('Delete succeed')}, status=200)
class DjangoSettingsAPI(APIView): class DjangoSettingsAPI(APIView):
def get(self, request): def get(self, request):
if not settings.DEBUG: if not settings.DEBUG:
return Response("Not in debug mode") return Response("Not in debug mode")
data = {} data = {}
for k, v in settings.__dict__.items(): for i in [settings, getattr(settings, '_wrapped')]:
if k and k.isupper(): if not i:
try: continue
json.dumps(v) for k, v in i.__dict__.items():
data[k] = v if k and k.isupper():
except (json.JSONDecodeError, TypeError): try:
data[k] = str(v) json.dumps(v)
data[k] = v
except (json.JSONDecodeError, TypeError):
data[k] = str(v)
return Response(data) return Response(data)
......
...@@ -4,29 +4,28 @@ import json ...@@ -4,29 +4,28 @@ import json
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db import transaction from django.db import transaction
from django.conf import settings
from .models import Setting, common_settings from .models import Setting, settings
from .fields import FormDictField, FormEncryptCharField, \ from .fields import FormDictField, FormEncryptCharField, \
FormEncryptMixin, FormEncryptDictField FormEncryptMixin
class BaseForm(forms.Form): class BaseForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for name, field in self.fields.items(): for name, field in self.fields.items():
db_value = getattr(common_settings, name) value = getattr(settings, name, None)
django_value = getattr(settings, name) if hasattr(settings, name) else None # django_value = getattr(settings, name) if hasattr(settings, name) else None
if db_value is None and django_value is None: if value is None: # and django_value is None:
continue continue
if db_value is False or db_value: if value is not None:
if isinstance(db_value, dict): if isinstance(value, dict):
db_value = json.dumps(db_value) value = json.dumps(value)
initial_value = db_value initial_value = value
elif django_value is False or django_value: # elif django_value is False or django_value:
initial_value = django_value # initial_value = django_value
else: else:
initial_value = '' initial_value = ''
field.initial = initial_value field.initial = initial_value
...@@ -44,7 +43,7 @@ class BaseForm(forms.Form): ...@@ -44,7 +43,7 @@ class BaseForm(forms.Form):
field = self.fields[name] field = self.fields[name]
if isinstance(field.widget, forms.PasswordInput) and not value: if isinstance(field.widget, forms.PasswordInput) and not value:
continue continue
if value == getattr(common_settings, name): if value == getattr(settings, name):
continue continue
encrypted = True if isinstance(field, FormEncryptMixin) else False encrypted = True if isinstance(field, FormEncryptMixin) else False
...@@ -70,7 +69,7 @@ class BasicSettingForm(BaseForm): ...@@ -70,7 +69,7 @@ class BasicSettingForm(BaseForm):
) )
EMAIL_SUBJECT_PREFIX = forms.CharField( EMAIL_SUBJECT_PREFIX = forms.CharField(
max_length=1024, label=_("Email Subject Prefix"), max_length=1024, label=_("Email Subject Prefix"),
initial="[Jumpserver] " help_text=_("Tips: Some word will be intercept by mail provider")
) )
...@@ -98,21 +97,21 @@ class EmailSettingForm(BaseForm): ...@@ -98,21 +97,21 @@ class EmailSettingForm(BaseForm):
class LDAPSettingForm(BaseForm): class LDAPSettingForm(BaseForm):
AUTH_LDAP_SERVER_URI = forms.CharField( AUTH_LDAP_SERVER_URI = forms.CharField(
label=_("LDAP server"), initial='ldap://localhost:389' label=_("LDAP server"),
) )
AUTH_LDAP_BIND_DN = forms.CharField( AUTH_LDAP_BIND_DN = forms.CharField(
label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org' label=_("Bind DN"),
) )
AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField( AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField(
label=_("Password"), initial='', label=_("Password"),
widget=forms.PasswordInput, required=False widget=forms.PasswordInput, required=False
) )
AUTH_LDAP_SEARCH_OU = forms.CharField( AUTH_LDAP_SEARCH_OU = forms.CharField(
label=_("User OU"), initial='ou=tech,dc=jumpserver,dc=org', label=_("User OU"),
help_text=_("Use | split User OUs") help_text=_("Use | split User OUs")
) )
AUTH_LDAP_SEARCH_FILTER = forms.CharField( AUTH_LDAP_SEARCH_FILTER = forms.CharField(
label=_("User search filter"), initial='(cn=%(user)s)', label=_("User search filter"),
help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)") help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)")
) )
AUTH_LDAP_USER_ATTR_MAP = FormDictField( AUTH_LDAP_USER_ATTR_MAP = FormDictField(
...@@ -120,14 +119,14 @@ class LDAPSettingForm(BaseForm): ...@@ -120,14 +119,14 @@ class LDAPSettingForm(BaseForm):
help_text=_( help_text=_(
"User attr map present how to map LDAP user attr to jumpserver, " "User attr map present how to map LDAP user attr to jumpserver, "
"username,name,email is jumpserver attr" "username,name,email is jumpserver attr"
) ),
) )
# AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
# AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
AUTH_LDAP_START_TLS = forms.BooleanField( AUTH_LDAP_START_TLS = forms.BooleanField(
label=_("Use SSL"), initial=False, required=False label=_("Use SSL"), required=False
) )
AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), initial=False, required=False) AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), required=False)
class TerminalSettingForm(BaseForm): class TerminalSettingForm(BaseForm):
...@@ -135,32 +134,24 @@ class TerminalSettingForm(BaseForm): ...@@ -135,32 +134,24 @@ class TerminalSettingForm(BaseForm):
('hostname', _('Hostname')), ('hostname', _('Hostname')),
('ip', _('IP')), ('ip', _('IP')),
) )
TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField(
choices=SORT_BY_CHOICES, initial='hostname', label=_("List sort by")
)
TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField(
initial=5, label=_("Heartbeat interval"), help_text=_("Units: seconds")
)
TERMINAL_PASSWORD_AUTH = forms.BooleanField( TERMINAL_PASSWORD_AUTH = forms.BooleanField(
initial=True, required=False, label=_("Password auth") initial=True, required=False, label=_("Password auth")
) )
TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField( TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField(
initial=True, required=False, label=_("Public key auth") initial=True, required=False, label=_("Public key auth")
) )
TERMINAL_COMMAND_STORAGE = FormEncryptDictField( TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField(
label=_("Command storage"), help_text=_( initial=5, label=_("Heartbeat interval"), help_text=_("Units: seconds")
"Set terminal storage setting, `default` is the using as default,"
"You can set other storage and some terminal using"
)
) )
TERMINAL_REPLAY_STORAGE = FormEncryptDictField( TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField(
label=_("Replay storage"), help_text=_( choices=SORT_BY_CHOICES, initial='hostname', label=_("List sort by")
"Set replay storage setting, `default` is the using as default,"
"You can set other storage and some terminal using"
)
) )
class TerminalCommandStorage(BaseForm):
pass
class SecuritySettingForm(BaseForm): class SecuritySettingForm(BaseForm):
# MFA global setting # MFA global setting
SECURITY_MFA_AUTH = forms.BooleanField( SECURITY_MFA_AUTH = forms.BooleanField(
...@@ -181,10 +172,11 @@ class SecuritySettingForm(BaseForm): ...@@ -181,10 +172,11 @@ class SecuritySettingForm(BaseForm):
initial=30, min_value=5, initial=30, min_value=5,
label=_("No logon interval"), label=_("No logon interval"),
help_text=_( help_text=_(
"Tip :(unit/minute) if the user has failed to log in for a limited " "Tip: (unit/minute) if the user has failed to log in for a limited "
"number of times, no login is allowed during this time interval." "number of times, no login is allowed during this time interval."
) )
) )
# ssh max idle time
SECURITY_MAX_IDLE_TIME = forms.IntegerField( SECURITY_MAX_IDLE_TIME = forms.IntegerField(
initial=30, required=False, initial=30, required=False,
label=_("Connection max idle time"), label=_("Connection max idle time"),
...@@ -193,6 +185,18 @@ class SecuritySettingForm(BaseForm): ...@@ -193,6 +185,18 @@ class SecuritySettingForm(BaseForm):
'Unit: minute' 'Unit: minute'
), ),
) )
# password expiration time
SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField(
initial=9999, label=_("Password expiration time"),
min_value=1,
help_text=_(
"Tip: (unit/day) "
"If the user does not update the password during the time, "
"the user password will expire failure;"
"The password expiration reminder mail will be automatic sent to the user "
"by system within 5 days (daily) before the password expires"
)
)
# min length # min length
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField( SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
initial=6, label=_("Password minimum length"), initial=6, label=_("Password minimum length"),
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-11 05:35
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.manager
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Settings',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('value', models.TextField(verbose_name='Value')),
('enabled', models.BooleanField(default=True, verbose_name='Enabled')),
('comment', models.TextField(verbose_name='Comment')),
],
managers=[
('configs', django.db.models.manager.Manager()),
],
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-11 06:07
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('common', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='Settings',
new_name='Setting',
),
migrations.AlterModelManagers(
name='setting',
managers=[
],
),
migrations.AlterModelTable(
name='setting',
table='settings',
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-22 03:54
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0002_auto_20180111_1407'),
]
operations = [
migrations.AddField(
model_name='setting',
name='category',
field=models.CharField(default='default', max_length=128),
),
]
# Generated by Django 2.1 on 2018-09-03 03:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('common', '0003_setting_category'),
]
operations = [
migrations.AddField(
model_name='setting',
name='encrypted',
field=models.BooleanField(default=False),
),
]
...@@ -117,6 +117,3 @@ class DatetimeSearchMixin: ...@@ -117,6 +117,3 @@ class DatetimeSearchMixin:
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.get_date_range() self.get_date_range()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
import json import json
import ldap
from django.db import models from django.db import models
from django.core.cache import cache
from django.db.utils import ProgrammingError, OperationalError from django.db.utils import ProgrammingError, OperationalError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings from django.conf import settings
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from .utils import get_signer from .utils import get_signer
...@@ -40,11 +39,7 @@ class Setting(models.Model): ...@@ -40,11 +39,7 @@ class Setting(models.Model):
return self.name return self.name
def __getattr__(self, item): def __getattr__(self, item):
instances = self.__class__.objects.filter(name=item) return cache.get(item)
if len(instances) == 1:
return instances[0].cleaned_value
else:
return None
@property @property
def cleaned_value(self): def cleaned_value(self):
...@@ -67,6 +62,32 @@ class Setting(models.Model): ...@@ -67,6 +62,32 @@ class Setting(models.Model):
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise ValueError("Json dump error: {}".format(str(e))) raise ValueError("Json dump error: {}".format(str(e)))
@classmethod
def save_storage(cls, name, data):
obj = cls.objects.filter(name=name).first()
if not obj:
obj = cls()
obj.name = name
obj.encrypted = True
obj.cleaned_value = data
else:
value = obj.cleaned_value
value.update(data)
obj.cleaned_value = value
obj.save()
return obj
@classmethod
def delete_storage(cls, name, storage_name):
obj = cls.objects.filter(name=name).first()
if not obj:
return False
value = obj.cleaned_value
value.pop(storage_name, '')
obj.cleaned_value = value
obj.save()
return True
@classmethod @classmethod
def refresh_all_settings(cls): def refresh_all_settings(cls):
try: try:
...@@ -78,22 +99,15 @@ class Setting(models.Model): ...@@ -78,22 +99,15 @@ class Setting(models.Model):
def refresh_setting(self): def refresh_setting(self):
setattr(settings, self.name, self.cleaned_value) setattr(settings, self.name, self.cleaned_value)
if self.name == "AUTH_LDAP": if self.name == "AUTH_LDAP":
if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS:
settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND) old_setting = settings.AUTHENTICATION_BACKENDS
old_setting.insert(0, settings.AUTH_LDAP_BACKEND)
settings.AUTHENTICATION_BACKENDS = old_setting
elif not self.cleaned_value and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: elif not self.cleaned_value and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS:
settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND) old_setting = settings.AUTHENTICATION_BACKENDS
old_setting.remove(settings.AUTH_LDAP_BACKEND)
if self.name == "AUTH_LDAP_SEARCH_FILTER": settings.AUTHENTICATION_BACKENDS = old_setting
settings.AUTH_LDAP_USER_SEARCH_UNION = [
LDAPSearch(USER_SEARCH, ldap.SCOPE_SUBTREE, settings.AUTH_LDAP_SEARCH_FILTER)
for USER_SEARCH in str(settings.AUTH_LDAP_SEARCH_OU).split("|")
]
settings.AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(*settings.AUTH_LDAP_USER_SEARCH_UNION)
class Meta: class Meta:
db_table = "settings" db_table = "settings"
common_settings = Setting()
...@@ -5,6 +5,7 @@ from rest_framework import permissions ...@@ -5,6 +5,7 @@ from rest_framework import permissions
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import redirect from django.shortcuts import redirect
from django.http.response import HttpResponseForbidden from django.http.response import HttpResponseForbidden
from django.conf import settings
from orgs.utils import current_org from orgs.utils import current_org
...@@ -96,3 +97,12 @@ class SuperUserRequiredMixin(UserPassesTestMixin): ...@@ -96,3 +97,12 @@ class SuperUserRequiredMixin(UserPassesTestMixin):
def test_func(self): def test_func(self):
if self.request.user.is_authenticated and self.request.user.is_superuser: if self.request.user.is_authenticated and self.request.user.is_superuser:
return True return True
class WithBootstrapToken(permissions.BasePermission):
def has_permission(self, request, view):
authorization = request.META.get('HTTP_AUTHORIZATION', '')
if not authorization:
return False
request_bootstrap_token = authorization.split()[-1]
return settings.BOOTSTRAP_TOKEN == request_bootstrap_token
...@@ -4,4 +4,3 @@ ...@@ -4,4 +4,3 @@
from django.dispatch import Signal from django.dispatch import Signal
django_ready = Signal() django_ready = Signal()
ldap_auth_enable = Signal(providing_args=["enabled"])
...@@ -2,13 +2,14 @@ ...@@ -2,13 +2,14 @@
# #
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save from django.db.models.signals import post_save, pre_save
from django.conf import settings from django.conf import LazySettings, empty
from django.db.utils import ProgrammingError, OperationalError from django.db.utils import ProgrammingError, OperationalError
from django.core.cache import cache
from jumpserver.utils import current_request from jumpserver.utils import current_request
from .models import Setting from .models import Setting
from .utils import get_logger from .utils import get_logger
from .signals import django_ready, ldap_auth_enable from .signals import django_ready
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -25,25 +26,43 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs): ...@@ -25,25 +26,43 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs):
def refresh_all_settings_on_django_ready(sender, **kwargs): def refresh_all_settings_on_django_ready(sender, **kwargs):
logger.debug("Receive django ready signal") logger.debug("Receive django ready signal")
logger.debug(" - fresh all settings") logger.debug(" - fresh all settings")
CACHE_KEY_PREFIX = '_SETTING_'
def monkey_patch_getattr(self, name):
key = CACHE_KEY_PREFIX + name
cached = cache.get(key)
if cached is not None:
return cached
if self._wrapped is empty:
self._setup(name)
val = getattr(self._wrapped, name)
# self.__dict__[name] = val # Never set it
return val
def monkey_patch_setattr(self, name, value):
key = CACHE_KEY_PREFIX + name
cache.set(key, value, None)
if name == '_wrapped':
self.__dict__.clear()
else:
self.__dict__.pop(name, None)
super(LazySettings, self).__setattr__(name, value)
def monkey_patch_delattr(self, name):
super(LazySettings, self).__delattr__(name)
self.__dict__.pop(name, None)
key = CACHE_KEY_PREFIX + name
cache.delete(key)
try: try:
LazySettings.__getattr__ = monkey_patch_getattr
LazySettings.__setattr__ = monkey_patch_setattr
LazySettings.__delattr__ = monkey_patch_delattr
Setting.refresh_all_settings() Setting.refresh_all_settings()
except (ProgrammingError, OperationalError): except (ProgrammingError, OperationalError):
pass pass
@receiver(ldap_auth_enable, dispatch_uid="my_unique_identifier")
def ldap_auth_on_changed(sender, enabled=True, **kwargs):
if enabled:
logger.debug("Enable LDAP auth")
if settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS:
settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND)
else:
logger.debug("Disable LDAP auth")
if settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS:
settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND)
@receiver(pre_save, dispatch_uid="my_unique_identifier") @receiver(pre_save, dispatch_uid="my_unique_identifier")
def on_create_set_created_by(sender, instance=None, **kwargs): def on_create_set_created_by(sender, instance=None, **kwargs):
if hasattr(instance, 'created_by') and not instance.created_by: if hasattr(instance, 'created_by') and not instance.created_by:
......
...@@ -22,10 +22,6 @@ def send_mail_async(*args, **kwargs): ...@@ -22,10 +22,6 @@ def send_mail_async(*args, **kwargs):
Example: Example:
send_mail_sync.delay(subject, message, recipient_list, fail_silently=False, html_message=None) send_mail_sync.delay(subject, message, recipient_list, fail_silently=False, html_message=None)
""" """
configs = Setting.objects.filter(name__startswith='EMAIL')
for config in configs:
setattr(settings, config.name, config.cleaned_value)
if len(args) == 3: if len(args) == 3:
args = list(args) args = list(args)
args[0] = settings.EMAIL_SUBJECT_PREFIX + args[0] args[0] = settings.EMAIL_SUBJECT_PREFIX + args[0]
......
...@@ -75,32 +75,6 @@ ...@@ -75,32 +75,6 @@
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
})
.on("click", ".btn-test", function () {
var data = {};
var form = $("form").serializeArray();
$.each(form, function (i, field) {
data[field.name] = field.value;
});
var the_url = "{% url 'api-common:mail-testing' %}";
function error(message) {
toastr.error(message)
}
function success(message) {
toastr.success(message.msg)
}
APIUpdateAttr({
url: the_url,
body: JSON.stringify(data),
method: "POST",
flash_message: false,
success: success,
error: error
});
}) })
</script> </script>
{% endblock %} {% endblock %}
{#{% extends 'base.html' %}#}
{% extends '_base_create_update.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% load common_tags %}
{% 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>{{ action }}</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">
<form action="" method="POST" class="form-horizontal">
<div class="form-group">
<label class="col-md-2 control-label" for="id_type">{% trans "Type" %}</label>
<div class="col-md-9">
<select id="id_type" class="selector form-control">
<option value ="server" selected="selected">server</option>
<option value ="es">es (elasticsearch)</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="id_name">{% trans "Name" %}</label>
<div class="col-md-9">
<input id="id_name" class="form-control" type="text" name="NAME" value="">
<div class="help-block">* required</div>
<div id="id_error" style="color: red;"></div>
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_hosts">{% trans "Hosts" %}</label>
<div class="col-md-9">
<input id="id_hosts" class="form-control" type="text" name="HOSTS" value="">
<div class="help-block">{% trans 'Tips: If there are multiple hosts, separate them with a comma (,)' %}</div>
<div class="help-block">eg: http://www.jumpserver.a.com, http://www.jumpserver.b.com</div>
</div>
</div>
{# <div class="form-group" style="display: none;" >#}
{# <label class="col-md-2 control-label" for="id_other">{% trans "Other" %}</label>#}
{# <div class="col-md-9">#}
{# <input id="id_other" class="form-control" type="text" name="OTHER" value="">#}
{# </div>#}
{# </div>#}
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_bucket">{% trans "Index" %}</label>
<div class="col-md-9">
<input id="id_index" class="form-control" type="text" name="INDEX" value="jumpserver">
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_doc_type">{% trans "Doc type" %}</label>
<div class="col-md-9">
<input id="id_doc_type" class="form-control" type="text" name="DOC_TYPE" value="command_store">
</div>
</div>
<div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<a class="btn btn-primary" type="" id="id_submit_button" >{% trans 'Submit' %}</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
var field_of_all, need_get_field_of_server, need_get_field_of_es;
function showField(field){
$.each(field, function(index, value){
$(value).parent('div').parent('div').css('display', '');
});
}
function hiddenField(field){
$.each(field, function(index, value){
$(value).parent('div').parent('div').css('display', 'none');
})
}
function getFieldByType(type){
if(type === 'server'){
return need_get_field_of_server
}
else if(type === 'es'){
return need_get_field_of_es
}
}
function ajaxAPI(url, data, success, error){
$.ajax({
url: url,
data: data,
method: 'POST',
contentType: 'application/json; charset=utf-8',
success: success,
error: error
})
}
$(document).ready(function() {
var name_id = '#id_name';
var hosts_id = '#id_hosts';
{#var other_id = '#id_other';#}
var index_id = '#id_index';
var doc_type_id = '#id_doc_type';
field_of_all = [name_id, hosts_id, index_id, doc_type_id];
need_get_field_of_server = [name_id];
need_get_field_of_es = [name_id, hosts_id, index_id, doc_type_id];
})
.on('change', '.selector', function(){
var type = $('.selector').val();
console.log(type);
hiddenField(field_of_all);
var field = getFieldByType(type);
showField(field)
})
.on('click', '#id_submit_button', function(){
var type = $('.selector').val();
var field = getFieldByType(type);
var data = {'TYPE': type};
$.each(field, function(index, id_field){
var name = $(id_field).attr('name');
var value = $(id_field).val();
if(name === 'HOSTS'){
data[name] = value.split(',');
}
else{
data[name] = value
}
});
var url = "{% url 'api-common:command-storage-create' %}";
var success = function(data, textStatus) {
console.log(data, textStatus);
location = "{% url 'common:terminal-setting' %}";
};
var error = function(data, textStatus) {
var error_msg = data.responseJSON.error;
$('#id_error').html(error_msg)
};
ajaxAPI(url, JSON.stringify(data), success, error)
})
</script>
{% endblock %}
{#{% extends 'base.html' %}#}
{% extends '_base_create_update.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% load common_tags %}
{% 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>{{ action }}</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">
<form action="" method="POST" class="form-horizontal">
<div class="form-group">
<label class="col-md-2 control-label" for="id_type">{% trans "Type" %}</label>
<div class="col-md-9">
<select id="id_type" class="selector form-control">
<option value ="server" selected="selected">server</option>
<option value ="s3">s3</option>
<option value="oss">oss</option>
<option value ="azure">azure</option>
<option value="ceph">ceph</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="id_name">{% trans "Name" %}</label>
<div class="col-md-9">
<input id="id_name" class="form-control" type="text" name="NAME" value="">
<div class="help-block">* required</div>
<div id="id_error" style="color: red;"></div>
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_host">{% trans "Host" %}</label>
<div class="col-md-9">
<input id="id_host" class="form-control" type="text" name="HOSTNAME" value="" placeholder="Host">
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_port">{% trans "Port" %}</label>
<div class="col-md-9">
<input id="id_port" class="form-control" type="text" name="PORT" value="" placeholder="7480">
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_bucket">{% trans "Bucket" %}</label>
<div class="col-md-9">
<input id="id_bucket" class="form-control" type="text" name="BUCKET" value="" placeholder="Bucket">
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_access_key">{% trans "Access key" %}</label>
<div class="col-md-9">
<input id="id_access_key" class="form-control" type="text" name="ACCESS_KEY" value="" placeholder="Access key">
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_secret_key">{% trans "Secret key" %}</label>
<div class="col-md-9">
<input id="id_secret_key" class="form-control" type="text" name="SECRET_KEY" value="", placeholder="Secret key">
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_container_name">{% trans "Container name" %}</label>
<div class="col-md-9">
<input id="id_container_name" class="form-control" type="text" name="CONTAINER_NAME" value="" placeholder="Container">
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_account_name">{% trans "Account name" %}</label>
<div class="col-md-9">
<input id="id_account_name" class="form-control" type="text" name="ACCOUNT_NAME" value="" placeholder="Account name">
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_account_key">{% trans "Account key" %}</label>
<div class="col-md-9">
<input id="id_account_key" class="form-control" type="text" name="ACCOUNT_KEY" value="" placeholder="Account key">
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_endpoint">{% trans "Endpoint" %}</label>
<div class="col-md-9">
<input id="id_endpoint" class="form-control" type="text" name="ENDPOINT" value="" placeholder="Endpoint">
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_endpoint_suffix">{% trans "Endpoint suffix" %}</label>
{# <div class="col-md-9">#}
{# <input id="id_endpoint_suffix" class="form-control" type="text" name="ENDPOINT_SUFFIX" value="">#}
{# <div class="help-block">{% trans '' %}</div>#}
{# </div>#}
<div class="col-md-9">
<select id="id_endpoint_suffix" name="ENDPOINT_SUFFIX" class="endpoint-suffix-selector form-control">
<option value="core.chinacloudapi.cn" selected="selected">core.chinacloudapi.cn</option>
<option value="core.windows.net">core.windows.net</option>
</select>
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_region">{% trans "Region" %}</label>
<div class="col-md-9">
<input id="id_region" class="form-control" type="text" name="REGION" value="" placeholder="">
</div>
</div>
<div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<a class="btn btn-primary" type="" id="id_submit_button" >{% trans 'Submit' %}</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
var field_of_all, need_get_field_of_server, need_get_field_of_s3,
need_get_field_of_oss, need_get_field_of_azure, need_get_field_of_ceph;
function showField(field){
$.each(field, function(index, value){
$(value).parent('div').parent('div').css('display', '');
});
}
function hiddenField(field){
$.each(field, function(index, value){
$(value).parent('div').parent('div').css('display', 'none');
})
}
function getFieldByType(type){
if(type === 'server'){
return need_get_field_of_server
}
else if(type === 's3'){
return need_get_field_of_s3
}
else if(type === 'oss'){
return need_get_field_of_oss
}
else if(type === 'azure'){
return need_get_field_of_azure
}
else if(type === 'ceph'){
return need_get_field_of_ceph
}
}
function ajaxAPI(url, data, success, error){
$.ajax({
url: url,
data: data,
method: 'POST',
contentType: 'application/json; charset=utf-8',
success: success,
error: error
})
}
$(document).ready(function() {
var name_id = '#id_name';
var host_id = '#id_host';
var port_id = '#id_port';
var bucket_id = '#id_bucket';
var access_key_id = '#id_access_key';
var secret_key_id = '#id_secret_key';
var container_name_id = '#id_container_name';
var account_name_id = '#id_account_name';
var account_key_id = '#id_account_key';
var endpoint_id = '#id_endpoint';
var endpoint_suffix_id = '#id_endpoint_suffix';
var region_id = '#id_region';
field_of_all = [name_id, host_id, port_id, bucket_id, access_key_id, secret_key_id, container_name_id, account_name_id, account_key_id, endpoint_id, endpoint_suffix_id, region_id];
need_get_field_of_server = [name_id];
need_get_field_of_s3 = [name_id, bucket_id, access_key_id, secret_key_id, region_id];
need_get_field_of_oss = [name_id, bucket_id, access_key_id, secret_key_id, endpoint_id];
need_get_field_of_azure = [name_id, container_name_id, account_name_id, account_key_id, endpoint_suffix_id];
need_get_field_of_ceph = [name_id, host_id, port_id, bucket_id, access_key_id, secret_key_id, region_id];
})
.on('change', '.selector', function(){
var type = $('.selector').val();
console.log(type);
hiddenField(field_of_all);
var field = getFieldByType(type);
showField(field)
})
.on('click', '#id_submit_button', function(){
var type = $('.selector').val();
var field = getFieldByType(type);
var data = {'TYPE': type};
$.each(field, function(index, id_field){
var name = $(id_field).attr('name');
data[name] = $(id_field).val();
});
var url = "{% url 'api-common:replay-storage-create' %}";
var success = function(data, textStatus) {
location = "{% url 'common:terminal-setting' %}";
};
var error = function(data, textStatus) {
var error_msg = data.responseJSON.error;
$('#id_error').html(error_msg)
};
ajaxAPI(url, JSON.stringify(data), success, error)
})
</script>
{% endblock %}
...@@ -63,6 +63,14 @@ ...@@ -63,6 +63,14 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary"
type="submit">{% trans 'Submit' %}</button>
</div>
</div>
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans "Command storage" %}</h3> <h3>{% trans "Command storage" %}</h3>
...@@ -71,6 +79,7 @@ ...@@ -71,6 +79,7 @@
<tr> <tr>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Type' %}</th> <th>{% trans 'Type' %}</th>
<th>{% trans 'Action' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -78,10 +87,13 @@ ...@@ -78,10 +87,13 @@
<tr> <tr>
<td>{{ name }}</td> <td>{{ name }}</td>
<td>{{ setting.TYPE }}</td> <td>{{ setting.TYPE }}</td>
<td><a class="btn btn-xs btn-danger m-l-xs btn-del-command" data-name="{{ name }}">{% trans 'Delete' %}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<a href="{% url 'common:command-storage-create' %}" class="btn btn-primary btn-xs">{% trans 'Add' %}</a>
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<h3>{% trans "Replay storage" %}</h3> <h3>{% trans "Replay storage" %}</h3>
<table class="table table-hover " id="task-history-list-table"> <table class="table table-hover " id="task-history-list-table">
...@@ -89,6 +101,7 @@ ...@@ -89,6 +101,7 @@
<tr> <tr>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Type' %}</th> <th>{% trans 'Type' %}</th>
<th>{% trans 'Action' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -96,18 +109,14 @@ ...@@ -96,18 +109,14 @@
<tr> <tr>
<td>{{ name }}</td> <td>{{ name }}</td>
<td>{{ setting.TYPE }}</td> <td>{{ setting.TYPE }}</td>
<td><a class="btn btn-xs btn-danger m-l-xs btn-del-replay" data-name="{{ name }}">{% trans 'Delete' %}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<a href="{% url 'common:replay-storage-create' %}" class="btn btn-primary btn-xs">{% trans 'Add' %}</a>
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary"
type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form> </form>
</div> </div>
</div> </div>
...@@ -116,40 +125,63 @@ ...@@ -116,40 +125,63 @@
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
$(document).ready(function () {
})
.on("click", ".btn-test", function () {
var data = {};
var form = $("form").serializeArray();
$.each(form, function (i, field) {
data[field.name] = field.value;
});
var the_url = "{% url 'api-common:ldap-testing' %}"; function ajaxAPI(url, data, success, error, method){
$.ajax({
url: url,
data: data,
method: method,
contentType: 'application/json; charset=utf-8',
success: success,
error: error
})
}
function error(message) { function deleteStorage($this, the_url){
toastr.error(message) var name = $this.data('name');
} function doDelete(){
console.log('delete storage');
var data = {"name": name};
var method = 'POST';
var success = function(){
$this.parent().parent().remove();
toastr.success("{% trans 'Delete succeed' %}");
};
var error = function(){
toastr.error("{% trans 'Delete failed' %}");
};
ajaxAPI(the_url, JSON.stringify(data), success, error, method);
}
swal({
title: "{% trans 'Are you sure about deleting it?' %}",
text: " [" + name + "] ",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#ed5565",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: true
}, function () {
doDelete()
});
}
function success(message) { $(document).ready(function () {
toastr.success(message.msg)
}
APIUpdateAttr({ })
url: the_url, .on('click', '.btn-del-replay', function(){
body: JSON.stringify(data), var $this = $(this);
method: "POST", var the_url = "{% url 'api-common:replay-storage-delete' %}";
flash_message: false, deleteStorage($this, the_url);
success: success, })
error: error .on('click', '.btn-del-command', function() {
}); var $this = $(this);
}) var the_url = "{% url 'api-common:command-storage-delete' %}";
.on('click', '', function () { deleteStorage($this, the_url)
});
}) </script>
</script>
{% endblock %} {% endblock %}
...@@ -9,5 +9,9 @@ app_name = 'common' ...@@ -9,5 +9,9 @@ app_name = 'common'
urlpatterns = [ urlpatterns = [
path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'), path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'),
path('ldap/testing/', api.LDAPTestingAPI.as_view(), name='ldap-testing'), path('ldap/testing/', api.LDAPTestingAPI.as_view(), name='ldap-testing'),
# path('django-settings/', api.DjangoSettingsAPI.as_view(), name='django-settings'), path('terminal/replay-storage/create/', api.ReplayStorageCreateAPI.as_view(), name='replay-storage-create'),
path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'),
path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'),
path('terminal/command-storage/delete/', api.CommandStorageDeleteAPI.as_view(), name='command-storage-delete'),
path('django-settings/', api.DjangoSettingsAPI.as_view(), name='django-settings'),
] ]
...@@ -11,5 +11,7 @@ urlpatterns = [ ...@@ -11,5 +11,7 @@ urlpatterns = [
url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'), url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'),
url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'), url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'),
url(r'^terminal/$', views.TerminalSettingView.as_view(), name='terminal-setting'), url(r'^terminal/$', views.TerminalSettingView.as_view(), name='terminal-setting'),
url(r'^terminal/replay-storage/create$', views.ReplayStorageCreateView.as_view(), name='replay-storage-create'),
url(r'^terminal/command-storage/create$', views.CommandStorageCreateView.as_view(), name='command-storage-create'),
url(r'^security/$', views.SecuritySettingView.as_view(), name='security-setting'), url(r'^security/$', views.SecuritySettingView.as_view(), name='security-setting'),
] ]
...@@ -37,7 +37,8 @@ def reverse(view_name, urlconf=None, args=None, kwargs=None, ...@@ -37,7 +37,8 @@ def reverse(view_name, urlconf=None, args=None, kwargs=None,
kwargs=kwargs, current_app=current_app) kwargs=kwargs, current_app=current_app)
if external: if external:
url = settings.SITE_URL.strip('/') + url site_url = settings.SITE_URL
url = site_url.strip('/') + url
return url return url
...@@ -387,6 +388,20 @@ def get_request_ip(request): ...@@ -387,6 +388,20 @@ def get_request_ip(request):
return login_ip return login_ip
def get_command_storage_setting():
default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
value = settings.TERMINAL_COMMAND_STORAGE
value.update(default)
return value
def get_replay_storage_setting():
default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE
value = settings.TERMINAL_REPLAY_STORAGE
value.update(default)
return value
class TeeObj: class TeeObj:
origin_stdout = sys.stdout origin_stdout = sys.stdout
......
...@@ -2,12 +2,11 @@ from django.views.generic import TemplateView ...@@ -2,12 +2,11 @@ from django.views.generic import TemplateView
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib import messages from django.contrib import messages
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
TerminalSettingForm, SecuritySettingForm TerminalSettingForm, SecuritySettingForm
from common.permissions import SuperUserRequiredMixin from common.permissions import SuperUserRequiredMixin
from .signals import ldap_auth_enable from . import utils
class BasicSettingView(SuperUserRequiredMixin, TemplateView): class BasicSettingView(SuperUserRequiredMixin, TemplateView):
...@@ -27,7 +26,7 @@ class BasicSettingView(SuperUserRequiredMixin, TemplateView): ...@@ -27,7 +26,7 @@ class BasicSettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST) form = self.form_class(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
msg = _("Update setting successfully, please restart program") msg = _("Update setting successfully")
messages.success(request, msg) messages.success(request, msg)
return redirect('settings:basic-setting') return redirect('settings:basic-setting')
else: else:
...@@ -79,9 +78,7 @@ class LDAPSettingView(SuperUserRequiredMixin, TemplateView): ...@@ -79,9 +78,7 @@ class LDAPSettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST) form = self.form_class(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
if "AUTH_LDAP" in form.cleaned_data: msg = _("Update setting successfully")
ldap_auth_enable.send(sender=self.__class__, enabled=form.cleaned_data["AUTH_LDAP"])
msg = _("Update setting successfully, please restart program")
messages.success(request, msg) messages.success(request, msg)
return redirect('settings:ldap-setting') return redirect('settings:ldap-setting')
else: else:
...@@ -95,14 +92,15 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView): ...@@ -95,14 +92,15 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
template_name = "common/terminal_setting.html" template_name = "common/terminal_setting.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
command_storage = settings.TERMINAL_COMMAND_STORAGE command_storage = utils.get_command_storage_setting()
replay_storage = settings.TERMINAL_REPLAY_STORAGE replay_storage = utils.get_replay_storage_setting()
context = { context = {
'app': _('Settings'), 'app': _('Settings'),
'action': _('Terminal setting'), 'action': _('Terminal setting'),
'form': self.form_class(), 'form': self.form_class(),
'replay_storage': replay_storage, 'replay_storage': replay_storage,
'command_storage': command_storage, 'command_storage': command_storage
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
...@@ -111,7 +109,7 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView): ...@@ -111,7 +109,7 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST) form = self.form_class(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
msg = _("Update setting successfully, please restart program") msg = _("Update setting successfully")
messages.success(request, msg) messages.success(request, msg)
return redirect('settings:terminal-setting') return redirect('settings:terminal-setting')
else: else:
...@@ -120,6 +118,30 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView): ...@@ -120,6 +118,30 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
return render(request, self.template_name, context) return render(request, self.template_name, context)
class ReplayStorageCreateView(SuperUserRequiredMixin, TemplateView):
template_name = 'common/replay_storage_create.html'
def get_context_data(self, **kwargs):
context = {
'app': _('Settings'),
'action': _('Create replay storage')
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class CommandStorageCreateView(SuperUserRequiredMixin, TemplateView):
template_name = 'common/command_storage_create.html'
def get_context_data(self, **kwargs):
context = {
'app': _('Settings'),
'action': _('Create command storage')
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class SecuritySettingView(SuperUserRequiredMixin, TemplateView): class SecuritySettingView(SuperUserRequiredMixin, TemplateView):
form_class = SecuritySettingForm form_class = SecuritySettingForm
template_name = "common/security_setting.html" template_name = "common/security_setting.html"
...@@ -137,7 +159,7 @@ class SecuritySettingView(SuperUserRequiredMixin, TemplateView): ...@@ -137,7 +159,7 @@ class SecuritySettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST) form = self.form_class(request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
msg = _("Update setting successfully, please restart program") msg = _("Update setting successfully")
messages.success(request, msg) messages.success(request, msg)
return redirect('settings:security-setting') return redirect('settings:security-setting')
else: else:
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import os
import sys
import types
import errno
import json
import yaml
from importlib import import_module
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR)
def import_string(dotted_path):
try:
module_path, class_name = dotted_path.rsplit('.', 1)
except ValueError as err:
raise ImportError("%s doesn't look like a module path" % dotted_path) from err
module = import_module(module_path)
try:
return getattr(module, class_name)
except AttributeError as err:
raise ImportError('Module "%s" does not define a "%s" attribute/class' % (
module_path, class_name)
) from err
class Config(dict):
"""Works exactly like a dict but provides ways to fill it from files
or special dictionaries. There are two common patterns to populate the
config.
Either you can fill the config from a config file::
app.config.from_pyfile('yourconfig.cfg')
Or alternatively you can define the configuration options in the
module that calls :meth:`from_object` or provide an import path to
a module that should be loaded. It is also possible to tell it to
use the same module and with that provide the configuration values
just before the call::
DEBUG = True
SECRET_KEY = 'development key'
app.config.from_object(__name__)
In both cases (loading from any Python file or loading from modules),
only uppercase keys are added to the config. This makes it possible to use
lowercase values in the config file for temporary values that are not added
to the config or to define the config keys in the same file that implements
the application.
Probably the most interesting way to load configurations is from an
environment variable pointing to a file::
app.config.from_envvar('YOURAPPLICATION_SETTINGS')
In this case before launching the application you have to set this
environment variable to the file you want to use. On Linux and OS X
use the export statement::
export YOURAPPLICATION_SETTINGS='/path/to/config/file'
On windows use `set` instead.
:param root_path: path to which files are read relative from. When the
config object is created by the application, this is
the application's :attr:`~flask.Flask.root_path`.
:param defaults: an optional dictionary of default values
"""
def __init__(self, root_path=None, defaults=None):
self.defaults = defaults or {}
self.root_path = root_path
super().__init__({})
def from_envvar(self, variable_name, silent=False):
"""Loads a configuration from an environment variable pointing to
a configuration file. This is basically just a shortcut with nicer
error messages for this line of code::
app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS'])
:param variable_name: name of the environment variable
:param silent: set to ``True`` if you want silent failure for missing
files.
:return: bool. ``True`` if able to load config, ``False`` otherwise.
"""
rv = os.environ.get(variable_name)
if not rv:
if silent:
return False
raise RuntimeError('The environment variable %r is not set '
'and as such configuration could not be '
'loaded. Set this variable and make it '
'point to a configuration file' %
variable_name)
return self.from_pyfile(rv, silent=silent)
def from_pyfile(self, filename, silent=False):
"""Updates the values in the config from a Python file. This function
behaves as if the file was imported as module with the
:meth:`from_object` function.
:param filename: the filename of the config. This can either be an
absolute filename or a filename relative to the
root path.
:param silent: set to ``True`` if you want silent failure for missing
files.
.. versionadded:: 0.7
`silent` parameter.
"""
if self.root_path:
filename = os.path.join(self.root_path, filename)
d = types.ModuleType('config')
d.__file__ = filename
try:
with open(filename, mode='rb') as config_file:
exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
self.from_object(d)
return True
def from_object(self, obj):
"""Updates the values from the given object. An object can be of one
of the following two types:
- a string: in this case the object with that name will be imported
- an actual object reference: that object is used directly
Objects are usually either modules or classes. :meth:`from_object`
loads only the uppercase attributes of the module/class. A ``dict``
object will not work with :meth:`from_object` because the keys of a
``dict`` are not attributes of the ``dict`` class.
Example of module-based configuration::
app.config.from_object('yourapplication.default_config')
from yourapplication import default_config
app.config.from_object(default_config)
You should not use this function to load the actual configuration but
rather configuration defaults. The actual config should be loaded
with :meth:`from_pyfile` and ideally from a location not within the
package because the package might be installed system wide.
See :ref:`config-dev-prod` for an example of class-based configuration
using :meth:`from_object`.
:param obj: an import name or object
"""
if isinstance(obj, str):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
def from_json(self, filename, silent=False):
"""Updates the values in the config from a JSON file. This function
behaves as if the JSON object was a dictionary and passed to the
:meth:`from_mapping` function.
:param filename: the filename of the JSON file. This can either be an
absolute filename or a filename relative to the
root path.
:param silent: set to ``True`` if you want silent failure for missing
files.
.. versionadded:: 0.11
"""
if self.root_path:
filename = os.path.join(self.root_path, filename)
try:
with open(filename) as json_file:
obj = json.loads(json_file.read())
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
return self.from_mapping(obj)
def from_yaml(self, filename, silent=False):
if self.root_path:
filename = os.path.join(self.root_path, filename)
try:
with open(filename) as json_file:
obj = yaml.load(json_file)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
return self.from_mapping(obj)
def from_mapping(self, *mapping, **kwargs):
"""Updates the config like :meth:`update` ignoring items with non-upper
keys.
.. versionadded:: 0.11
"""
mappings = []
if len(mapping) == 1:
if hasattr(mapping[0], 'items'):
mappings.append(mapping[0].items())
else:
mappings.append(mapping[0])
elif len(mapping) > 1:
raise TypeError(
'expected at most 1 positional argument, got %d' % len(mapping)
)
mappings.append(kwargs.items())
for mapping in mappings:
for (key, value) in mapping:
if key.isupper():
self[key] = value
return True
def get_namespace(self, namespace, lowercase=True, trim_namespace=True):
"""Returns a dictionary containing a subset of configuration options
that match the specified namespace/prefix. Example usage::
app.config['IMAGE_STORE_TYPE'] = 'fs'
app.config['IMAGE_STORE_PATH'] = '/var/app/images'
app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com'
image_store_config = app.config.get_namespace('IMAGE_STORE_')
The resulting dictionary `image_store_config` would look like::
{
'types': 'fs',
'path': '/var/app/images',
'base_url': 'http://img.website.com'
}
This is often useful when configuration options map directly to
keyword arguments in functions or class constructors.
:param namespace: a configuration namespace
:param lowercase: a flag indicating if the keys of the resulting
dictionary should be lowercase
:param trim_namespace: a flag indicating if the keys of the resulting
dictionary should not include the namespace
.. versionadded:: 0.11
"""
rv = {}
for k, v in self.items():
if not k.startswith(namespace):
continue
if trim_namespace:
key = k[len(namespace):]
else:
key = k
if lowercase:
key = key.lower()
rv[key] = v
return rv
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self))
def __getitem__(self, item):
try:
value = super().__getitem__(item)
except KeyError:
value = None
if value is not None:
return value
value = os.environ.get(item, None)
if value is not None:
return value
return self.defaults.get(item)
def __getattr__(self, item):
return self.__getitem__(item)
defaults = {
'SECRET_KEY': '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x',
'BOOTSTRAP_TOKEN': 'PleaseChangeMe',
'DEBUG': True,
'SITE_URL': 'http://localhost',
'LOG_LEVEL': 'DEBUG',
'LOG_DIR': os.path.join(PROJECT_DIR, 'logs'),
'DB_ENGINE': 'mysql',
'DB_NAME': 'jumpserver',
'DB_HOST': '127.0.0.1',
'DB_PORT': 3306,
'DB_USER': 'root',
'DB_PASSWORD': '',
'REDIS_HOST': '127.0.0.1',
'REDIS_PORT': 6379,
'REDIS_PASSWORD': '',
'REDIS_DB_CELERY_BROKER': 3,
'REDIS_DB_CACHE': 4,
'CAPTCHA_TEST_MODE': None,
'TOKEN_EXPIRATION': 3600,
'DISPLAY_PER_PAGE': 25,
'DEFAULT_EXPIRED_YEARS': 70,
'SESSION_COOKIE_DOMAIN': None,
'CSRF_COOKIE_DOMAIN': None,
'SESSION_COOKIE_AGE': 3600 * 24,
'AUTH_OPENID': False,
}
def load_user_config():
sys.path.insert(0, PROJECT_DIR)
config = Config(PROJECT_DIR, defaults)
try:
from config import config as c
config.from_object(c)
except ImportError:
msg = """
Error: No config file found.
You can run `cp config_example.py config.py`, and edit it.
"""
raise ImportError(msg)
return config
...@@ -17,24 +17,12 @@ import ldap ...@@ -17,24 +17,12 @@ import ldap
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from django.urls import reverse_lazy from django.urls import reverse_lazy
from .conf import load_user_config
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR) PROJECT_DIR = os.path.dirname(BASE_DIR)
CONFIG = load_user_config()
sys.path.append(PROJECT_DIR)
# Import project config setting
try:
from config import config as CONFIG
except ImportError:
msg = """
Error: No config file found.
You can run `cp config_example.py config.py`, and edit it.
"""
raise ImportError(msg)
# CONFIG = type('_', (), {'__getattr__': lambda arg1, arg2: None})()
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
...@@ -42,16 +30,19 @@ except ImportError: ...@@ -42,16 +30,19 @@ except ImportError:
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = CONFIG.SECRET_KEY SECRET_KEY = CONFIG.SECRET_KEY
# SECURITY WARNING: keep the token secret, remove it if all coco, guacamole ok
BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.DEBUG or False DEBUG = CONFIG.DEBUG
# Absolute url for some case, for example email link # Absolute url for some case, for example email link
SITE_URL = CONFIG.SITE_URL or 'http://localhost' SITE_URL = CONFIG.SITE_URL
# LOG LEVEL # LOG LEVEL
LOG_LEVEL = 'DEBUG' if DEBUG else CONFIG.LOG_LEVEL or 'WARNING' LOG_LEVEL = CONFIG.LOG_LEVEL
ALLOWED_HOSTS = CONFIG.ALLOWED_HOSTS or [] ALLOWED_HOSTS = ['*']
# Application definition # Application definition
...@@ -64,6 +55,7 @@ INSTALLED_APPS = [ ...@@ -64,6 +55,7 @@ INSTALLED_APPS = [
'common.apps.CommonConfig', 'common.apps.CommonConfig',
'terminal.apps.TerminalConfig', 'terminal.apps.TerminalConfig',
'audits.apps.AuditsConfig', 'audits.apps.AuditsConfig',
'authentication.apps.AuthenticationConfig', # authentication
'rest_framework', 'rest_framework',
'rest_framework_swagger', 'rest_framework_swagger',
'drf_yasg', 'drf_yasg',
...@@ -94,6 +86,7 @@ MIDDLEWARE = [ ...@@ -94,6 +86,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'authentication.openid.middleware.OpenIDAuthenticationMiddleware', # openid
'jumpserver.middleware.TimezoneMiddleware', 'jumpserver.middleware.TimezoneMiddleware',
'jumpserver.middleware.DemoMiddleware', 'jumpserver.middleware.DemoMiddleware',
'jumpserver.middleware.RequestMiddleware', 'jumpserver.middleware.RequestMiddleware',
...@@ -150,9 +143,9 @@ TEMPLATES = [ ...@@ -150,9 +143,9 @@ TEMPLATES = [
LOGIN_REDIRECT_URL = reverse_lazy('index') LOGIN_REDIRECT_URL = reverse_lazy('index')
LOGIN_URL = reverse_lazy('users:login') LOGIN_URL = reverse_lazy('users:login')
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN or None SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN or None CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN
SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE or 3600 * 24 SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
# Database # Database
...@@ -315,13 +308,13 @@ MEDIA_ROOT = os.path.join(PROJECT_DIR, 'data', 'media').replace('\\', '/') + '/' ...@@ -315,13 +308,13 @@ MEDIA_ROOT = os.path.join(PROJECT_DIR, 'data', 'media').replace('\\', '/') + '/'
FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ] FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ]
# Email config # Email config
EMAIL_HOST = CONFIG.EMAIL_HOST EMAIL_HOST = 'smtp.jumpserver.org'
EMAIL_PORT = CONFIG.EMAIL_PORT EMAIL_PORT = 25
EMAIL_HOST_USER = CONFIG.EMAIL_HOST_USER EMAIL_HOST_USER = 'noreply@jumpserver.org'
EMAIL_HOST_PASSWORD = CONFIG.EMAIL_HOST_PASSWORD EMAIL_HOST_PASSWORD = ''
EMAIL_USE_SSL = CONFIG.EMAIL_USE_SSL EMAIL_USE_SSL = False
EMAIL_USE_TLS = CONFIG.EMAIL_USE_TLS EMAIL_USE_TLS = False
EMAIL_SUBJECT_PREFIX = CONFIG.EMAIL_SUBJECT_PREFIX or '' EMAIL_SUBJECT_PREFIX = '[JMS] '
REST_FRAMEWORK = { REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions, # Use Django's standard `django.contrib.auth` permissions,
...@@ -361,23 +354,23 @@ FILE_UPLOAD_PERMISSIONS = 0o644 ...@@ -361,23 +354,23 @@ FILE_UPLOAD_PERMISSIONS = 0o644
FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755 FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
# Auth LDAP settings # Auth LDAP settings
AUTH_LDAP = CONFIG.AUTH_LDAP AUTH_LDAP = False
AUTH_LDAP_SERVER_URI = CONFIG.AUTH_LDAP_SERVER_URI AUTH_LDAP_SERVER_URI = 'ldap://localhost:389'
AUTH_LDAP_BIND_DN = CONFIG.AUTH_LDAP_BIND_DN AUTH_LDAP_BIND_DN = 'cn=admin,dc=jumpserver,dc=org'
AUTH_LDAP_BIND_PASSWORD = CONFIG.AUTH_LDAP_BIND_PASSWORD AUTH_LDAP_BIND_PASSWORD = ''
AUTH_LDAP_SEARCH_OU = CONFIG.AUTH_LDAP_SEARCH_OU AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org'
AUTH_LDAP_SEARCH_FILTER = CONFIG.AUTH_LDAP_SEARCH_FILTER AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)'
AUTH_LDAP_START_TLS = CONFIG.AUTH_LDAP_START_TLS AUTH_LDAP_START_TLS = False
AUTH_LDAP_USER_ATTR_MAP = CONFIG.AUTH_LDAP_USER_ATTR_MAP AUTH_LDAP_USER_ATTR_MAP = {"username": "cn", "name": "sn", "email": "mail"}
AUTH_LDAP_USER_SEARCH_UNION = [ AUTH_LDAP_USER_SEARCH_UNION = lambda: [
LDAPSearch(USER_SEARCH, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER) LDAPSearch(USER_SEARCH, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER)
for USER_SEARCH in str(AUTH_LDAP_SEARCH_OU).split("|") for USER_SEARCH in str(AUTH_LDAP_SEARCH_OU).split("|")
] ]
AUTH_LDAP_USER_SEARCH = LDAPSearchUnion(*AUTH_LDAP_USER_SEARCH_UNION) AUTH_LDAP_USER_SEARCH = lambda: LDAPSearchUnion(*AUTH_LDAP_USER_SEARCH_UNION())
AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
AUTH_LDAP_GROUP_SEARCH = LDAPSearch( AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER
) )
AUTH_LDAP_CONNECTION_OPTIONS = { AUTH_LDAP_CONNECTION_OPTIONS = {
ldap.OPT_TIMEOUT: 5 ldap.OPT_TIMEOUT: 5
...@@ -389,12 +382,30 @@ AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend' ...@@ -389,12 +382,30 @@ AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend'
if AUTH_LDAP: if AUTH_LDAP:
AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND) AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND)
# openid
# Auth OpenID settings
BASE_SITE_URL = CONFIG.BASE_SITE_URL
AUTH_OPENID = CONFIG.AUTH_OPENID
AUTH_OPENID_SERVER_URL = CONFIG.AUTH_OPENID_SERVER_URL
AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME
AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID
AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET
AUTH_OPENID_BACKENDS = [
'authentication.openid.backends.OpenIDAuthorizationPasswordBackend',
'authentication.openid.backends.OpenIDAuthorizationCodeBackend',
]
if AUTH_OPENID:
LOGIN_URL = reverse_lazy("authentication:openid-login")
AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[0])
AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[1])
# Celery using redis as broker # Celery using redis as broker
CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '', 'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '',
'host': CONFIG.REDIS_HOST or '127.0.0.1', 'host': CONFIG.REDIS_HOST or '127.0.0.1',
'port': CONFIG.REDIS_PORT or 6379, 'port': CONFIG.REDIS_PORT or 6379,
'db':CONFIG.REDIS_DB_CELERY_BROKER or 3, 'db': CONFIG.REDIS_DB_CELERY_BROKER or 3,
} }
CELERY_TASK_SERIALIZER = 'pickle' CELERY_TASK_SERIALIZER = 'pickle'
CELERY_RESULT_SERIALIZER = 'pickle' CELERY_RESULT_SERIALIZER = 'pickle'
...@@ -416,10 +427,10 @@ CACHES = { ...@@ -416,10 +427,10 @@ CACHES = {
'default': { 'default': {
'BACKEND': 'redis_cache.RedisCache', 'BACKEND': 'redis_cache.RedisCache',
'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { 'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '', 'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST or '127.0.0.1', 'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT or 6379, 'port': CONFIG.REDIS_PORT,
'db': CONFIG.REDIS_DB_CACHE or 4, 'db': CONFIG.REDIS_DB_CACHE,
} }
} }
} }
...@@ -434,27 +445,45 @@ COMMAND_STORAGE = { ...@@ -434,27 +445,45 @@ COMMAND_STORAGE = {
'ENGINE': 'terminal.backends.command.db', 'ENGINE': 'terminal.backends.command.db',
} }
TERMINAL_COMMAND_STORAGE = { DEFAULT_TERMINAL_COMMAND_STORAGE = {
"default": { "default": {
"TYPE": "server", "TYPE": "server",
}, },
}
TERMINAL_COMMAND_STORAGE = {
# 'ali-es': { # 'ali-es': {
# 'TYPE': 'elasticsearch', # 'TYPE': 'elasticsearch',
# 'HOSTS': ['http://elastic:changeme@localhost:9200'], # 'HOSTS': ['http://elastic:changeme@localhost:9200'],
# }, # },
} }
TERMINAL_REPLAY_STORAGE = { DEFAULT_TERMINAL_REPLAY_STORAGE = {
"default": { "default": {
"TYPE": "server", "TYPE": "server",
}, },
} }
TERMINAL_REPLAY_STORAGE = {
}
DEFAULT_PASSWORD_MIN_LENGTH = 6 SECURITY_MFA_AUTH = False
DEFAULT_LOGIN_LIMIT_COUNT = 7 SECURITY_LOGIN_LIMIT_COUNT = 7
DEFAULT_LOGIN_LIMIT_TIME = 30 # Unit: minute SECURITY_LOGIN_LIMIT_TIME = 30 # Unit: minute
DEFAULT_SECURITY_MAX_IDLE_TIME = 30 # Unit: minute SECURITY_MAX_IDLE_TIME = 30 # Unit: minute
SECURITY_PASSWORD_EXPIRATION_TIME = 9999 # Unit: day
SECURITY_PASSWORD_MIN_LENGTH = 6 # Unit: bit
SECURITY_PASSWORD_UPPER_CASE = False
SECURITY_PASSWORD_LOWER_CASE = False
SECURITY_PASSWORD_NUMBER = False
SECURITY_PASSWORD_SPECIAL_CHAR = False
SECURITY_PASSWORD_RULES = [
'SECURITY_PASSWORD_MIN_LENGTH',
'SECURITY_PASSWORD_UPPER_CASE',
'SECURITY_PASSWORD_LOWER_CASE',
'SECURITY_PASSWORD_NUMBER',
'SECURITY_PASSWORD_SPECIAL_CHAR'
]
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3 = { BOOTSTRAP3 = {
...@@ -466,16 +495,18 @@ BOOTSTRAP3 = { ...@@ -466,16 +495,18 @@ BOOTSTRAP3 = {
'success_css_class': '', 'success_css_class': '',
} }
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION or 3600 TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE or 25 DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE
DEFAULT_EXPIRED_YEARS = 70 DEFAULT_EXPIRED_YEARS = 70
USER_GUIDE_URL = "" USER_GUIDE_URL = ""
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'DEFAULT_AUTO_SCHEMA_CLASS': 'jumpserver.swagger.CustomSwaggerAutoSchema',
'SECURITY_DEFINITIONS': { 'SECURITY_DEFINITIONS': {
'basic': { 'basic': {
'type': 'basic' 'type': 'basic'
} }
}, },
} }
from drf_yasg.inspectors import SwaggerAutoSchema
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
class CustomSwaggerAutoSchema(SwaggerAutoSchema):
def get_tags(self, operation_keys):
if len(operation_keys) > 2 and operation_keys[1].startswith('v'):
return [operation_keys[2]]
return super().get_tags(operation_keys)
def get_swagger_view(version='v1'):
from .urls import api_v1_patterns, api_v2_patterns
if version == "v2":
patterns = api_v2_patterns
else:
patterns = api_v1_patterns
schema_view = get_schema_view(
openapi.Info(
title="Jumpserver API Docs",
default_version=version,
description="Jumpserver Restful api docs",
terms_of_service="https://www.jumpserver.org",
contact=openapi.Contact(email="support@fit2cloud.com"),
license=openapi.License(name="GPLv2 License"),
),
public=True,
patterns=patterns,
permission_classes=(permissions.AllowAny,),
)
return schema_view
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals from __future__ import unicode_literals
import re
import os
from django.urls import path, include, re_path from django.urls import path, include, re_path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from django.views.i18n import JavaScriptCatalog from django.views.i18n import JavaScriptCatalog
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from django.utils.encoding import iri_to_uri
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from .views import IndexView, LunaView, I18NView from .views import IndexView, LunaView, I18NView
from .swagger import get_swagger_view
api_v1_patterns = [
path('api/', include([
path('users/v1/', include('users.urls.api_urls', namespace='api-users')),
path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')),
path('terminal/v1/', include('terminal.urls.api_urls', namespace='api-terminal')),
path('ops/v1/', include('ops.urls.api_urls', namespace='api-ops')),
path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('common/v1/', include('common.urls.api_urls', namespace='api-common')),
]))
]
schema_view = get_schema_view( api_v2_patterns = [
openapi.Info( path('api/', include([
title="Jumpserver API Docs", path('terminal/v2/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')),
default_version='v1', path('users/v2/', include('users.urls.api_urls_v2', namespace='api-users-v2')),
description="Jumpserver Restful api docs", ]))
terms_of_service="https://www.jumpserver.org",
contact=openapi.Contact(email="support@fit2cloud.com"),
license=openapi.License(name="GPLv2 License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
api_url_pattern = re.compile(r'^/api/(?P<version>\w+)/(?P<app>\w+)/(?P<extra>.*)$')
class HttpResponseTemporaryRedirect(HttpResponse):
status_code = 307
def __init__(self, redirect_to):
HttpResponse.__init__(self)
self['Location'] = iri_to_uri(redirect_to)
@csrf_exempt
def redirect_format_api(request, *args, **kwargs):
_path, query = request.path, request.GET.urlencode()
matched = api_url_pattern.match(_path)
if matched:
version, app, extra = matched.groups()
_path = '/api/{app}/{version}/{extra}?{query}'.format(**{
"app": app, "version": version, "extra": extra,
"query": query
})
return HttpResponseTemporaryRedirect(_path)
else:
return Response({"msg": "Redirect url failed: {}".format(_path)}, status=404)
v1_api_patterns = [
path('users/v1/', include('users.urls.api_urls', namespace='api-users')),
path('assets/v1/', include('assets.urls.api_urls', namespace='api-assets')),
path('perms/v1/', include('perms.urls.api_urls', namespace='api-perms')),
path('terminal/v1/', include('terminal.urls.api_urls', namespace='api-terminal')),
path('ops/v1/', include('ops.urls.api_urls', namespace='api-ops')),
path('audits/v1/', include('audits.urls.api_urls', namespace='api-audits')),
path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('common/v1/', include('common.urls.api_urls', namespace='api-common')),
] ]
app_view_patterns = [ app_view_patterns = [
...@@ -75,8 +39,10 @@ app_view_patterns = [ ...@@ -75,8 +39,10 @@ app_view_patterns = [
path('ops/', include('ops.urls.view_urls', namespace='ops')), path('ops/', include('ops.urls.view_urls', namespace='ops')),
path('audits/', include('audits.urls.view_urls', namespace='audits')), path('audits/', include('audits.urls.view_urls', namespace='audits')),
path('orgs/', include('orgs.urls.views_urls', namespace='orgs')), path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
path('auth/', include('authentication.urls.view_urls'), name='auth'),
] ]
if settings.XPACK_ENABLED: if settings.XPACK_ENABLED:
app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack'))) app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack')))
...@@ -86,12 +52,13 @@ js_i18n_patterns = i18n_patterns( ...@@ -86,12 +52,13 @@ js_i18n_patterns = i18n_patterns(
urlpatterns = [ urlpatterns = [
path('', IndexView.as_view(), name='index'), path('', IndexView.as_view(), name='index'),
path('', include(api_v2_patterns)),
path('', include(api_v1_patterns)),
path('luna/', LunaView.as_view(), name='luna-error'), path('luna/', LunaView.as_view(), name='luna-error'),
path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'), path('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'),
path('settings/', include('common.urls.view_urls', namespace='settings')), path('settings/', include('common.urls.view_urls', namespace='settings')),
path('common/', include('common.urls.view_urls', namespace='common')), path('common/', include('common.urls.view_urls', namespace='common')),
path('api/v1/', redirect_format_api), # path('api/v2/', include(api_v2_patterns)),
path('api/', include(v1_api_patterns)),
# External apps url # External apps url
path('captcha/', include('captcha.urls')), path('captcha/', include('captcha.urls')),
...@@ -103,7 +70,13 @@ urlpatterns += js_i18n_patterns ...@@ -103,7 +70,13 @@ urlpatterns += js_i18n_patterns
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [ urlpatterns += [
re_path('swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'), re_path('^swagger(?P<format>\.json|\.yaml)$',
path('docs/', schema_view.with_ui('swagger', cache_timeout=None), name="docs"), get_swagger_view().without_ui(cache_timeout=1), name='schema-json'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='redoc'), path('docs/', get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"),
path('redoc/', get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'),
re_path('^v2/swagger(?P<format>\.json|\.yaml)$',
get_swagger_view().without_ui(cache_timeout=1), name='schema-json'),
path('docs/v2/', get_swagger_view("v2").with_ui('swagger', cache_timeout=1), name="docs"),
path('redoc/v2/', get_swagger_view("v2").with_ui('redoc', cache_timeout=1), name='redoc'),
] ]
import datetime import datetime
import re
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.conf import settings from django.conf import settings
...@@ -8,6 +9,10 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -8,6 +9,10 @@ from django.utils.translation import ugettext_lazy as _
from django.db.models import Count from django.db.models import Count
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from django.utils.encoding import iri_to_uri
from users.models import User from users.models import User
from assets.models import Asset from assets.models import Asset
...@@ -188,3 +193,29 @@ class I18NView(View): ...@@ -188,3 +193,29 @@ class I18NView(View):
response = HttpResponseRedirect(referer_url) response = HttpResponseRedirect(referer_url)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang) response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang)
return response return response
api_url_pattern = re.compile(r'^/api/(?P<version>\w+)/(?P<app>\w+)/(?P<extra>.*)$')
class HttpResponseTemporaryRedirect(HttpResponse):
status_code = 307
def __init__(self, redirect_to):
HttpResponse.__init__(self)
self['Location'] = iri_to_uri(redirect_to)
@csrf_exempt
def redirect_format_api(request, *args, **kwargs):
_path, query = request.path, request.GET.urlencode()
matched = api_url_pattern.match(_path)
if matched:
version, app, extra = matched.groups()
_path = '/api/{app}/{version}/{extra}?{query}'.format(**{
"app": app, "version": version, "extra": extra,
"query": query
})
return HttpResponseTemporaryRedirect(_path)
else:
return Response({"msg": "Redirect url failed: {}".format(_path)}, status=404)
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\n" "Project-Id-Version: Jumpserver 0.3.3\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-16 16:03+0800\n" "POT-Creation-Date: 2018-11-23 16:19+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n" "Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: Jumpserver team<ibuler@qq.com>\n" "Language-Team: Jumpserver team<ibuler@qq.com>\n"
...@@ -34,8 +34,8 @@ msgid "Test if the assets under the node are connectable: {}" ...@@ -34,8 +34,8 @@ msgid "Test if the assets under the node are connectable: {}"
msgstr "测试节点下资产是否可连接: {}" msgstr "测试节点下资产是否可连接: {}"
#: assets/forms/asset.py:27 assets/models/asset.py:83 assets/models/user.py:113 #: assets/forms/asset.py:27 assets/models/asset.py:83 assets/models/user.py:113
#: assets/templates/assets/asset_detail.html:183 #: assets/templates/assets/asset_detail.html:187
#: assets/templates/assets/asset_detail.html:191 #: assets/templates/assets/asset_detail.html:195
#: assets/templates/assets/system_user_asset.html:95 perms/models.py:32 #: assets/templates/assets/system_user_asset.html:95 perms/models.py:32
msgid "Nodes" msgid "Nodes"
msgstr "节点管理" msgstr "节点管理"
...@@ -43,7 +43,7 @@ msgstr "节点管理" ...@@ -43,7 +43,7 @@ msgstr "节点管理"
#: assets/forms/asset.py:30 assets/forms/asset.py:69 assets/forms/asset.py:112 #: assets/forms/asset.py:30 assets/forms/asset.py:69 assets/forms/asset.py:112
#: assets/forms/asset.py:116 assets/models/asset.py:88 #: assets/forms/asset.py:116 assets/models/asset.py:88
#: assets/models/cluster.py:19 assets/models/user.py:73 #: assets/models/cluster.py:19 assets/models/user.py:73
#: assets/templates/assets/asset_detail.html:73 templates/_nav.html:24 #: assets/templates/assets/asset_detail.html:77 templates/_nav.html:24
#: xpack/plugins/cloud/models.py:137 #: xpack/plugins/cloud/models.py:137
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:67 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:67
#: xpack/plugins/orgs/templates/orgs/org_list.html:18 #: xpack/plugins/orgs/templates/orgs/org_list.html:18
...@@ -110,11 +110,12 @@ msgstr "选择资产" ...@@ -110,11 +110,12 @@ msgstr "选择资产"
#: assets/templates/assets/domain_gateway_list.html:58 #: assets/templates/assets/domain_gateway_list.html:58
#: assets/templates/assets/system_user_asset.html:52 #: assets/templates/assets/system_user_asset.html:52
#: assets/templates/assets/user_asset_list.html:163 #: assets/templates/assets/user_asset_list.html:163
#: common/templates/common/replay_storage_create.html:60
msgid "Port" msgid "Port"
msgstr "端口" msgstr "端口"
#: assets/forms/domain.py:15 assets/forms/label.py:13 #: assets/forms/domain.py:15 assets/forms/label.py:13
#: assets/models/asset.py:243 assets/templates/assets/admin_user_list.html:28 #: assets/models/asset.py:253 assets/templates/assets/admin_user_list.html:28
#: assets/templates/assets/domain_detail.html:60 #: assets/templates/assets/domain_detail.html:60
#: assets/templates/assets/domain_list.html:26 #: assets/templates/assets/domain_list.html:26
#: assets/templates/assets/label_list.html:16 #: assets/templates/assets/label_list.html:16
...@@ -125,7 +126,7 @@ msgstr "端口" ...@@ -125,7 +126,7 @@ msgstr "端口"
#: perms/templates/perms/asset_permission_create_update.html:40 #: perms/templates/perms/asset_permission_create_update.html:40
#: perms/templates/perms/asset_permission_list.html:56 #: perms/templates/perms/asset_permission_list.html:56
#: perms/templates/perms/asset_permission_list.html:148 #: perms/templates/perms/asset_permission_list.html:148
#: terminal/backends/command/models.py:13 terminal/models.py:133 #: terminal/backends/command/models.py:13 terminal/models.py:138
#: terminal/templates/terminal/command_list.html:40 #: terminal/templates/terminal/command_list.html:40
#: terminal/templates/terminal/command_list.html:73 #: terminal/templates/terminal/command_list.html:73
#: terminal/templates/terminal/session_list.html:41 #: terminal/templates/terminal/session_list.html:41
...@@ -153,17 +154,19 @@ msgstr "不能包含特殊字符" ...@@ -153,17 +154,19 @@ msgstr "不能包含特殊字符"
#: assets/templates/assets/domain_list.html:25 #: assets/templates/assets/domain_list.html:25
#: assets/templates/assets/label_list.html:14 #: assets/templates/assets/label_list.html:14
#: assets/templates/assets/system_user_detail.html:58 #: assets/templates/assets/system_user_detail.html:58
#: assets/templates/assets/system_user_list.html:29 common/models.py:30 #: assets/templates/assets/system_user_list.html:29 common/models.py:29
#: common/templates/common/terminal_setting.html:72 #: common/templates/common/command_storage_create.html:41
#: common/templates/common/terminal_setting.html:90 ops/models/adhoc.py:37 #: common/templates/common/replay_storage_create.html:44
#: common/templates/common/terminal_setting.html:80
#: common/templates/common/terminal_setting.html:102 ops/models/adhoc.py:37
#: ops/templates/ops/task_detail.html:59 ops/templates/ops/task_list.html:35 #: ops/templates/ops/task_detail.html:59 ops/templates/ops/task_list.html:35
#: orgs/models.py:12 perms/models.py:28 #: orgs/models.py:12 perms/models.py:28
#: perms/templates/perms/asset_permission_detail.html:62 #: perms/templates/perms/asset_permission_detail.html:62
#: perms/templates/perms/asset_permission_list.html:53 #: perms/templates/perms/asset_permission_list.html:53
#: perms/templates/perms/asset_permission_user.html:54 terminal/models.py:18 #: perms/templates/perms/asset_permission_user.html:54 terminal/models.py:18
#: terminal/models.py:160 terminal/templates/terminal/terminal_detail.html:43 #: terminal/models.py:165 terminal/templates/terminal/terminal_detail.html:43
#: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 #: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14
#: users/models/user.py:51 users/templates/users/_select_user_modal.html:13 #: users/models/user.py:53 users/templates/users/_select_user_modal.html:13
#: users/templates/users/user_detail.html:63 #: users/templates/users/user_detail.html:63
#: users/templates/users/user_group_detail.html:55 #: users/templates/users/user_group_detail.html:55
#: users/templates/users/user_group_list.html:12 #: users/templates/users/user_group_list.html:12
...@@ -188,9 +191,9 @@ msgstr "名称" ...@@ -188,9 +191,9 @@ msgstr "名称"
#: assets/templates/assets/system_user_list.html:30 #: assets/templates/assets/system_user_list.html:30
#: audits/templates/audits/login_log_list.html:49 #: audits/templates/audits/login_log_list.html:49
#: perms/templates/perms/asset_permission_user.html:55 users/forms.py:15 #: perms/templates/perms/asset_permission_user.html:55 users/forms.py:15
#: users/forms.py:33 users/models/authentication.py:70 users/models/user.py:49 #: users/forms.py:33 users/models/authentication.py:77 users/models/user.py:51
#: users/templates/users/_select_user_modal.html:14 #: users/templates/users/_select_user_modal.html:14
#: users/templates/users/login.html:62 #: users/templates/users/login.html:64
#: users/templates/users/user_detail.html:67 #: users/templates/users/user_detail.html:67
#: users/templates/users/user_list.html:24 #: users/templates/users/user_list.html:24
#: users/templates/users/user_profile.html:47 #: users/templates/users/user_profile.html:47
...@@ -201,9 +204,9 @@ msgstr "用户名" ...@@ -201,9 +204,9 @@ msgstr "用户名"
msgid "Password or private key passphrase" msgid "Password or private key passphrase"
msgstr "密码或密钥密码" msgstr "密码或密钥密码"
#: assets/forms/user.py:26 assets/models/base.py:24 common/forms.py:107 #: assets/forms/user.py:26 assets/models/base.py:24 common/forms.py:106
#: users/forms.py:17 users/forms.py:35 users/forms.py:47 #: users/forms.py:17 users/forms.py:35 users/forms.py:47
#: users/templates/users/login.html:65 #: users/templates/users/login.html:67
#: users/templates/users/reset_password.html:53 #: users/templates/users/reset_password.html:53
#: users/templates/users/user_create.html:10 #: users/templates/users/user_create.html:10
#: users/templates/users/user_password_authentication.html:18 #: users/templates/users/user_password_authentication.html:18
...@@ -213,7 +216,7 @@ msgstr "密码或密钥密码" ...@@ -213,7 +216,7 @@ msgstr "密码或密钥密码"
msgid "Password" msgid "Password"
msgstr "密码" msgstr "密码"
#: assets/forms/user.py:29 users/models/user.py:78 #: assets/forms/user.py:29 users/models/user.py:80
msgid "Private key" msgid "Private key"
msgstr "ssh私钥" msgstr "ssh私钥"
...@@ -243,7 +246,9 @@ msgstr "自动推送系统用户到资产" ...@@ -243,7 +246,9 @@ msgstr "自动推送系统用户到资产"
msgid "" msgid ""
"1-100, High level will be using login asset as default, if user was granted " "1-100, High level will be using login asset as default, if user was granted "
"more than 2 system user" "more than 2 system user"
msgstr "1-100, 1最低优先级,100最高优先级。授权多个用户时,高优先级的系统用户将会作为默认登录用户" msgstr ""
"1-100, 1最低优先级,100最高优先级。授权多个用户时,高优先级的系统用户将会作为"
"默认登录用户"
#: assets/forms/user.py:155 #: assets/forms/user.py:155
msgid "" msgid ""
...@@ -260,7 +265,7 @@ msgstr "如果选择手动登录模式,用户名和密码可以不填写" ...@@ -260,7 +265,7 @@ msgstr "如果选择手动登录模式,用户名和密码可以不填写"
#: assets/templates/assets/system_user_asset.html:51 #: assets/templates/assets/system_user_asset.html:51
#: assets/templates/assets/user_asset_list.html:46 #: assets/templates/assets/user_asset_list.html:46
#: assets/templates/assets/user_asset_list.html:162 #: assets/templates/assets/user_asset_list.html:162
#: audits/templates/audits/login_log_list.html:52 common/forms.py:136 #: audits/templates/audits/login_log_list.html:52 common/forms.py:135
#: perms/templates/perms/asset_permission_asset.html:55 #: perms/templates/perms/asset_permission_asset.html:55
#: users/templates/users/user_granted_asset.html:45 #: users/templates/users/user_granted_asset.html:45
#: users/templates/users/user_group_granted_asset.html:45 #: users/templates/users/user_group_granted_asset.html:45
...@@ -273,7 +278,7 @@ msgstr "IP" ...@@ -273,7 +278,7 @@ msgstr "IP"
#: assets/templates/assets/asset_list.html:92 #: assets/templates/assets/asset_list.html:92
#: assets/templates/assets/system_user_asset.html:50 #: assets/templates/assets/system_user_asset.html:50
#: assets/templates/assets/user_asset_list.html:45 #: assets/templates/assets/user_asset_list.html:45
#: assets/templates/assets/user_asset_list.html:161 common/forms.py:135 #: assets/templates/assets/user_asset_list.html:161 common/forms.py:134
#: perms/templates/perms/asset_permission_asset.html:54 #: perms/templates/perms/asset_permission_asset.html:54
#: users/templates/users/user_granted_asset.html:44 #: users/templates/users/user_granted_asset.html:44
#: users/templates/users/user_group_granted_asset.html:44 #: users/templates/users/user_group_granted_asset.html:44
...@@ -281,7 +286,7 @@ msgid "Hostname" ...@@ -281,7 +286,7 @@ msgid "Hostname"
msgstr "主机名" msgstr "主机名"
#: assets/models/asset.py:75 assets/models/domain.py:49 #: assets/models/asset.py:75 assets/models/domain.py:49
#: assets/models/user.py:117 #: assets/models/user.py:117 assets/templates/assets/asset_detail.html:73
#: assets/templates/assets/domain_gateway_list.html:59 #: assets/templates/assets/domain_gateway_list.html:59
#: assets/templates/assets/system_user_detail.html:70 #: assets/templates/assets/system_user_detail.html:70
#: assets/templates/assets/system_user_list.html:31 #: assets/templates/assets/system_user_list.html:31
...@@ -290,14 +295,14 @@ msgstr "主机名" ...@@ -290,14 +295,14 @@ msgstr "主机名"
msgid "Protocol" msgid "Protocol"
msgstr "协议" msgstr "协议"
#: assets/models/asset.py:77 assets/templates/assets/asset_detail.html:97 #: assets/models/asset.py:77 assets/templates/assets/asset_detail.html:101
#: assets/templates/assets/user_asset_list.html:165 #: assets/templates/assets/user_asset_list.html:165
msgid "Platform" msgid "Platform"
msgstr "系统平台" msgstr "系统平台"
#: assets/models/asset.py:84 assets/models/cmd_filter.py:20 #: assets/models/asset.py:84 assets/models/cmd_filter.py:20
#: assets/models/domain.py:52 assets/models/label.py:21 #: assets/models/domain.py:52 assets/models/label.py:21
#: assets/templates/assets/asset_detail.html:105 #: assets/templates/assets/asset_detail.html:109
#: assets/templates/assets/user_asset_list.html:169 #: assets/templates/assets/user_asset_list.html:169
msgid "Is active" msgid "Is active"
msgstr "激活" msgstr "激活"
...@@ -306,19 +311,19 @@ msgstr "激活" ...@@ -306,19 +311,19 @@ msgstr "激活"
msgid "Public IP" msgid "Public IP"
msgstr "公网IP" msgstr "公网IP"
#: assets/models/asset.py:92 assets/templates/assets/asset_detail.html:113 #: assets/models/asset.py:92 assets/templates/assets/asset_detail.html:117
msgid "Asset number" msgid "Asset number"
msgstr "资产编号" msgstr "资产编号"
#: assets/models/asset.py:96 assets/templates/assets/asset_detail.html:77 #: assets/models/asset.py:96 assets/templates/assets/asset_detail.html:81
msgid "Vendor" msgid "Vendor"
msgstr "制造商" msgstr "制造商"
#: assets/models/asset.py:98 assets/templates/assets/asset_detail.html:81 #: assets/models/asset.py:98 assets/templates/assets/asset_detail.html:85
msgid "Model" msgid "Model"
msgstr "型号" msgstr "型号"
#: assets/models/asset.py:100 assets/templates/assets/asset_detail.html:109 #: assets/models/asset.py:100 assets/templates/assets/asset_detail.html:113
msgid "Serial number" msgid "Serial number"
msgstr "序列号" msgstr "序列号"
...@@ -338,7 +343,7 @@ msgstr "CPU核数" ...@@ -338,7 +343,7 @@ msgstr "CPU核数"
msgid "CPU vcpus" msgid "CPU vcpus"
msgstr "CPU总数" msgstr "CPU总数"
#: assets/models/asset.py:108 assets/templates/assets/asset_detail.html:89 #: assets/models/asset.py:108 assets/templates/assets/asset_detail.html:93
msgid "Memory" msgid "Memory"
msgstr "内存" msgstr "内存"
...@@ -350,7 +355,7 @@ msgstr "硬盘大小" ...@@ -350,7 +355,7 @@ msgstr "硬盘大小"
msgid "Disk info" msgid "Disk info"
msgstr "硬盘信息" msgstr "硬盘信息"
#: assets/models/asset.py:115 assets/templates/assets/asset_detail.html:101 #: assets/models/asset.py:115 assets/templates/assets/asset_detail.html:105
#: assets/templates/assets/user_asset_list.html:166 #: assets/templates/assets/user_asset_list.html:166
msgid "OS" msgid "OS"
msgstr "操作系统" msgstr "操作系统"
...@@ -368,7 +373,7 @@ msgid "Hostname raw" ...@@ -368,7 +373,7 @@ msgid "Hostname raw"
msgstr "主机名原始" msgstr "主机名原始"
#: assets/models/asset.py:125 assets/templates/assets/asset_create.html:34 #: assets/models/asset.py:125 assets/templates/assets/asset_create.html:34
#: assets/templates/assets/asset_detail.html:220 #: assets/templates/assets/asset_detail.html:224
#: assets/templates/assets/asset_update.html:39 templates/_nav.html:26 #: assets/templates/assets/asset_update.html:39 templates/_nav.html:26
msgid "Labels" msgid "Labels"
msgstr "标签管理" msgstr "标签管理"
...@@ -377,13 +382,13 @@ msgstr "标签管理" ...@@ -377,13 +382,13 @@ msgstr "标签管理"
#: assets/models/cluster.py:28 assets/models/cmd_filter.py:24 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:24
#: assets/models/cmd_filter.py:54 assets/models/group.py:21 #: assets/models/cmd_filter.py:54 assets/models/group.py:21
#: assets/templates/assets/admin_user_detail.html:68 #: assets/templates/assets/admin_user_detail.html:68
#: assets/templates/assets/asset_detail.html:117 #: assets/templates/assets/asset_detail.html:121
#: assets/templates/assets/cmd_filter_detail.html:77 #: assets/templates/assets/cmd_filter_detail.html:77
#: assets/templates/assets/domain_detail.html:72 #: assets/templates/assets/domain_detail.html:72
#: assets/templates/assets/system_user_detail.html:100 #: assets/templates/assets/system_user_detail.html:100
#: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:15 perms/models.py:37 #: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:15 perms/models.py:37
#: perms/models.py:84 perms/templates/perms/asset_permission_detail.html:98 #: perms/models.py:84 perms/templates/perms/asset_permission_detail.html:98
#: users/models/user.py:92 users/templates/users/user_detail.html:111 #: users/models/user.py:94 users/templates/users/user_detail.html:111
#: xpack/plugins/cloud/models.py:46 xpack/plugins/cloud/models.py:140 #: xpack/plugins/cloud/models.py:46 xpack/plugins/cloud/models.py:140
msgid "Created by" msgid "Created by"
msgstr "创建者" msgstr "创建者"
...@@ -412,7 +417,7 @@ msgstr "创建日期" ...@@ -412,7 +417,7 @@ msgstr "创建日期"
#: assets/models/domain.py:51 assets/models/group.py:23 #: assets/models/domain.py:51 assets/models/group.py:23
#: assets/models/label.py:22 assets/templates/assets/admin_user_detail.html:72 #: assets/models/label.py:22 assets/templates/assets/admin_user_detail.html:72
#: assets/templates/assets/admin_user_list.html:32 #: assets/templates/assets/admin_user_list.html:32
#: assets/templates/assets/asset_detail.html:125 #: assets/templates/assets/asset_detail.html:129
#: assets/templates/assets/cmd_filter_detail.html:65 #: assets/templates/assets/cmd_filter_detail.html:65
#: assets/templates/assets/cmd_filter_list.html:27 #: assets/templates/assets/cmd_filter_list.html:27
#: assets/templates/assets/cmd_filter_rule_list.html:62 #: assets/templates/assets/cmd_filter_rule_list.html:62
...@@ -421,15 +426,15 @@ msgstr "创建日期" ...@@ -421,15 +426,15 @@ msgstr "创建日期"
#: assets/templates/assets/domain_list.html:28 #: assets/templates/assets/domain_list.html:28
#: assets/templates/assets/system_user_detail.html:104 #: assets/templates/assets/system_user_detail.html:104
#: assets/templates/assets/system_user_list.html:37 #: assets/templates/assets/system_user_list.html:37
#: assets/templates/assets/user_asset_list.html:170 common/models.py:35 #: assets/templates/assets/user_asset_list.html:170 common/models.py:34
#: ops/models/adhoc.py:43 orgs/models.py:17 perms/models.py:39 #: ops/models/adhoc.py:43 orgs/models.py:17 perms/models.py:39
#: perms/models.py:86 perms/templates/perms/asset_permission_detail.html:102 #: perms/models.py:86 perms/templates/perms/asset_permission_detail.html:102
#: terminal/models.py:28 terminal/templates/terminal/terminal_detail.html:63 #: terminal/models.py:28 terminal/templates/terminal/terminal_detail.html:63
#: users/models/group.py:15 users/models/user.py:84 #: users/models/group.py:15 users/models/user.py:86
#: users/templates/users/user_detail.html:123 #: users/templates/users/user_detail.html:127
#: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_detail.html:67
#: users/templates/users/user_group_list.html:14 #: users/templates/users/user_group_list.html:14
#: users/templates/users/user_profile.html:130 xpack/plugins/cloud/models.py:45 #: users/templates/users/user_profile.html:134 xpack/plugins/cloud/models.py:45
#: xpack/plugins/cloud/models.py:138 #: xpack/plugins/cloud/models.py:138
#: xpack/plugins/cloud/templates/cloud/account_detail.html:72 #: xpack/plugins/cloud/templates/cloud/account_detail.html:72
#: xpack/plugins/cloud/templates/cloud/account_list.html:15 #: xpack/plugins/cloud/templates/cloud/account_list.html:15
...@@ -456,7 +461,7 @@ msgstr "带宽" ...@@ -456,7 +461,7 @@ msgstr "带宽"
msgid "Contact" msgid "Contact"
msgstr "联系人" msgstr "联系人"
#: assets/models/cluster.py:22 users/models/user.py:70 #: assets/models/cluster.py:22 users/models/user.py:72
#: users/templates/users/user_detail.html:76 #: users/templates/users/user_detail.html:76
msgid "Phone" msgid "Phone"
msgstr "手机" msgstr "手机"
...@@ -482,7 +487,7 @@ msgid "Default" ...@@ -482,7 +487,7 @@ msgid "Default"
msgstr "默认" msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14 #: assets/models/cluster.py:36 assets/models/label.py:14
#: users/models/user.py:363 #: users/models/user.py:422
msgid "System" msgid "System"
msgstr "系统" msgstr "系统"
...@@ -510,7 +515,7 @@ msgstr "BGP全网通" ...@@ -510,7 +515,7 @@ msgstr "BGP全网通"
msgid "Regex" msgid "Regex"
msgstr "正则表达式" msgstr "正则表达式"
#: assets/models/cmd_filter.py:35 terminal/models.py:139 #: assets/models/cmd_filter.py:35 terminal/models.py:144
#: terminal/templates/terminal/command_list.html:55 #: terminal/templates/terminal/command_list.html:55
#: terminal/templates/terminal/command_list.html:71 #: terminal/templates/terminal/command_list.html:71
#: terminal/templates/terminal/session_detail.html:48 #: terminal/templates/terminal/session_detail.html:48
...@@ -533,8 +538,10 @@ msgstr "过滤器" ...@@ -533,8 +538,10 @@ msgstr "过滤器"
#: assets/models/cmd_filter.py:46 #: assets/models/cmd_filter.py:46
#: assets/templates/assets/cmd_filter_rule_list.html:58 #: assets/templates/assets/cmd_filter_rule_list.html:58
#: audits/templates/audits/login_log_list.html:50 #: audits/templates/audits/login_log_list.html:50
#: common/templates/common/terminal_setting.html:73 #: common/templates/common/command_storage_create.html:31
#: common/templates/common/terminal_setting.html:91 #: common/templates/common/replay_storage_create.html:31
#: common/templates/common/terminal_setting.html:81
#: common/templates/common/terminal_setting.html:103
msgid "Type" msgid "Type"
msgstr "类型" msgstr "类型"
...@@ -568,6 +575,8 @@ msgstr "每行一个命令" ...@@ -568,6 +575,8 @@ msgstr "每行一个命令"
#: assets/templates/assets/system_user_list.html:38 audits/models.py:37 #: assets/templates/assets/system_user_list.html:38 audits/models.py:37
#: audits/templates/audits/operate_log_list.html:41 #: audits/templates/audits/operate_log_list.html:41
#: audits/templates/audits/operate_log_list.html:67 #: audits/templates/audits/operate_log_list.html:67
#: common/templates/common/terminal_setting.html:82
#: common/templates/common/terminal_setting.html:104
#: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64 #: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64
#: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:42 #: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:42
#: perms/templates/perms/asset_permission_list.html:60 #: perms/templates/perms/asset_permission_list.html:60
...@@ -607,14 +616,14 @@ msgstr "默认资产组" ...@@ -607,14 +616,14 @@ msgstr "默认资产组"
#: perms/templates/perms/asset_permission_create_update.html:36 #: perms/templates/perms/asset_permission_create_update.html:36
#: perms/templates/perms/asset_permission_list.html:54 #: perms/templates/perms/asset_permission_list.html:54
#: perms/templates/perms/asset_permission_list.html:142 templates/index.html:87 #: perms/templates/perms/asset_permission_list.html:142 templates/index.html:87
#: terminal/backends/command/models.py:12 terminal/models.py:132 #: terminal/backends/command/models.py:12 terminal/models.py:137
#: terminal/templates/terminal/command_list.html:32 #: terminal/templates/terminal/command_list.html:32
#: terminal/templates/terminal/command_list.html:72 #: terminal/templates/terminal/command_list.html:72
#: terminal/templates/terminal/session_list.html:33 #: terminal/templates/terminal/session_list.html:33
#: terminal/templates/terminal/session_list.html:71 users/forms.py:312 #: terminal/templates/terminal/session_list.html:71 users/forms.py:310
#: users/models/user.py:33 users/models/user.py:351 #: users/models/user.py:33 users/models/user.py:410
#: users/templates/users/user_group_detail.html:78 #: users/templates/users/user_group_detail.html:78
#: users/templates/users/user_group_list.html:13 users/views/user.py:385 #: users/templates/users/user_group_list.html:13 users/views/user.py:384
#: xpack/plugins/orgs/forms.py:26 #: xpack/plugins/orgs/forms.py:26
#: xpack/plugins/orgs/templates/orgs/org_detail.html:113 #: xpack/plugins/orgs/templates/orgs/org_detail.html:113
#: xpack/plugins/orgs/templates/orgs/org_list.html:14 #: xpack/plugins/orgs/templates/orgs/org_list.html:14
...@@ -622,7 +631,7 @@ msgid "User" ...@@ -622,7 +631,7 @@ msgid "User"
msgstr "用户" msgstr "用户"
#: assets/models/label.py:19 assets/models/node.py:20 #: assets/models/label.py:19 assets/models/node.py:20
#: assets/templates/assets/label_list.html:15 common/models.py:31 #: assets/templates/assets/label_list.html:15 common/models.py:30
msgid "Value" msgid "Value"
msgstr "值" msgstr "值"
...@@ -657,8 +666,8 @@ msgstr "手动登录" ...@@ -657,8 +666,8 @@ msgstr "手动登录"
#: assets/views/domain.py:29 assets/views/domain.py:45 #: assets/views/domain.py:29 assets/views/domain.py:45
#: assets/views/domain.py:61 assets/views/domain.py:74 #: assets/views/domain.py:61 assets/views/domain.py:74
#: assets/views/domain.py:98 assets/views/domain.py:126 #: assets/views/domain.py:98 assets/views/domain.py:126
#: assets/views/domain.py:145 assets/views/label.py:26 assets/views/label.py:42 #: assets/views/domain.py:145 assets/views/label.py:26 assets/views/label.py:43
#: assets/views/label.py:58 assets/views/system_user.py:28 #: assets/views/label.py:69 assets/views/system_user.py:28
#: assets/views/system_user.py:44 assets/views/system_user.py:60 #: assets/views/system_user.py:44 assets/views/system_user.py:60
#: assets/views/system_user.py:74 templates/_nav.html:19 #: assets/views/system_user.py:74 templates/_nav.html:19
msgid "Assets" msgid "Assets"
...@@ -690,7 +699,7 @@ msgstr "登录模式" ...@@ -690,7 +699,7 @@ msgstr "登录模式"
#: perms/templates/perms/asset_permission_detail.html:140 #: perms/templates/perms/asset_permission_detail.html:140
#: perms/templates/perms/asset_permission_list.html:58 #: perms/templates/perms/asset_permission_list.html:58
#: perms/templates/perms/asset_permission_list.html:154 templates/_nav.html:25 #: perms/templates/perms/asset_permission_list.html:154 templates/_nav.html:25
#: terminal/backends/command/models.py:14 terminal/models.py:134 #: terminal/backends/command/models.py:14 terminal/models.py:139
#: terminal/templates/terminal/command_list.html:48 #: terminal/templates/terminal/command_list.html:48
#: terminal/templates/terminal/command_list.html:74 #: terminal/templates/terminal/command_list.html:74
#: terminal/templates/terminal/session_list.html:49 #: terminal/templates/terminal/session_list.html:49
...@@ -843,17 +852,19 @@ msgstr "其它" ...@@ -843,17 +852,19 @@ msgstr "其它"
#: assets/templates/assets/gateway_create_update.html:58 #: assets/templates/assets/gateway_create_update.html:58
#: assets/templates/assets/label_create_update.html:18 #: assets/templates/assets/label_create_update.html:18
#: common/templates/common/basic_setting.html:61 #: common/templates/common/basic_setting.html:61
#: common/templates/common/command_storage_create.html:80
#: common/templates/common/email_setting.html:62 #: common/templates/common/email_setting.html:62
#: common/templates/common/ldap_setting.html:62 #: common/templates/common/ldap_setting.html:62
#: common/templates/common/replay_storage_create.html:139
#: common/templates/common/security_setting.html:70 #: common/templates/common/security_setting.html:70
#: common/templates/common/terminal_setting.html:106 #: common/templates/common/terminal_setting.html:68
#: perms/templates/perms/asset_permission_create_update.html:69 #: perms/templates/perms/asset_permission_create_update.html:69
#: terminal/templates/terminal/terminal_update.html:47 #: terminal/templates/terminal/terminal_update.html:47
#: users/templates/users/_user.html:46 #: users/templates/users/_user.html:46
#: users/templates/users/user_bulk_update.html:23 #: users/templates/users/user_bulk_update.html:23
#: users/templates/users/user_detail.html:172 #: users/templates/users/user_detail.html:176
#: users/templates/users/user_password_update.html:71 #: users/templates/users/user_password_update.html:71
#: users/templates/users/user_profile.html:198 #: users/templates/users/user_profile.html:202
#: users/templates/users/user_profile_update.html:63 #: users/templates/users/user_profile_update.html:63
#: users/templates/users/user_pubkey_update.html:70 #: users/templates/users/user_pubkey_update.html:70
#: users/templates/users/user_pubkey_update.html:76 #: users/templates/users/user_pubkey_update.html:76
...@@ -874,10 +885,12 @@ msgstr "重置" ...@@ -874,10 +885,12 @@ msgstr "重置"
#: assets/templates/assets/gateway_create_update.html:59 #: assets/templates/assets/gateway_create_update.html:59
#: assets/templates/assets/label_create_update.html:19 #: assets/templates/assets/label_create_update.html:19
#: common/templates/common/basic_setting.html:62 #: common/templates/common/basic_setting.html:62
#: common/templates/common/command_storage_create.html:81
#: common/templates/common/email_setting.html:63 #: common/templates/common/email_setting.html:63
#: common/templates/common/ldap_setting.html:63 #: common/templates/common/ldap_setting.html:63
#: common/templates/common/replay_storage_create.html:140
#: common/templates/common/security_setting.html:71 #: common/templates/common/security_setting.html:71
#: common/templates/common/terminal_setting.html:108 #: common/templates/common/terminal_setting.html:70
#: perms/templates/perms/asset_permission_create_update.html:70 #: perms/templates/perms/asset_permission_create_update.html:70
#: terminal/templates/terminal/command_list.html:103 #: terminal/templates/terminal/command_list.html:103
#: terminal/templates/terminal/session_list.html:127 #: terminal/templates/terminal/session_list.html:127
...@@ -945,12 +958,12 @@ msgid "Quick update" ...@@ -945,12 +958,12 @@ msgid "Quick update"
msgstr "快速更新" msgstr "快速更新"
#: assets/templates/assets/admin_user_assets.html:72 #: assets/templates/assets/admin_user_assets.html:72
#: assets/templates/assets/asset_detail.html:168 #: assets/templates/assets/asset_detail.html:172
msgid "Test connective" msgid "Test connective"
msgstr "测试可连接性" msgstr "测试可连接性"
#: assets/templates/assets/admin_user_assets.html:75 #: assets/templates/assets/admin_user_assets.html:75
#: assets/templates/assets/asset_detail.html:171 #: assets/templates/assets/asset_detail.html:175
#: assets/templates/assets/system_user_asset.html:75 #: assets/templates/assets/system_user_asset.html:75
#: assets/templates/assets/system_user_asset.html:161 #: assets/templates/assets/system_user_asset.html:161
#: assets/templates/assets/system_user_detail.html:151 #: assets/templates/assets/system_user_detail.html:151
...@@ -979,9 +992,9 @@ msgstr "测试" ...@@ -979,9 +992,9 @@ msgstr "测试"
#: users/templates/users/user_group_detail.html:28 #: users/templates/users/user_group_detail.html:28
#: users/templates/users/user_group_list.html:43 #: users/templates/users/user_group_list.html:43
#: users/templates/users/user_list.html:77 #: users/templates/users/user_list.html:77
#: users/templates/users/user_profile.html:151 #: users/templates/users/user_profile.html:155
#: users/templates/users/user_profile.html:181 #: users/templates/users/user_profile.html:185
#: users/templates/users/user_profile.html:190 #: users/templates/users/user_profile.html:194
#: xpack/plugins/cloud/templates/cloud/account_detail.html:25 #: xpack/plugins/cloud/templates/cloud/account_detail.html:25
#: xpack/plugins/cloud/templates/cloud/account_list.html:38 #: xpack/plugins/cloud/templates/cloud/account_list.html:38
#: xpack/plugins/orgs/templates/orgs/org_detail.html:25 #: xpack/plugins/orgs/templates/orgs/org_detail.html:25
...@@ -1003,6 +1016,8 @@ msgstr "更新" ...@@ -1003,6 +1016,8 @@ msgstr "更新"
#: assets/templates/assets/label_list.html:39 #: assets/templates/assets/label_list.html:39
#: assets/templates/assets/system_user_detail.html:30 #: assets/templates/assets/system_user_detail.html:30
#: assets/templates/assets/system_user_list.html:93 audits/models.py:33 #: assets/templates/assets/system_user_list.html:93 audits/models.py:33
#: common/templates/common/terminal_setting.html:90
#: common/templates/common/terminal_setting.html:112
#: ops/templates/ops/task_list.html:72 #: ops/templates/ops/task_list.html:72
#: perms/templates/perms/asset_permission_detail.html:34 #: perms/templates/perms/asset_permission_detail.html:34
#: perms/templates/perms/asset_permission_list.html:201 #: perms/templates/perms/asset_permission_list.html:201
...@@ -1031,21 +1046,22 @@ msgid "Select nodes" ...@@ -1031,21 +1046,22 @@ msgid "Select nodes"
msgstr "选择节点" msgstr "选择节点"
#: assets/templates/assets/admin_user_detail.html:100 #: assets/templates/assets/admin_user_detail.html:100
#: assets/templates/assets/asset_detail.html:200 #: assets/templates/assets/asset_detail.html:204
#: assets/templates/assets/asset_list.html:633 #: assets/templates/assets/asset_list.html:633
#: assets/templates/assets/cmd_filter_detail.html:106 #: assets/templates/assets/cmd_filter_detail.html:106
#: assets/templates/assets/system_user_asset.html:112 #: assets/templates/assets/system_user_asset.html:112
#: assets/templates/assets/system_user_detail.html:182 #: assets/templates/assets/system_user_detail.html:182
#: assets/templates/assets/system_user_list.html:143 templates/_modal.html:22 #: assets/templates/assets/system_user_list.html:143
#: common/templates/common/terminal_setting.html:165 templates/_modal.html:22
#: terminal/templates/terminal/session_detail.html:108 #: terminal/templates/terminal/session_detail.html:108
#: users/templates/users/user_detail.html:382 #: users/templates/users/user_detail.html:386
#: users/templates/users/user_detail.html:408 #: users/templates/users/user_detail.html:412
#: users/templates/users/user_detail.html:431 #: users/templates/users/user_detail.html:435
#: users/templates/users/user_detail.html:476 #: users/templates/users/user_detail.html:480
#: users/templates/users/user_group_create_update.html:32 #: users/templates/users/user_group_create_update.html:32
#: users/templates/users/user_group_list.html:87 #: users/templates/users/user_group_list.html:88
#: users/templates/users/user_list.html:201 #: users/templates/users/user_list.html:205
#: users/templates/users/user_profile.html:232 #: users/templates/users/user_profile.html:236
#: xpack/plugins/cloud/templates/cloud/account_create_update.html:34 #: xpack/plugins/cloud/templates/cloud/account_create_update.html:34
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create.html:36 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_create.html:36
#: xpack/plugins/orgs/templates/orgs/org_create_update.html:33 #: xpack/plugins/orgs/templates/orgs/org_create_update.html:33
...@@ -1096,28 +1112,28 @@ msgstr "选择需要修改属性" ...@@ -1096,28 +1112,28 @@ msgstr "选择需要修改属性"
msgid "Select all" msgid "Select all"
msgstr "全选" msgstr "全选"
#: assets/templates/assets/asset_detail.html:85 #: assets/templates/assets/asset_detail.html:89
msgid "CPU" msgid "CPU"
msgstr "CPU" msgstr "CPU"
#: assets/templates/assets/asset_detail.html:93 #: assets/templates/assets/asset_detail.html:97
msgid "Disk" msgid "Disk"
msgstr "硬盘" msgstr "硬盘"
#: assets/templates/assets/asset_detail.html:121 #: assets/templates/assets/asset_detail.html:125
#: users/templates/users/user_detail.html:115 #: users/templates/users/user_detail.html:115
#: users/templates/users/user_profile.html:104 #: users/templates/users/user_profile.html:104
msgid "Date joined" msgid "Date joined"
msgstr "创建日期" msgstr "创建日期"
#: assets/templates/assets/asset_detail.html:137 #: assets/templates/assets/asset_detail.html:141
#: terminal/templates/terminal/session_detail.html:81 #: terminal/templates/terminal/session_detail.html:81
#: users/templates/users/user_detail.html:134 #: users/templates/users/user_detail.html:138
#: users/templates/users/user_profile.html:142 #: users/templates/users/user_profile.html:146
msgid "Quick modify" msgid "Quick modify"
msgstr "快速修改" msgstr "快速修改"
#: assets/templates/assets/asset_detail.html:143 #: assets/templates/assets/asset_detail.html:147
#: assets/templates/assets/asset_list.html:95 #: assets/templates/assets/asset_list.html:95
#: assets/templates/assets/user_asset_list.html:47 perms/models.py:34 #: assets/templates/assets/user_asset_list.html:47 perms/models.py:34
#: perms/models.py:82 #: perms/models.py:82
...@@ -1126,7 +1142,7 @@ msgstr "快速修改" ...@@ -1126,7 +1142,7 @@ msgstr "快速修改"
#: perms/templates/perms/asset_permission_list.html:59 #: perms/templates/perms/asset_permission_list.html:59
#: terminal/templates/terminal/terminal_list.html:34 #: terminal/templates/terminal/terminal_list.html:34
#: users/templates/users/_select_user_modal.html:18 #: users/templates/users/_select_user_modal.html:18
#: users/templates/users/user_detail.html:140 #: users/templates/users/user_detail.html:144
#: users/templates/users/user_granted_asset.html:46 #: users/templates/users/user_granted_asset.html:46
#: users/templates/users/user_group_granted_asset.html:46 #: users/templates/users/user_group_granted_asset.html:46
#: users/templates/users/user_list.html:28 #: users/templates/users/user_list.html:28
...@@ -1134,17 +1150,17 @@ msgstr "快速修改" ...@@ -1134,17 +1150,17 @@ msgstr "快速修改"
msgid "Active" msgid "Active"
msgstr "激活中" msgstr "激活中"
#: assets/templates/assets/asset_detail.html:160 #: assets/templates/assets/asset_detail.html:164
msgid "Refresh hardware" msgid "Refresh hardware"
msgstr "更新硬件信息" msgstr "更新硬件信息"
#: assets/templates/assets/asset_detail.html:163 #: assets/templates/assets/asset_detail.html:167
msgid "Refresh" msgid "Refresh"
msgstr "刷新" msgstr "刷新"
#: assets/templates/assets/asset_detail.html:300 #: assets/templates/assets/asset_detail.html:304
#: users/templates/users/user_detail.html:301 #: users/templates/users/user_detail.html:305
#: users/templates/users/user_detail.html:328 #: users/templates/users/user_detail.html:332
msgid "Update successfully!" msgid "Update successfully!"
msgstr "更新成功" msgstr "更新成功"
...@@ -1257,11 +1273,11 @@ msgstr "重命名失败,不能更改root节点的名称" ...@@ -1257,11 +1273,11 @@ msgstr "重命名失败,不能更改root节点的名称"
#: assets/templates/assets/asset_list.html:627 #: assets/templates/assets/asset_list.html:627
#: assets/templates/assets/system_user_list.html:137 #: assets/templates/assets/system_user_list.html:137
#: users/templates/users/user_detail.html:376 #: users/templates/users/user_detail.html:380
#: users/templates/users/user_detail.html:402 #: users/templates/users/user_detail.html:406
#: users/templates/users/user_detail.html:470 #: users/templates/users/user_detail.html:474
#: users/templates/users/user_group_list.html:81 #: users/templates/users/user_group_list.html:82
#: users/templates/users/user_list.html:195 #: users/templates/users/user_list.html:199
msgid "Are you sure?" msgid "Are you sure?"
msgstr "你确认吗?" msgstr "你确认吗?"
...@@ -1271,12 +1287,13 @@ msgstr "删除选择资产" ...@@ -1271,12 +1287,13 @@ msgstr "删除选择资产"
#: assets/templates/assets/asset_list.html:631 #: assets/templates/assets/asset_list.html:631
#: assets/templates/assets/system_user_list.html:141 #: assets/templates/assets/system_user_list.html:141
#: users/templates/users/user_detail.html:380 #: common/templates/common/terminal_setting.html:163
#: users/templates/users/user_detail.html:406 #: users/templates/users/user_detail.html:384
#: users/templates/users/user_detail.html:474 #: users/templates/users/user_detail.html:410
#: users/templates/users/user_detail.html:478
#: users/templates/users/user_group_create_update.html:31 #: users/templates/users/user_group_create_update.html:31
#: users/templates/users/user_group_list.html:85 #: users/templates/users/user_group_list.html:86
#: users/templates/users/user_list.html:199 #: users/templates/users/user_list.html:203
#: xpack/plugins/orgs/templates/orgs/org_create_update.html:32 #: xpack/plugins/orgs/templates/orgs/org_create_update.html:32
msgid "Cancel" msgid "Cancel"
msgstr "取消" msgstr "取消"
...@@ -1346,7 +1363,7 @@ msgstr "创建命令过滤器" ...@@ -1346,7 +1363,7 @@ msgstr "创建命令过滤器"
#: assets/templates/assets/cmd_filter_rule_list.html:33 #: assets/templates/assets/cmd_filter_rule_list.html:33
#: assets/views/cmd_filter.py:98 #: assets/views/cmd_filter.py:98
msgid "Command filter rule list" msgid "Command filter rule list"
msgstr "命令过滤器列表" msgstr "命令过滤器规则列表"
#: assets/templates/assets/cmd_filter_rule_list.html:50 #: assets/templates/assets/cmd_filter_rule_list.html:50
msgid "Create rule" msgid "Create rule"
...@@ -1407,7 +1424,7 @@ msgstr "JMS => 网域网关 => 目标资产" ...@@ -1407,7 +1424,7 @@ msgstr "JMS => 网域网关 => 目标资产"
msgid "Create domain" msgid "Create domain"
msgstr "创建网域" msgstr "创建网域"
#: assets/templates/assets/label_list.html:6 assets/views/label.py:43 #: assets/templates/assets/label_list.html:6 assets/views/label.py:44
msgid "Create label" msgid "Create label"
msgstr "创建标签" msgstr "创建标签"
...@@ -1527,7 +1544,7 @@ msgstr "批量更新资产" ...@@ -1527,7 +1544,7 @@ msgstr "批量更新资产"
msgid "Update asset" msgid "Update asset"
msgstr "更新资产" msgstr "更新资产"
#: assets/views/asset.py:291 #: assets/views/asset.py:292
msgid "already exists" msgid "already exists"
msgstr "已经存在" msgstr "已经存在"
...@@ -1575,7 +1592,11 @@ msgstr "创建网关" ...@@ -1575,7 +1592,11 @@ msgstr "创建网关"
msgid "Label list" msgid "Label list"
msgstr "标签列表" msgstr "标签列表"
#: assets/views/label.py:59 #: assets/views/label.py:53
msgid "Tips: Avoid using label names reserved internally: {}"
msgstr "提示: 请避免使用内部预留标签名: {}"
#: assets/views/label.py:70
msgid "Update label" msgid "Update label"
msgstr "更新标签" msgstr "更新标签"
...@@ -1603,7 +1624,7 @@ msgstr "系统用户集群资产" ...@@ -1603,7 +1624,7 @@ msgstr "系统用户集群资产"
#: audits/templates/audits/ftp_log_list.html:73 #: audits/templates/audits/ftp_log_list.html:73
#: audits/templates/audits/operate_log_list.html:70 #: audits/templates/audits/operate_log_list.html:70
#: audits/templates/audits/password_change_log_list.html:52 #: audits/templates/audits/password_change_log_list.html:52
#: terminal/models.py:136 terminal/templates/terminal/session_list.html:74 #: terminal/models.py:141 terminal/templates/terminal/session_list.html:74
#: terminal/templates/terminal/terminal_detail.html:47 #: terminal/templates/terminal/terminal_detail.html:47
msgid "Remote addr" msgid "Remote addr"
msgstr "远端地址" msgstr "远端地址"
...@@ -1618,8 +1639,8 @@ msgid "Filename" ...@@ -1618,8 +1639,8 @@ msgid "Filename"
msgstr "文件名" msgstr "文件名"
#: audits/models.py:22 audits/templates/audits/ftp_log_list.html:76 #: audits/models.py:22 audits/templates/audits/ftp_log_list.html:76
#: ops/templates/ops/task_list.html:39 users/models/authentication.py:66 #: ops/templates/ops/task_list.html:39 users/models/authentication.py:73
#: users/templates/users/user_detail.html:452 xpack/plugins/cloud/api.py:61 #: users/templates/users/user_detail.html:456 xpack/plugins/cloud/api.py:61
msgid "Success" msgid "Success"
msgstr "成功" msgstr "成功"
...@@ -1644,7 +1665,7 @@ msgstr "修改者" ...@@ -1644,7 +1665,7 @@ msgstr "修改者"
#: ops/templates/ops/adhoc_history.html:52 #: ops/templates/ops/adhoc_history.html:52
#: ops/templates/ops/adhoc_history_detail.html:61 #: ops/templates/ops/adhoc_history_detail.html:61
#: ops/templates/ops/task_history.html:58 perms/models.py:35 #: ops/templates/ops/task_history.html:58 perms/models.py:35
#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:143 #: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:148
#: terminal/templates/terminal/session_list.html:78 #: terminal/templates/terminal/session_list.html:78
msgid "Date start" msgid "Date start"
msgstr "开始日期" msgstr "开始日期"
...@@ -1685,20 +1706,20 @@ msgstr "Agent" ...@@ -1685,20 +1706,20 @@ msgstr "Agent"
msgid "City" msgid "City"
msgstr "城市" msgstr "城市"
#: audits/templates/audits/login_log_list.html:54 users/forms.py:169 #: audits/templates/audits/login_log_list.html:54 users/forms.py:168
#: users/models/authentication.py:75 users/models/user.py:73 #: users/models/authentication.py:82 users/models/user.py:75
#: users/templates/users/first_login.html:45 #: users/templates/users/first_login.html:45
msgid "MFA" msgid "MFA"
msgstr "MFA" msgstr "MFA"
#: audits/templates/audits/login_log_list.html:55 #: audits/templates/audits/login_log_list.html:55
#: users/models/authentication.py:76 xpack/plugins/cloud/models.py:192 #: users/models/authentication.py:83 xpack/plugins/cloud/models.py:192
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:69 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:69
msgid "Reason" msgid "Reason"
msgstr "原因" msgstr "原因"
#: audits/templates/audits/login_log_list.html:56 #: audits/templates/audits/login_log_list.html:56
#: users/models/authentication.py:77 xpack/plugins/cloud/models.py:191 #: users/models/authentication.py:84 xpack/plugins/cloud/models.py:191
#: xpack/plugins/cloud/models.py:208 #: xpack/plugins/cloud/models.py:208
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:67 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:67
...@@ -1735,34 +1756,47 @@ msgstr "操作日志" ...@@ -1735,34 +1756,47 @@ msgstr "操作日志"
msgid "Password change log" msgid "Password change log"
msgstr "改密日志" msgstr "改密日志"
#: audits/views.py:183 templates/_nav.html:10 users/views/group.py:28 #: audits/views.py:187 templates/_nav.html:10 users/views/group.py:28
#: users/views/group.py:44 users/views/group.py:60 users/views/group.py:76 #: users/views/group.py:44 users/views/group.py:60 users/views/group.py:76
#: users/views/group.py:92 users/views/login.py:327 users/views/user.py:68 #: users/views/group.py:92 users/views/login.py:346 users/views/user.py:68
#: users/views/user.py:83 users/views/user.py:111 users/views/user.py:193 #: users/views/user.py:83 users/views/user.py:111 users/views/user.py:192
#: users/views/user.py:354 users/views/user.py:404 users/views/user.py:439 #: users/views/user.py:353 users/views/user.py:403 users/views/user.py:437
msgid "Users" msgid "Users"
msgstr "用户管理" msgstr "用户管理"
#: audits/views.py:184 templates/_nav.html:76 #: audits/views.py:188 templates/_nav.html:76
msgid "Login log" msgid "Login log"
msgstr "登录日志" msgstr "登录日志"
#: common/api.py:18 #: common/api.py:22
msgid "Test mail sent to {}, please check" msgid "Test mail sent to {}, please check"
msgstr "邮件已经发送{}, 请检查" msgstr "邮件已经发送{}, 请检查"
#: common/api.py:42 #: common/api.py:46
msgid "Test ldap success" msgid "Test ldap success"
msgstr "连接LDAP成功" msgstr "连接LDAP成功"
#: common/api.py:72 #: common/api.py:76
msgid "Search no entry matched in ou {}" msgid "Search no entry matched in ou {}"
msgstr "在ou:{}中没有匹配条目" msgstr "在ou:{}中没有匹配条目"
#: common/api.py:81 #: common/api.py:85
msgid "Match {} s users" msgid "Match {} s users"
msgstr "匹配 {} 个用户" msgstr "匹配 {} 个用户"
#: common/api.py:107 common/api.py:139
msgid "Error: Account invalid"
msgstr ""
#: common/api.py:110 common/api.py:142
msgid "Create succeed"
msgstr "创建成功"
#: common/api.py:128 common/api.py:162
#: common/templates/common/terminal_setting.html:151
msgid "Delete succeed"
msgstr "删除成功"
#: common/const.py:6 #: common/const.py:6
#, python-format #, python-format
msgid "<b>%(name)s</b> was created successfully" msgid "<b>%(name)s</b> was created successfully"
...@@ -1785,88 +1819,92 @@ msgstr "不是字符类型" ...@@ -1785,88 +1819,92 @@ msgstr "不是字符类型"
msgid "Encrypt field using Secret Key" msgid "Encrypt field using Secret Key"
msgstr "" msgstr ""
#: common/forms.py:64 #: common/forms.py:63
msgid "Current SITE URL" msgid "Current SITE URL"
msgstr "当前站点URL" msgstr "当前站点URL"
#: common/forms.py:68 #: common/forms.py:67
msgid "User Guide URL" msgid "User Guide URL"
msgstr "用户向导URL" msgstr "用户向导URL"
#: common/forms.py:69 #: common/forms.py:68
msgid "User first login update profile done redirect to it" msgid "User first login update profile done redirect to it"
msgstr "用户第一次登录,修改profile后重定向到地址" msgstr "用户第一次登录,修改profile后重定向到地址"
#: common/forms.py:72 #: common/forms.py:71
msgid "Email Subject Prefix" msgid "Email Subject Prefix"
msgstr "Email主题前缀" msgstr "Email主题前缀"
#: common/forms.py:79 #: common/forms.py:72
msgid "Tips: Some word will be intercept by mail provider"
msgstr "提示: 一些关键字可能会被邮件提供商拦截,如 跳板机、Jumpserver"
#: common/forms.py:78
msgid "SMTP host" msgid "SMTP host"
msgstr "SMTP主机" msgstr "SMTP主机"
#: common/forms.py:81 #: common/forms.py:80
msgid "SMTP port" msgid "SMTP port"
msgstr "SMTP端口" msgstr "SMTP端口"
#: common/forms.py:83 #: common/forms.py:82
msgid "SMTP user" msgid "SMTP user"
msgstr "SMTP账号" msgstr "SMTP账号"
#: common/forms.py:86 #: common/forms.py:85
msgid "SMTP password" msgid "SMTP password"
msgstr "SMTP密码" msgstr "SMTP密码"
#: common/forms.py:87 #: common/forms.py:86
msgid "Some provider use token except password" msgid "Some provider use token except password"
msgstr "一些邮件提供商需要输入的是Token" msgstr "一些邮件提供商需要输入的是Token"
#: common/forms.py:90 common/forms.py:128 #: common/forms.py:89 common/forms.py:127
msgid "Use SSL" msgid "Use SSL"
msgstr "使用SSL" msgstr "使用SSL"
#: common/forms.py:91 #: common/forms.py:90
msgid "If SMTP port is 465, may be select" msgid "If SMTP port is 465, may be select"
msgstr "如果SMTP端口是465,通常需要启用SSL" msgstr "如果SMTP端口是465,通常需要启用SSL"
#: common/forms.py:94 #: common/forms.py:93
msgid "Use TLS" msgid "Use TLS"
msgstr "使用TLS" msgstr "使用TLS"
#: common/forms.py:95 #: common/forms.py:94
msgid "If SMTP port is 587, may be select" msgid "If SMTP port is 587, may be select"
msgstr "如果SMTP端口是587,通常需要启用TLS" msgstr "如果SMTP端口是587,通常需要启用TLS"
#: common/forms.py:101 #: common/forms.py:100
msgid "LDAP server" msgid "LDAP server"
msgstr "LDAP地址" msgstr "LDAP地址"
#: common/forms.py:104 #: common/forms.py:103
msgid "Bind DN" msgid "Bind DN"
msgstr "绑定DN" msgstr "绑定DN"
#: common/forms.py:111 #: common/forms.py:110
msgid "User OU" msgid "User OU"
msgstr "用户OU" msgstr "用户OU"
#: common/forms.py:112 #: common/forms.py:111
msgid "Use | split User OUs" msgid "Use | split User OUs"
msgstr "使用|分隔各OU" msgstr "使用|分隔各OU"
#: common/forms.py:115 #: common/forms.py:114
msgid "User search filter" msgid "User search filter"
msgstr "用户过滤器" msgstr "用户过滤器"
#: common/forms.py:116 #: common/forms.py:115
#, python-format #, python-format
msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)"
msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)"
#: common/forms.py:119 #: common/forms.py:118
msgid "User attr map" msgid "User attr map"
msgstr "LDAP属性映射" msgstr "LDAP属性映射"
#: common/forms.py:121 #: common/forms.py:120
msgid "" msgid ""
"User attr map present how to map LDAP user attr to jumpserver, username,name," "User attr map present how to map LDAP user attr to jumpserver, username,name,"
"email is jumpserver attr" "email is jumpserver attr"
...@@ -1874,125 +1912,115 @@ msgstr "" ...@@ -1874,125 +1912,115 @@ msgstr ""
"用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name," "用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name,"
"email 是jumpserver的属性" "email 是jumpserver的属性"
#: common/forms.py:130 #: common/forms.py:129
msgid "Enable LDAP auth" msgid "Enable LDAP auth"
msgstr "启用LDAP认证" msgstr "启用LDAP认证"
#: common/forms.py:139 #: common/forms.py:138
msgid "List sort by"
msgstr "资产列表排序"
#: common/forms.py:142
msgid "Heartbeat interval"
msgstr "心跳间隔"
#: common/forms.py:142 ops/models/adhoc.py:38
msgid "Units: seconds"
msgstr "单位: 秒"
#: common/forms.py:145
msgid "Password auth" msgid "Password auth"
msgstr "密码认证" msgstr "密码认证"
#: common/forms.py:148 #: common/forms.py:141
msgid "Public key auth" msgid "Public key auth"
msgstr "密钥认证" msgstr "密钥认证"
#: common/forms.py:151 common/templates/common/terminal_setting.html:68 #: common/forms.py:144
#: terminal/forms.py:30 terminal/models.py:22 msgid "Heartbeat interval"
msgid "Command storage" msgstr "心跳间隔"
msgstr "命令存储"
#: common/forms.py:152
msgid ""
"Set terminal storage setting, `default` is the using as default,You can set "
"other storage and some terminal using"
msgstr "设置终端命令存储,default是默认用的存储方式"
#: common/forms.py:157 common/templates/common/terminal_setting.html:86 #: common/forms.py:144 ops/models/adhoc.py:38
#: terminal/forms.py:35 terminal/models.py:23 msgid "Units: seconds"
msgid "Replay storage" msgstr "单位: 秒"
msgstr "录像存储"
#: common/forms.py:158 #: common/forms.py:147
msgid "" msgid "List sort by"
"Set replay storage setting, `default` is the using as default,You can set " msgstr "资产列表排序"
"other storage and some terminal using"
msgstr "设置终端录像存储,default是默认用的存储方式"
#: common/forms.py:168 #: common/forms.py:159
msgid "MFA Secondary certification" msgid "MFA Secondary certification"
msgstr "MFA 二次认证" msgstr "MFA 二次认证"
#: common/forms.py:170 #: common/forms.py:161
msgid "" msgid ""
"After opening, the user login must use MFA secondary authentication (valid " "After opening, the user login must use MFA secondary authentication (valid "
"for all users, including administrators)" "for all users, including administrators)"
msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)" msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)"
#: common/forms.py:177 #: common/forms.py:168
msgid "Limit the number of login failures" msgid "Limit the number of login failures"
msgstr "限制登录失败次数" msgstr "限制登录失败次数"
#: common/forms.py:182 #: common/forms.py:173
msgid "No logon interval" msgid "No logon interval"
msgstr "禁止登录时间间隔" msgstr "禁止登录时间间隔"
#: common/forms.py:184 #: common/forms.py:175
msgid "" msgid ""
"Tip :(unit/minute) if the user has failed to log in for a limited number of " "Tip: (unit/minute) if the user has failed to log in for a limited number of "
"times, no login is allowed during this time interval." "times, no login is allowed during this time interval."
msgstr "" msgstr ""
"提示: (单位: 分钟) 当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录." "提示: (单位: 分钟) 当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录."
#: common/forms.py:190 #: common/forms.py:182
msgid "Connection max idle time" msgid "Connection max idle time"
msgstr "SSH最大空闲时间" msgstr "SSH最大空闲时间"
#: common/forms.py:192 #: common/forms.py:184
msgid "" msgid ""
"If idle time more than it, disconnect connection(only ssh now) Unit: minute" "If idle time more than it, disconnect connection(only ssh now) Unit: minute"
msgstr "提示: (单位: 分钟) 如果超过该配置没有操作,连接会被断开(仅ssh) " msgstr "提示: (单位: 分钟) 如果超过该配置没有操作,连接会被断开(仅ssh) "
#: common/forms.py:198 #: common/forms.py:190
msgid "Password expiration time"
msgstr "密码过期时间"
#: common/forms.py:193
msgid ""
"Tip: (unit/day) If the user does not update the password during the time, "
"the user password will expire failure;The password expiration reminder mail "
"will be automatic sent to the user by system within 5 days (daily) before "
"the password expires"
msgstr ""
#: common/forms.py:202
msgid "Password minimum length" msgid "Password minimum length"
msgstr "密码最小长度 " msgstr "密码最小长度 "
#: common/forms.py:204 #: common/forms.py:208
msgid "Must contain capital letters" msgid "Must contain capital letters"
msgstr "必须包含大写字母" msgstr "必须包含大写字母"
#: common/forms.py:206 #: common/forms.py:210
msgid "" msgid ""
"After opening, the user password changes and resets must contain uppercase " "After opening, the user password changes and resets must contain uppercase "
"letters" "letters"
msgstr "开启后,用户密码修改、重置必须包含大写字母" msgstr "开启后,用户密码修改、重置必须包含大写字母"
#: common/forms.py:212 #: common/forms.py:216
msgid "Must contain lowercase letters" msgid "Must contain lowercase letters"
msgstr "必须包含小写字母" msgstr "必须包含小写字母"
#: common/forms.py:213 #: common/forms.py:217
msgid "" msgid ""
"After opening, the user password changes and resets must contain lowercase " "After opening, the user password changes and resets must contain lowercase "
"letters" "letters"
msgstr "开启后,用户密码修改、重置必须包含小写字母" msgstr "开启后,用户密码修改、重置必须包含小写字母"
#: common/forms.py:219 #: common/forms.py:223
msgid "Must contain numeric characters" msgid "Must contain numeric characters"
msgstr "必须包含数字字符" msgstr "必须包含数字字符"
#: common/forms.py:220 #: common/forms.py:224
msgid "" msgid ""
"After opening, the user password changes and resets must contain numeric " "After opening, the user password changes and resets must contain numeric "
"characters" "characters"
msgstr "开启后,用户密码修改、重置必须包含数字字符" msgstr "开启后,用户密码修改、重置必须包含数字字符"
#: common/forms.py:226 #: common/forms.py:230
msgid "Must contain special characters" msgid "Must contain special characters"
msgstr "必须包含特殊字符" msgstr "必须包含特殊字符"
#: common/forms.py:227 #: common/forms.py:231
msgid "" msgid ""
"After opening, the user password changes and resets must contain special " "After opening, the user password changes and resets must contain special "
"characters" "characters"
...@@ -2006,7 +2034,7 @@ msgstr "" ...@@ -2006,7 +2034,7 @@ msgstr ""
msgid "discard time" msgid "discard time"
msgstr "" msgstr ""
#: common/models.py:34 users/models/authentication.py:51 #: common/models.py:33 users/models/authentication.py:54
#: users/templates/users/user_detail.html:96 #: users/templates/users/user_detail.html:96
msgid "Enabled" msgid "Enabled"
msgstr "启用" msgstr "启用"
...@@ -2016,7 +2044,7 @@ msgstr "启用" ...@@ -2016,7 +2044,7 @@ msgstr "启用"
#: common/templates/common/ldap_setting.html:15 #: common/templates/common/ldap_setting.html:15
#: common/templates/common/security_setting.html:15 #: common/templates/common/security_setting.html:15
#: common/templates/common/terminal_setting.html:16 #: common/templates/common/terminal_setting.html:16
#: common/templates/common/terminal_setting.html:46 common/views.py:20 #: common/templates/common/terminal_setting.html:46 common/views.py:19
msgid "Basic setting" msgid "Basic setting"
msgstr "基本设置" msgstr "基本设置"
...@@ -2024,7 +2052,7 @@ msgstr "基本设置" ...@@ -2024,7 +2052,7 @@ msgstr "基本设置"
#: common/templates/common/email_setting.html:18 #: common/templates/common/email_setting.html:18
#: common/templates/common/ldap_setting.html:18 #: common/templates/common/ldap_setting.html:18
#: common/templates/common/security_setting.html:18 #: common/templates/common/security_setting.html:18
#: common/templates/common/terminal_setting.html:20 common/views.py:46 #: common/templates/common/terminal_setting.html:20 common/views.py:45
msgid "Email setting" msgid "Email setting"
msgstr "邮件设置" msgstr "邮件设置"
...@@ -2032,7 +2060,7 @@ msgstr "邮件设置" ...@@ -2032,7 +2060,7 @@ msgstr "邮件设置"
#: common/templates/common/email_setting.html:21 #: common/templates/common/email_setting.html:21
#: common/templates/common/ldap_setting.html:21 #: common/templates/common/ldap_setting.html:21
#: common/templates/common/security_setting.html:21 #: common/templates/common/security_setting.html:21
#: common/templates/common/terminal_setting.html:24 common/views.py:72 #: common/templates/common/terminal_setting.html:24 common/views.py:71
msgid "LDAP setting" msgid "LDAP setting"
msgstr "LDAP设置" msgstr "LDAP设置"
...@@ -2040,7 +2068,7 @@ msgstr "LDAP设置" ...@@ -2040,7 +2068,7 @@ msgstr "LDAP设置"
#: common/templates/common/email_setting.html:24 #: common/templates/common/email_setting.html:24
#: common/templates/common/ldap_setting.html:24 #: common/templates/common/ldap_setting.html:24
#: common/templates/common/security_setting.html:24 #: common/templates/common/security_setting.html:24
#: common/templates/common/terminal_setting.html:28 common/views.py:102 #: common/templates/common/terminal_setting.html:28 common/views.py:100
msgid "Terminal setting" msgid "Terminal setting"
msgstr "终端设置" msgstr "终端设置"
...@@ -2048,10 +2076,72 @@ msgstr "终端设置" ...@@ -2048,10 +2076,72 @@ msgstr "终端设置"
#: common/templates/common/email_setting.html:27 #: common/templates/common/email_setting.html:27
#: common/templates/common/ldap_setting.html:27 #: common/templates/common/ldap_setting.html:27
#: common/templates/common/security_setting.html:27 #: common/templates/common/security_setting.html:27
#: common/templates/common/terminal_setting.html:31 common/views.py:130 #: common/templates/common/terminal_setting.html:31 common/views.py:152
msgid "Security setting" msgid "Security setting"
msgstr "安全设置" msgstr "安全设置"
#: common/templates/common/command_storage_create.html:50
#: ops/models/adhoc.py:159 ops/templates/ops/adhoc_detail.html:53
#: ops/templates/ops/task_adhoc.html:59 ops/templates/ops/task_list.html:38
msgid "Hosts"
msgstr "主机"
#: common/templates/common/command_storage_create.html:53
msgid "Tips: If there are multiple hosts, separate them with a comma (,)"
msgstr "提示: 如果有多台主机,请使用逗号 ( , ) 进行分割"
#: common/templates/common/command_storage_create.html:64
msgid "Index"
msgstr "索引"
#: common/templates/common/command_storage_create.html:71
msgid "Doc type"
msgstr "文档类型"
#: common/templates/common/replay_storage_create.html:53
#: templates/index.html:91
msgid "Host"
msgstr "主机"
#: common/templates/common/replay_storage_create.html:67
msgid "Bucket"
msgstr "桶名称"
#: common/templates/common/replay_storage_create.html:74
msgid "Access key"
msgstr ""
#: common/templates/common/replay_storage_create.html:81
msgid "Secret key"
msgstr ""
#: common/templates/common/replay_storage_create.html:88
msgid "Container name"
msgstr "容器名称"
#: common/templates/common/replay_storage_create.html:95
msgid "Account name"
msgstr "账户名称"
#: common/templates/common/replay_storage_create.html:102
msgid "Account key"
msgstr "账户密钥"
#: common/templates/common/replay_storage_create.html:109
msgid "Endpoint"
msgstr "端点"
#: common/templates/common/replay_storage_create.html:116
msgid "Endpoint suffix"
msgstr "端点后缀"
#: common/templates/common/replay_storage_create.html:130
#: xpack/plugins/cloud/models.py:206
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:83
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:64
msgid "Region"
msgstr "地域"
#: common/templates/common/security_setting.html:42 #: common/templates/common/security_setting.html:42
msgid "User login settings" msgid "User login settings"
msgstr "用户登录设置" msgstr "用户登录设置"
...@@ -2060,21 +2150,66 @@ msgstr "用户登录设置" ...@@ -2060,21 +2150,66 @@ msgstr "用户登录设置"
msgid "Password check rule" msgid "Password check rule"
msgstr "密码校验规则" msgstr "密码校验规则"
#: common/templates/common/terminal_setting.html:76 terminal/forms.py:27
#: terminal/models.py:22
msgid "Command storage"
msgstr "命令存储"
#: common/templates/common/terminal_setting.html:95
#: common/templates/common/terminal_setting.html:117
#: perms/templates/perms/asset_permission_asset.html:97
#: perms/templates/perms/asset_permission_detail.html:157
#: perms/templates/perms/asset_permission_user.html:97
#: perms/templates/perms/asset_permission_user.html:125
#: users/templates/users/user_group_detail.html:95
#: xpack/plugins/orgs/templates/orgs/org_detail.html:93
#: xpack/plugins/orgs/templates/orgs/org_detail.html:130
msgid "Add"
msgstr "添加"
#: common/templates/common/terminal_setting.html:98 terminal/forms.py:32
#: terminal/models.py:23
msgid "Replay storage"
msgstr "录像存储"
#: common/templates/common/terminal_setting.html:154
msgid "Delete failed"
msgstr "删除失败"
#: common/templates/common/terminal_setting.html:159
msgid "Are you sure about deleting it?"
msgstr "您确定删除吗?"
#: common/validators.py:7 #: common/validators.py:7
msgid "Special char not allowed" msgid "Special char not allowed"
msgstr "不能包含特殊字符" msgstr "不能包含特殊字符"
#: common/views.py:19 common/views.py:45 common/views.py:71 common/views.py:101 #: common/views.py:18 common/views.py:44 common/views.py:70 common/views.py:99
#: common/views.py:129 templates/_nav.html:116 #: common/views.py:126 common/views.py:138 common/views.py:151
#: templates/_nav.html:116
msgid "Settings" msgid "Settings"
msgstr "系统设置" msgstr "系统设置"
#: common/views.py:30 common/views.py:56 common/views.py:84 common/views.py:114 #: common/views.py:29 common/views.py:81 common/views.py:112
#: common/views.py:140 #: common/views.py:162
msgid "Update setting successfully"
msgstr "更新设置成功"
#: common/views.py:55
#, fuzzy
#| msgid "Update setting successfully"
msgid "Update setting successfully, please restart program" msgid "Update setting successfully, please restart program"
msgstr "更新设置成功, 请手动重启程序" msgstr "更新设置成功"
#: common/views.py:127
msgid "Create replay storage"
msgstr "创建录像存储"
#: common/views.py:139
msgid "Create command storage"
msgstr "创建命令存储"
#: jumpserver/views.py:180 #: jumpserver/views.py:185
msgid "" msgid ""
"<div>Luna is a separately deployed program, you need to deploy Luna, coco, " "<div>Luna is a separately deployed program, you need to deploy Luna, coco, "
"configure nginx for url distribution,</div> </div>If you see this page, " "configure nginx for url distribution,</div> </div>If you see this page, "
...@@ -2117,11 +2252,6 @@ msgstr "模式" ...@@ -2117,11 +2252,6 @@ msgstr "模式"
msgid "Options" msgid "Options"
msgstr "选项" msgstr "选项"
#: ops/models/adhoc.py:159 ops/templates/ops/adhoc_detail.html:53
#: ops/templates/ops/task_adhoc.html:59 ops/templates/ops/task_list.html:38
msgid "Hosts"
msgstr "主机"
#: ops/models/adhoc.py:160 #: ops/models/adhoc.py:160
msgid "Run as admin" msgid "Run as admin"
msgstr "再次执行" msgstr "再次执行"
...@@ -2330,16 +2460,16 @@ msgstr "任务列表" ...@@ -2330,16 +2460,16 @@ msgstr "任务列表"
msgid "Task run history" msgid "Task run history"
msgstr "执行历史" msgstr "执行历史"
#: orgs/mixins.py:78 orgs/models.py:24 #: orgs/mixins.py:77 orgs/models.py:24
msgid "Organization" msgid "Organization"
msgstr "组织管理" msgstr "组织管理"
#: perms/forms.py:31 perms/models.py:30 perms/models.py:80 #: perms/forms.py:31 perms/models.py:30 perms/models.py:80
#: perms/templates/perms/asset_permission_list.html:55 #: perms/templates/perms/asset_permission_list.html:55
#: perms/templates/perms/asset_permission_list.html:145 templates/_nav.html:14 #: perms/templates/perms/asset_permission_list.html:145 templates/_nav.html:14
#: users/forms.py:282 users/models/group.py:26 users/models/user.py:57 #: users/forms.py:280 users/models/group.py:26 users/models/user.py:59
#: users/templates/users/_select_user_modal.html:16 #: users/templates/users/_select_user_modal.html:16
#: users/templates/users/user_detail.html:207 #: users/templates/users/user_detail.html:211
#: users/templates/users/user_list.html:26 #: users/templates/users/user_list.html:26
#: xpack/plugins/orgs/templates/orgs/org_list.html:15 #: xpack/plugins/orgs/templates/orgs/org_list.html:15
msgid "User group" msgid "User group"
...@@ -2355,8 +2485,8 @@ msgstr "资产和节点至少选一个" ...@@ -2355,8 +2485,8 @@ msgstr "资产和节点至少选一个"
#: perms/models.py:36 perms/models.py:83 #: perms/models.py:36 perms/models.py:83
#: perms/templates/perms/asset_permission_detail.html:90 #: perms/templates/perms/asset_permission_detail.html:90
#: users/models/user.py:89 users/templates/users/user_detail.html:107 #: users/models/user.py:91 users/templates/users/user_detail.html:107
#: users/templates/users/user_profile.html:112 #: users/templates/users/user_profile.html:116
msgid "Date expired" msgid "Date expired"
msgstr "失效日期" msgstr "失效日期"
...@@ -2373,29 +2503,19 @@ msgstr "用户或用户组" ...@@ -2373,29 +2503,19 @@ msgstr "用户或用户组"
#: perms/templates/perms/asset_permission_asset.html:27 #: perms/templates/perms/asset_permission_asset.html:27
#: perms/templates/perms/asset_permission_detail.html:27 #: perms/templates/perms/asset_permission_detail.html:27
#: perms/templates/perms/asset_permission_user.html:27 #: perms/templates/perms/asset_permission_user.html:27
msgid "Assets and asset groups" msgid "Assets and node"
msgstr "资产或资产组" msgstr "资产或节点"
#: perms/templates/perms/asset_permission_asset.html:80 #: perms/templates/perms/asset_permission_asset.html:80
msgid "Add asset to this permission" msgid "Add asset to this permission"
msgstr "添加资产" msgstr "添加资产"
#: perms/templates/perms/asset_permission_asset.html:97
#: perms/templates/perms/asset_permission_detail.html:157
#: perms/templates/perms/asset_permission_user.html:97
#: perms/templates/perms/asset_permission_user.html:125
#: users/templates/users/user_group_detail.html:95
#: xpack/plugins/orgs/templates/orgs/org_detail.html:93
#: xpack/plugins/orgs/templates/orgs/org_detail.html:130
msgid "Add"
msgstr "添加"
#: perms/templates/perms/asset_permission_asset.html:108 #: perms/templates/perms/asset_permission_asset.html:108
msgid "Add node to this permission" msgid "Add node to this permission"
msgstr "添加节点" msgstr "添加节点"
#: perms/templates/perms/asset_permission_asset.html:125 #: perms/templates/perms/asset_permission_asset.html:125
#: users/templates/users/user_detail.html:224 #: users/templates/users/user_detail.html:228
msgid "Join" msgid "Join"
msgstr "加入" msgstr "加入"
...@@ -2489,14 +2609,14 @@ msgstr "文档" ...@@ -2489,14 +2609,14 @@ msgstr "文档"
msgid "Commercial support" msgid "Commercial support"
msgstr "商业支持" msgstr "商业支持"
#: templates/_header_bar.html:89 templates/_nav_user.html:9 users/forms.py:148 #: templates/_header_bar.html:89 templates/_nav_user.html:9 users/forms.py:147
#: users/templates/users/_user.html:39 #: users/templates/users/_user.html:39
#: users/templates/users/first_login.html:39 #: users/templates/users/first_login.html:39
#: users/templates/users/user_password_update.html:40 #: users/templates/users/user_password_update.html:40
#: users/templates/users/user_profile.html:17 #: users/templates/users/user_profile.html:17
#: users/templates/users/user_profile_update.html:37 #: users/templates/users/user_profile_update.html:37
#: users/templates/users/user_profile_update.html:57 #: users/templates/users/user_profile_update.html:57
#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:367 #: users/templates/users/user_pubkey_update.html:37 users/views/user.py:366
msgid "Profile" msgid "Profile"
msgstr "个人信息" msgstr "个人信息"
...@@ -2513,7 +2633,7 @@ msgid "Logout" ...@@ -2513,7 +2633,7 @@ msgid "Logout"
msgstr "注销登录" msgstr "注销登录"
#: templates/_header_bar.html:101 users/templates/users/login.html:46 #: templates/_header_bar.html:101 users/templates/users/login.html:46
#: users/templates/users/login.html:70 #: users/templates/users/login.html:72
msgid "Login" msgid "Login"
msgstr "登录" msgstr "登录"
...@@ -2521,7 +2641,41 @@ msgstr "登录" ...@@ -2521,7 +2641,41 @@ msgstr "登录"
msgid "Dashboard" msgid "Dashboard"
msgstr "仪表盘" msgstr "仪表盘"
#: templates/_message.html:6 #: templates/_message.html:7
#, python-format
msgid ""
"\n"
" Your password has expired, please click <a href="
"\"%(user_password_update_url)s\"> this link </a> update password.\n"
" "
msgstr ""
"\n"
" 您的密码已经过期,请点击 <a href="
"\"%(user_password_update_url)s\"> 链接 </a> 更新密码\n"
" "
#: templates/_message.html:14
msgid "Your password will at"
msgstr "您的密码将于"
#: templates/_message.html:14
msgid "expired. "
msgstr "过期。"
#: templates/_message.html:15
#, python-format
msgid ""
"\n"
" please click <a href=\"%(user_password_update_url)s\"> this "
"link </a> to update your password.\n"
" "
msgstr ""
"\n"
" 请点击 <a href=\"%(user_password_update_url)s\"> 链接 </a> 更"
"新密码\n"
" "
#: templates/_message.html:27
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
...@@ -2534,7 +2688,7 @@ msgstr "" ...@@ -2534,7 +2688,7 @@ msgstr ""
"</a> 补充完整\n" "</a> 补充完整\n"
" " " "
#: templates/_message.html:20 #: templates/_message.html:40
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
...@@ -2665,10 +2819,6 @@ msgid "" ...@@ -2665,10 +2819,6 @@ msgid ""
"assets per user host per month, respectively." "assets per user host per month, respectively."
msgstr "以下图形分别描述一个月活跃用户和资产占所有用户主机的百分比" msgstr "以下图形分别描述一个月活跃用户和资产占所有用户主机的百分比"
#: templates/index.html:91
msgid "Host"
msgstr "主机"
#: templates/index.html:106 templates/index.html:121 #: templates/index.html:106 templates/index.html:121
msgid "Top 10 assets in a week" msgid "Top 10 assets in a week"
msgstr "一周Top10资产" msgstr "一周Top10资产"
...@@ -2787,12 +2937,12 @@ msgstr "输入" ...@@ -2787,12 +2937,12 @@ msgstr "输入"
msgid "Session" msgid "Session"
msgstr "会话" msgstr "会话"
#: terminal/forms.py:31 #: terminal/forms.py:28
msgid "Command can store in server db or ES, default to server, more see docs" msgid "Command can store in server db or ES, default to server, more see docs"
msgstr "" msgstr ""
"命令支持存储到服务器端数据库、ES中,默认存储的服务器端数据库,更多查看文档" "命令支持存储到服务器端数据库、ES中,默认存储的服务器端数据库,更多查看文档"
#: terminal/forms.py:36 #: terminal/forms.py:33
msgid "" msgid ""
"Replay file can store in server disk, AWS S3, Aliyun OSS, default to server, " "Replay file can store in server disk, AWS S3, Aliyun OSS, default to server, "
"more see docs" "more see docs"
...@@ -2812,43 +2962,43 @@ msgstr "SSH端口" ...@@ -2812,43 +2962,43 @@ msgstr "SSH端口"
msgid "HTTP Port" msgid "HTTP Port"
msgstr "HTTP端口" msgstr "HTTP端口"
#: terminal/models.py:104 #: terminal/models.py:109
msgid "Session Online" msgid "Session Online"
msgstr "在线会话" msgstr "在线会话"
#: terminal/models.py:105 #: terminal/models.py:110
msgid "CPU Usage" msgid "CPU Usage"
msgstr "CPU使用" msgstr "CPU使用"
#: terminal/models.py:106 #: terminal/models.py:111
msgid "Memory Used" msgid "Memory Used"
msgstr "内存使用" msgstr "内存使用"
#: terminal/models.py:107 #: terminal/models.py:112
msgid "Connections" msgid "Connections"
msgstr "连接数" msgstr "连接数"
#: terminal/models.py:108 #: terminal/models.py:113
msgid "Threads" msgid "Threads"
msgstr "线程数" msgstr "线程数"
#: terminal/models.py:109 #: terminal/models.py:114
msgid "Boot Time" msgid "Boot Time"
msgstr "运行时间" msgstr "运行时间"
#: terminal/models.py:138 terminal/templates/terminal/session_list.html:104 #: terminal/models.py:143 terminal/templates/terminal/session_list.html:104
msgid "Replay" msgid "Replay"
msgstr "回放" msgstr "回放"
#: terminal/models.py:142 #: terminal/models.py:147
msgid "Date last active" msgid "Date last active"
msgstr "最后活跃日期" msgstr "最后活跃日期"
#: terminal/models.py:144 #: terminal/models.py:149
msgid "Date end" msgid "Date end"
msgstr "结束日期" msgstr "结束日期"
#: terminal/models.py:161 #: terminal/models.py:166
msgid "Args" msgid "Args"
msgstr "参数" msgstr "参数"
...@@ -2994,70 +3144,74 @@ msgstr "你可以使用ssh客户端工具连接终端" ...@@ -2994,70 +3144,74 @@ msgstr "你可以使用ssh客户端工具连接终端"
msgid "Log in frequently and try again later" msgid "Log in frequently and try again later"
msgstr "登录频繁, 稍后重试" msgstr "登录频繁, 稍后重试"
#: users/api/auth.py:79 #: users/api/auth.py:67
msgid "The user {} password has expired, please update."
msgstr "用户 {} 密码已经过期,请更新。"
#: users/api/auth.py:92
msgid "Please carry seed value and conduct MFA secondary certification" msgid "Please carry seed value and conduct MFA secondary certification"
msgstr "请携带seed值, 进行MFA二次认证" msgstr "请携带seed值, 进行MFA二次认证"
#: users/api/auth.py:192 #: users/api/auth.py:204
msgid "Please verify the user name and password first" msgid "Please verify the user name and password first"
msgstr "请先进行用户名和密码验证" msgstr "请先进行用户名和密码验证"
#: users/api/auth.py:204 #: users/api/auth.py:216
msgid "MFA certification failed" msgid "MFA certification failed"
msgstr "MFA认证失败" msgstr "MFA认证失败"
#: users/api/user.py:135 #: users/api/user.py:140
msgid "Could not reset self otp, use profile reset instead" msgid "Could not reset self otp, use profile reset instead"
msgstr "不能再该页面重置MFA, 请去个人信息页面重置" msgstr "不能再该页面重置MFA, 请去个人信息页面重置"
#: users/authentication.py:56 #: users/authentication.py:53
msgid "Invalid signature header. No credentials provided." msgid "Invalid signature header. No credentials provided."
msgstr "" msgstr ""
#: users/authentication.py:59 #: users/authentication.py:56
msgid "Invalid signature header. Signature string should not contain spaces." msgid "Invalid signature header. Signature string should not contain spaces."
msgstr "" msgstr ""
#: users/authentication.py:66 #: users/authentication.py:63
msgid "Invalid signature header. Format like AccessKeyId:Signature" msgid "Invalid signature header. Format like AccessKeyId:Signature"
msgstr "" msgstr ""
#: users/authentication.py:70 #: users/authentication.py:67
msgid "" msgid ""
"Invalid signature header. Signature string should not contain invalid " "Invalid signature header. Signature string should not contain invalid "
"characters." "characters."
msgstr "" msgstr ""
#: users/authentication.py:90 users/authentication.py:106 #: users/authentication.py:87 users/authentication.py:103
msgid "Invalid signature." msgid "Invalid signature."
msgstr "" msgstr ""
#: users/authentication.py:97 #: users/authentication.py:94
msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT" msgid "HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT"
msgstr "" msgstr ""
#: users/authentication.py:102 #: users/authentication.py:99
msgid "Expired, more than 15 minutes" msgid "Expired, more than 15 minutes"
msgstr "" msgstr ""
#: users/authentication.py:109 #: users/authentication.py:106
msgid "User disabled." msgid "User disabled."
msgstr "用户已禁用" msgstr "用户已禁用"
#: users/authentication.py:124 #: users/authentication.py:121
msgid "Invalid token header. No credentials provided." msgid "Invalid token header. No credentials provided."
msgstr "" msgstr ""
#: users/authentication.py:127 #: users/authentication.py:124
msgid "Invalid token header. Sign string should not contain spaces." msgid "Invalid token header. Sign string should not contain spaces."
msgstr "" msgstr ""
#: users/authentication.py:134 #: users/authentication.py:131
msgid "" msgid ""
"Invalid token header. Sign string should not contain invalid characters." "Invalid token header. Sign string should not contain invalid characters."
msgstr "" msgstr ""
#: users/authentication.py:145 #: users/authentication.py:142
msgid "Invalid token or cache refreshed." msgid "Invalid token or cache refreshed."
msgstr "" msgstr ""
...@@ -3065,7 +3219,7 @@ msgstr "" ...@@ -3065,7 +3219,7 @@ msgstr ""
msgid "MFA code" msgid "MFA code"
msgstr "MFA 验证码" msgstr "MFA 验证码"
#: users/forms.py:52 users/models/user.py:61 #: users/forms.py:52 users/models/user.py:63
#: users/templates/users/_select_user_modal.html:15 #: users/templates/users/_select_user_modal.html:15
#: users/templates/users/user_detail.html:87 #: users/templates/users/user_detail.html:87
#: users/templates/users/user_list.html:25 #: users/templates/users/user_list.html:25
...@@ -3073,11 +3227,11 @@ msgstr "MFA 验证码" ...@@ -3073,11 +3227,11 @@ msgstr "MFA 验证码"
msgid "Role" msgid "Role"
msgstr "角色" msgstr "角色"
#: users/forms.py:55 users/forms.py:228 #: users/forms.py:55 users/forms.py:226
msgid "ssh public key" msgid "ssh public key"
msgstr "ssh公钥" msgstr "ssh公钥"
#: users/forms.py:56 users/forms.py:229 #: users/forms.py:56 users/forms.py:227
msgid "ssh-rsa AAAA..." msgid "ssh-rsa AAAA..."
msgstr "" msgstr ""
...@@ -3085,19 +3239,19 @@ msgstr "" ...@@ -3085,19 +3239,19 @@ msgstr ""
msgid "Paste user id_rsa.pub here." msgid "Paste user id_rsa.pub here."
msgstr "复制用户公钥到这里" msgstr "复制用户公钥到这里"
#: users/forms.py:76 users/templates/users/user_detail.html:215 #: users/forms.py:76 users/templates/users/user_detail.html:219
msgid "Join user groups" msgid "Join user groups"
msgstr "添加到用户组" msgstr "添加到用户组"
#: users/forms.py:110 users/forms.py:243 #: users/forms.py:110 users/forms.py:241
msgid "Public key should not be the same as your old one." msgid "Public key should not be the same as your old one."
msgstr "不能和原来的密钥相同" msgstr "不能和原来的密钥相同"
#: users/forms.py:114 users/forms.py:247 users/serializers.py:49 #: users/forms.py:114 users/forms.py:245 users/serializers/v1.py:51
msgid "Not a valid ssh public key" msgid "Not a valid ssh public key"
msgstr "ssh密钥不合法" msgstr "ssh密钥不合法"
#: users/forms.py:154 #: users/forms.py:153
msgid "" msgid ""
"Tip: when enabled, you will enter the MFA binding process the next time you " "Tip: when enabled, you will enter the MFA binding process the next time you "
"log in. you can also directly bind in \"personal information -> quick " "log in. you can also directly bind in \"personal information -> quick "
...@@ -3106,11 +3260,11 @@ msgstr "" ...@@ -3106,11 +3260,11 @@ msgstr ""
"提示:启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修" "提示:启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修"
"改->更改MFA设置)中直接绑定!" "改->更改MFA设置)中直接绑定!"
#: users/forms.py:164 #: users/forms.py:163
msgid "* Enable MFA authentication to make the account more secure." msgid "* Enable MFA authentication to make the account more secure."
msgstr "* 启用MFA认证,使账号更加安全." msgstr "* 启用MFA认证,使账号更加安全."
#: users/forms.py:174 #: users/forms.py:173
msgid "" msgid ""
"In order to protect you and your company, please keep your account, password " "In order to protect you and your company, please keep your account, password "
"and key sensitive information properly. (for example: setting complex " "and key sensitive information properly. (for example: setting complex "
...@@ -3119,41 +3273,41 @@ msgstr "" ...@@ -3119,41 +3273,41 @@ msgstr ""
"为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:" "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:"
"设置复杂密码,启用MFA认证)" "设置复杂密码,启用MFA认证)"
#: users/forms.py:181 users/templates/users/first_login.html:48 #: users/forms.py:180 users/templates/users/first_login.html:48
#: users/templates/users/first_login.html:107 #: users/templates/users/first_login.html:107
#: users/templates/users/first_login.html:130 #: users/templates/users/first_login.html:130
msgid "Finish" msgid "Finish"
msgstr "完成" msgstr "完成"
#: users/forms.py:187 #: users/forms.py:186
msgid "Old password" msgid "Old password"
msgstr "原来密码" msgstr "原来密码"
#: users/forms.py:192 #: users/forms.py:191
msgid "New password" msgid "New password"
msgstr "新密码" msgstr "新密码"
#: users/forms.py:197 #: users/forms.py:196
msgid "Confirm password" msgid "Confirm password"
msgstr "确认密码" msgstr "确认密码"
#: users/forms.py:207 #: users/forms.py:206
msgid "Old password error" msgid "Old password error"
msgstr "原来密码错误" msgstr "原来密码错误"
#: users/forms.py:215 #: users/forms.py:214
msgid "Password does not match" msgid "Password does not match"
msgstr "密码不一致" msgstr "密码不一致"
#: users/forms.py:226 #: users/forms.py:224
msgid "Automatically configure and download the SSH key" msgid "Automatically configure and download the SSH key"
msgstr "自动配置并下载SSH密钥" msgstr "自动配置并下载SSH密钥"
#: users/forms.py:230 #: users/forms.py:228
msgid "Paste your id_rsa.pub here." msgid "Paste your id_rsa.pub here."
msgstr "复制你的公钥到这里" msgstr "复制你的公钥到这里"
#: users/forms.py:258 users/models/user.py:81 #: users/forms.py:256 users/models/user.py:83
#: users/templates/users/first_login.html:42 #: users/templates/users/first_login.html:42
#: users/templates/users/user_password_update.html:46 #: users/templates/users/user_password_update.html:46
#: users/templates/users/user_profile.html:68 #: users/templates/users/user_profile.html:68
...@@ -3162,57 +3316,65 @@ msgstr "复制你的公钥到这里" ...@@ -3162,57 +3316,65 @@ msgstr "复制你的公钥到这里"
msgid "Public key" msgid "Public key"
msgstr "ssh公钥" msgstr "ssh公钥"
#: users/forms.py:265 users/forms.py:270 users/forms.py:316 #: users/forms.py:263 users/forms.py:268 users/forms.py:314
#: xpack/plugins/orgs/forms.py:30 #: xpack/plugins/orgs/forms.py:30
msgid "Select users" msgid "Select users"
msgstr "选择用户" msgstr "选择用户"
#: users/models/authentication.py:36 #: users/models/authentication.py:39
msgid "Private Token" msgid "Private Token"
msgstr "ssh密钥" msgstr "ssh密钥"
#: users/models/authentication.py:50 users/templates/users/user_detail.html:98 #: users/models/authentication.py:53 users/templates/users/user_detail.html:98
msgid "Disabled" msgid "Disabled"
msgstr "禁用" msgstr "禁用"
#: users/models/authentication.py:52 users/models/authentication.py:60 #: users/models/authentication.py:55 users/models/authentication.py:65
msgid "-" msgid "-"
msgstr "" msgstr ""
#: users/models/authentication.py:61 #: users/models/authentication.py:66
msgid "Username/password check failed" msgid "Username/password check failed"
msgstr "用户名/密码 校验失败" msgstr "用户名/密码 校验失败"
#: users/models/authentication.py:62 #: users/models/authentication.py:67
msgid "MFA authentication failed" msgid "MFA authentication failed"
msgstr "MFA 认证失败" msgstr "MFA 认证失败"
#: users/models/authentication.py:67 xpack/plugins/cloud/models.py:184 #: users/models/authentication.py:68
msgid "Username does not exist"
msgstr "用户名不存在"
#: users/models/authentication.py:69
msgid "Password expired"
msgstr "密码过期"
#: users/models/authentication.py:74 xpack/plugins/cloud/models.py:184
#: xpack/plugins/cloud/models.py:198 #: xpack/plugins/cloud/models.py:198
msgid "Failed" msgid "Failed"
msgstr "失败" msgstr "失败"
#: users/models/authentication.py:71 #: users/models/authentication.py:78
msgid "Login type" msgid "Login type"
msgstr "登录方式" msgstr "登录方式"
#: users/models/authentication.py:72 #: users/models/authentication.py:79
msgid "Login ip" msgid "Login ip"
msgstr "登录IP" msgstr "登录IP"
#: users/models/authentication.py:73 #: users/models/authentication.py:80
msgid "Login city" msgid "Login city"
msgstr "登录城市" msgstr "登录城市"
#: users/models/authentication.py:74 #: users/models/authentication.py:81
msgid "User agent" msgid "User agent"
msgstr "Agent" msgstr "Agent"
#: users/models/authentication.py:78 #: users/models/authentication.py:85
msgid "Date login" msgid "Date login"
msgstr "登录日期" msgstr "登录日期"
#: users/models/user.py:32 users/models/user.py:359 #: users/models/user.py:32 users/models/user.py:418
msgid "Administrator" msgid "Administrator"
msgstr "管理员" msgstr "管理员"
...@@ -3221,13 +3383,13 @@ msgid "Application" ...@@ -3221,13 +3383,13 @@ msgid "Application"
msgstr "应用程序" msgstr "应用程序"
#: users/models/user.py:37 users/templates/users/user_profile.html:92 #: users/models/user.py:37 users/templates/users/user_profile.html:92
#: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:167
#: users/templates/users/user_profile.html:166 #: users/templates/users/user_profile.html:170
msgid "Disable" msgid "Disable"
msgstr "禁用" msgstr "禁用"
#: users/models/user.py:38 users/templates/users/user_profile.html:90 #: users/models/user.py:38 users/templates/users/user_profile.html:90
#: users/templates/users/user_profile.html:170 #: users/templates/users/user_profile.html:174
msgid "Enable" msgid "Enable"
msgstr "启用" msgstr "启用"
...@@ -3235,26 +3397,30 @@ msgstr "启用" ...@@ -3235,26 +3397,30 @@ msgstr "启用"
msgid "Force enable" msgid "Force enable"
msgstr "强制启用" msgstr "强制启用"
#: users/models/user.py:53 users/templates/users/user_detail.html:71 #: users/models/user.py:55 users/templates/users/user_detail.html:71
#: users/templates/users/user_profile.html:59 #: users/templates/users/user_profile.html:59
msgid "Email" msgid "Email"
msgstr "邮件" msgstr "邮件"
#: users/models/user.py:64 #: users/models/user.py:66
msgid "Avatar" msgid "Avatar"
msgstr "头像" msgstr "头像"
#: users/models/user.py:67 users/templates/users/user_detail.html:82 #: users/models/user.py:69 users/templates/users/user_detail.html:82
msgid "Wechat" msgid "Wechat"
msgstr "微信" msgstr "微信"
#: users/models/user.py:96 users/templates/users/user_detail.html:103 #: users/models/user.py:98 users/templates/users/user_detail.html:103
#: users/templates/users/user_list.html:27 #: users/templates/users/user_list.html:27
#: users/templates/users/user_profile.html:100 #: users/templates/users/user_profile.html:100
msgid "Source" msgid "Source"
msgstr "用户来源" msgstr "用户来源"
#: users/models/user.py:362 #: users/models/user.py:102
msgid "Date password last updated"
msgstr "最后更新密码日期"
#: users/models/user.py:421
msgid "Administrator is the super user of system" msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员" msgstr "Administrator是初始的超级管理员"
...@@ -3350,7 +3516,7 @@ msgstr "获取更多信息" ...@@ -3350,7 +3516,7 @@ msgstr "获取更多信息"
#: users/templates/users/forgot_password.html:11 #: users/templates/users/forgot_password.html:11
#: users/templates/users/forgot_password.html:27 #: users/templates/users/forgot_password.html:27
#: users/templates/users/login.html:79 #: users/templates/users/login.html:83
msgid "Forgot password" msgid "Forgot password"
msgstr "忘记密码" msgstr "忘记密码"
...@@ -3395,10 +3561,22 @@ msgstr "" ...@@ -3395,10 +3561,22 @@ msgstr ""
msgid "Changes the world, starting with a little bit." msgid "Changes the world, starting with a little bit."
msgstr "改变世界,从一点点开始。" msgstr "改变世界,从一点点开始。"
#: users/templates/users/login.html:55 #: users/templates/users/login.html:54
msgid "The user password has expired"
msgstr "用户密码已过期"
#: users/templates/users/login.html:57
msgid "Captcha invalid" msgid "Captcha invalid"
msgstr "验证码错误" msgstr "验证码错误"
#: users/templates/users/login.html:89
msgid "More login options"
msgstr "更多登录方式"
#: users/templates/users/login.html:93
msgid "Keycloak"
msgstr ""
#: users/templates/users/login_otp.html:46 #: users/templates/users/login_otp.html:46
#: users/templates/users/user_detail.html:91 #: users/templates/users/user_detail.html:91
#: users/templates/users/user_profile.html:85 #: users/templates/users/user_profile.html:85
...@@ -3459,7 +3637,7 @@ msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry" ...@@ -3459,7 +3637,7 @@ msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry"
msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry" msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry"
#: users/templates/users/reset_password.html:46 #: users/templates/users/reset_password.html:46
#: users/templates/users/user_detail.html:367 users/utils.py:80 #: users/templates/users/user_detail.html:371 users/utils.py:81
msgid "Reset password" msgid "Reset password"
msgstr "重置密码" msgstr "重置密码"
...@@ -3530,7 +3708,7 @@ msgid "Reset link will be generated and sent to the user. " ...@@ -3530,7 +3708,7 @@ msgid "Reset link will be generated and sent to the user. "
msgstr "生成重置密码连接,通过邮件发送给用户" msgstr "生成重置密码连接,通过邮件发送给用户"
#: users/templates/users/user_detail.html:19 #: users/templates/users/user_detail.html:19
#: users/templates/users/user_granted_asset.html:18 users/views/user.py:194 #: users/templates/users/user_granted_asset.html:18 users/views/user.py:193
msgid "User detail" msgid "User detail"
msgstr "用户详情" msgstr "用户详情"
...@@ -3550,79 +3728,84 @@ msgstr "强制启用" ...@@ -3550,79 +3728,84 @@ msgstr "强制启用"
msgid "Last login" msgid "Last login"
msgstr "最后登录" msgstr "最后登录"
#: users/templates/users/user_detail.html:154 #: users/templates/users/user_detail.html:123
#: users/templates/users/user_profile.html:112
msgid "Last password updated"
msgstr "最后更新密码"
#: users/templates/users/user_detail.html:158
msgid "Force enabled MFA" msgid "Force enabled MFA"
msgstr "强制启用MFA" msgstr "强制启用MFA"
#: users/templates/users/user_detail.html:169 #: users/templates/users/user_detail.html:173
msgid "Reset MFA" msgid "Reset MFA"
msgstr "重置MFA" msgstr "重置MFA"
#: users/templates/users/user_detail.html:177 #: users/templates/users/user_detail.html:181
msgid "Send reset password mail" msgid "Send reset password mail"
msgstr "发送重置密码邮件" msgstr "发送重置密码邮件"
#: users/templates/users/user_detail.html:180 #: users/templates/users/user_detail.html:184
#: users/templates/users/user_detail.html:188 #: users/templates/users/user_detail.html:192
msgid "Send" msgid "Send"
msgstr "发送" msgstr "发送"
#: users/templates/users/user_detail.html:185 #: users/templates/users/user_detail.html:189
msgid "Send reset ssh key mail" msgid "Send reset ssh key mail"
msgstr "发送重置密钥邮件" msgstr "发送重置密钥邮件"
#: users/templates/users/user_detail.html:193 #: users/templates/users/user_detail.html:197
#: users/templates/users/user_detail.html:455 #: users/templates/users/user_detail.html:459
msgid "Unblock user" msgid "Unblock user"
msgstr "解除登录限制" msgstr "解除登录限制"
#: users/templates/users/user_detail.html:196 #: users/templates/users/user_detail.html:200
msgid "Unblock" msgid "Unblock"
msgstr "解除" msgstr "解除"
#: users/templates/users/user_detail.html:310 #: users/templates/users/user_detail.html:314
msgid "Goto profile page enable MFA" msgid "Goto profile page enable MFA"
msgstr "请去个人信息页面启用自己的MFA" msgstr "请去个人信息页面启用自己的MFA"
#: users/templates/users/user_detail.html:366 #: users/templates/users/user_detail.html:370
msgid "An e-mail has been sent to the user`s mailbox." msgid "An e-mail has been sent to the user`s mailbox."
msgstr "已发送邮件到用户邮箱" msgstr "已发送邮件到用户邮箱"
#: users/templates/users/user_detail.html:377 #: users/templates/users/user_detail.html:381
msgid "This will reset the user password and send a reset mail" msgid "This will reset the user password and send a reset mail"
msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱"
#: users/templates/users/user_detail.html:392 #: users/templates/users/user_detail.html:396
msgid "" msgid ""
"The reset-ssh-public-key E-mail has been sent successfully. Please inform " "The reset-ssh-public-key E-mail has been sent successfully. Please inform "
"the user to update his new ssh public key." "the user to update his new ssh public key."
msgstr "重设密钥邮件将会发送到用户邮箱" msgstr "重设密钥邮件将会发送到用户邮箱"
#: users/templates/users/user_detail.html:393 #: users/templates/users/user_detail.html:397
msgid "Reset SSH public key" msgid "Reset SSH public key"
msgstr "重置SSH密钥" msgstr "重置SSH密钥"
#: users/templates/users/user_detail.html:403 #: users/templates/users/user_detail.html:407
msgid "This will reset the user public key and send a reset mail" msgid "This will reset the user public key and send a reset mail"
msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱"
#: users/templates/users/user_detail.html:421 #: users/templates/users/user_detail.html:425
#: users/templates/users/user_profile.html:221 #: users/templates/users/user_profile.html:225
msgid "Successfully updated the SSH public key." msgid "Successfully updated the SSH public key."
msgstr "更新ssh密钥成功" msgstr "更新ssh密钥成功"
#: users/templates/users/user_detail.html:422
#: users/templates/users/user_detail.html:426 #: users/templates/users/user_detail.html:426
#: users/templates/users/user_profile.html:222 #: users/templates/users/user_detail.html:430
#: users/templates/users/user_profile.html:227 #: users/templates/users/user_profile.html:226
#: users/templates/users/user_profile.html:231
msgid "User SSH public key update" msgid "User SSH public key update"
msgstr "ssh密钥" msgstr "ssh密钥"
#: users/templates/users/user_detail.html:471 #: users/templates/users/user_detail.html:475
msgid "After unlocking the user, the user can log in normally." msgid "After unlocking the user, the user can log in normally."
msgstr "解除用户登录限制后,此用户即可正常登录" msgstr "解除用户登录限制后,此用户即可正常登录"
#: users/templates/users/user_detail.html:485 #: users/templates/users/user_detail.html:489
msgid "Reset user MFA success" msgid "Reset user MFA success"
msgstr "重置用户MFA成功" msgstr "重置用户MFA成功"
...@@ -3641,37 +3824,37 @@ msgstr "添加用户" ...@@ -3641,37 +3824,37 @@ msgstr "添加用户"
msgid "Create user group" msgid "Create user group"
msgstr "创建用户组" msgstr "创建用户组"
#: users/templates/users/user_group_list.html:82 #: users/templates/users/user_group_list.html:83
msgid "This will delete the selected groups !!!" msgid "This will delete the selected groups !!!"
msgstr "删除选择组" msgstr "删除选择组"
#: users/templates/users/user_group_list.html:91 #: users/templates/users/user_group_list.html:92
msgid "UserGroups Deleted." msgid "UserGroups Deleted."
msgstr "用户组删除" msgstr "用户组删除"
#: users/templates/users/user_group_list.html:92 #: users/templates/users/user_group_list.html:93
#: users/templates/users/user_group_list.html:97 #: users/templates/users/user_group_list.html:98
msgid "UserGroups Delete" msgid "UserGroups Delete"
msgstr "用户组删除" msgstr "用户组删除"
#: users/templates/users/user_group_list.html:96 #: users/templates/users/user_group_list.html:97
msgid "UserGroup Deleting failed." msgid "UserGroup Deleting failed."
msgstr "用户组删除失败" msgstr "用户组删除失败"
#: users/templates/users/user_list.html:196 #: users/templates/users/user_list.html:200
msgid "This will delete the selected users !!!" msgid "This will delete the selected users !!!"
msgstr "删除选中用户 !!!" msgstr "删除选中用户 !!!"
#: users/templates/users/user_list.html:205 #: users/templates/users/user_list.html:209
msgid "User Deleted." msgid "User Deleted."
msgstr "已被删除" msgstr "已被删除"
#: users/templates/users/user_list.html:206 #: users/templates/users/user_list.html:210
#: users/templates/users/user_list.html:211 #: users/templates/users/user_list.html:215
msgid "User Delete" msgid "User Delete"
msgstr "删除" msgstr "删除"
#: users/templates/users/user_list.html:210 #: users/templates/users/user_list.html:214
msgid "User Deleting failed." msgid "User Deleting failed."
msgstr "用户删除失败" msgstr "用户删除失败"
...@@ -3720,32 +3903,32 @@ msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接 ...@@ -3720,32 +3903,32 @@ msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接
msgid "Administrator Settings force MFA login" msgid "Administrator Settings force MFA login"
msgstr "管理员设置强制使用MFA登录" msgstr "管理员设置强制使用MFA登录"
#: users/templates/users/user_profile.html:116 users/views/user.py:230 #: users/templates/users/user_profile.html:120 users/views/user.py:229
#: users/views/user.py:284 #: users/views/user.py:283
msgid "User groups" msgid "User groups"
msgstr "用户组" msgstr "用户组"
#: users/templates/users/user_profile.html:148 #: users/templates/users/user_profile.html:152
msgid "Update password" msgid "Update password"
msgstr "更改密码" msgstr "更改密码"
#: users/templates/users/user_profile.html:156 #: users/templates/users/user_profile.html:160
msgid "Set MFA" msgid "Set MFA"
msgstr "设置MFA" msgstr "设置MFA"
#: users/templates/users/user_profile.html:178 #: users/templates/users/user_profile.html:182
msgid "Update MFA" msgid "Update MFA"
msgstr "更改MFA" msgstr "更改MFA"
#: users/templates/users/user_profile.html:187 #: users/templates/users/user_profile.html:191
msgid "Update SSH public key" msgid "Update SSH public key"
msgstr "更改SSH密钥" msgstr "更改SSH密钥"
#: users/templates/users/user_profile.html:195 #: users/templates/users/user_profile.html:199
msgid "Reset public key and download" msgid "Reset public key and download"
msgstr "重置并下载SSH密钥" msgstr "重置并下载SSH密钥"
#: users/templates/users/user_profile.html:225 #: users/templates/users/user_profile.html:229
msgid "Failed to update SSH public key." msgid "Failed to update SSH public key."
msgstr "更新密钥失败" msgstr "更新密钥失败"
...@@ -3775,11 +3958,11 @@ msgstr "新的公钥已设置成功,请下载对应的私钥" ...@@ -3775,11 +3958,11 @@ msgstr "新的公钥已设置成功,请下载对应的私钥"
msgid "Update user" msgid "Update user"
msgstr "更新用户" msgstr "更新用户"
#: users/utils.py:41 #: users/utils.py:42
msgid "Create account successfully" msgid "Create account successfully"
msgstr "创建账户成功" msgstr "创建账户成功"
#: users/utils.py:43 #: users/utils.py:44
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
...@@ -3824,7 +4007,7 @@ msgstr "" ...@@ -3824,7 +4007,7 @@ msgstr ""
" </br>\n" " </br>\n"
" " " "
#: users/utils.py:82 #: users/utils.py:83
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
...@@ -3868,11 +4051,64 @@ msgstr "" ...@@ -3868,11 +4051,64 @@ msgstr ""
" </br>\n" " </br>\n"
" " " "
#: users/utils.py:113 #: users/utils.py:114
msgid "Security notice"
msgstr "安全通知"
#: users/utils.py:116
#, python-format
msgid ""
"\n"
" Hello %(name)s:\n"
" </br>\n"
" Your password will expire in %(date_password_expired)s,\n"
" </br>\n"
" For your account security, please click on the link below to update your "
"password in time\n"
" </br>\n"
" <a href=\"%(update_password_url)s\">Click here update password</a>\n"
" </br>\n"
" If your password has expired, please click \n"
" <a href=\"%(forget_password_url)s?email=%(email)s\">Password expired</"
"a> \n"
" to apply for a password reset email.\n"
"\n"
" </br>\n"
" ---\n"
"\n"
" </br>\n"
" <a href=\"%(login_url)s\">Login direct</a>\n"
"\n"
" </br>\n"
" "
msgstr ""
"\n"
" 您好 %(name)s:\n"
" </br>\n"
" 您的密码会在 %(date_password_expired)s 过期,\n"
" </br>\n"
" 为了您的账号安全,请点击下面的链接及时更新密码\n"
" </br>\n"
" <a href=\"%(update_password_url)s\">请点击这里更新密码</a>\n"
" </br>\n"
" 如果您的密码已经过期,请点击 \n"
" <a href=\"%(forget_password_url)s?email=%(email)s\">密码过期</a> \n"
" 申请一份重置密码邮件。\n"
"\n"
" </br>\n"
" ---\n"
"\n"
" </br>\n"
" <a href=\"%(login_url)s\">直接登录</a>\n"
"\n"
" </br>\n"
" "
#: users/utils.py:152
msgid "SSH Key Reset" msgid "SSH Key Reset"
msgstr "重置ssh密钥" msgstr "重置ssh密钥"
#: users/utils.py:115 #: users/utils.py:154
#, python-format #, python-format
msgid "" msgid ""
"\n" "\n"
...@@ -3897,22 +4133,18 @@ msgstr "" ...@@ -3897,22 +4133,18 @@ msgstr ""
" </br>\n" " </br>\n"
" " " "
#: users/utils.py:148 #: users/utils.py:187
msgid "User not exist" msgid "User not exist"
msgstr "用户不存在" msgstr "用户不存在"
#: users/utils.py:150 #: users/utils.py:189
msgid "Disabled or expired" msgid "Disabled or expired"
msgstr "禁用或失效" msgstr "禁用或失效"
#: users/utils.py:163 #: users/utils.py:202
msgid "Password or SSH public key invalid" msgid "Password or SSH public key invalid"
msgstr "密码或密钥不合法" msgstr "密码或密钥不合法"
#: users/utils.py:286 users/utils.py:296
msgid "Bit"
msgstr " 位"
#: users/views/group.py:29 #: users/views/group.py:29
msgid "User group list" msgid "User group list"
msgstr "用户组列表" msgstr "用户组列表"
...@@ -3929,100 +4161,104 @@ msgstr "用户组授权资产" ...@@ -3929,100 +4161,104 @@ msgstr "用户组授权资产"
msgid "Please enable cookies and try again." msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie" msgstr "设置你的浏览器支持cookie"
#: users/views/login.py:175 users/views/user.py:526 users/views/user.py:551 #: users/views/login.py:191 users/views/user.py:524 users/views/user.py:549
msgid "MFA code invalid, or ntp sync server time" msgid "MFA code invalid, or ntp sync server time"
msgstr "MFA验证码不正确,或者服务器端时间不对" msgstr "MFA验证码不正确,或者服务器端时间不对"
#: users/views/login.py:204 #: users/views/login.py:223
msgid "Logout success" msgid "Logout success"
msgstr "退出登录成功" msgstr "退出登录成功"
#: users/views/login.py:205 #: users/views/login.py:224
msgid "Logout success, return login page" msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面" msgstr "退出登录成功,返回到登录页面"
#: users/views/login.py:221 #: users/views/login.py:240
msgid "Email address invalid, please input again" msgid "Email address invalid, please input again"
msgstr "邮箱地址错误,重新输入" msgstr "邮箱地址错误,重新输入"
#: users/views/login.py:234 #: users/views/login.py:243
msgid "User auth from {}, go there change password"
msgstr "用户认证源来自 {}, 请去相应系统修改密码"
#: users/views/login.py:256
msgid "Send reset password message" msgid "Send reset password message"
msgstr "发送重置密码邮件" msgstr "发送重置密码邮件"
#: users/views/login.py:235 #: users/views/login.py:257
msgid "Send reset password mail success, login your mail box and follow it " msgid "Send reset password mail success, login your mail box and follow it "
msgstr "" msgstr ""
"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" "发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)"
#: users/views/login.py:248 #: users/views/login.py:270
msgid "Reset password success" msgid "Reset password success"
msgstr "重置密码成功" msgstr "重置密码成功"
#: users/views/login.py:249 #: users/views/login.py:271
msgid "Reset password success, return to login page" msgid "Reset password success, return to login page"
msgstr "重置密码成功,返回到登录页面" msgstr "重置密码成功,返回到登录页面"
#: users/views/login.py:270 users/views/login.py:283 #: users/views/login.py:286 users/views/login.py:302
msgid "Token invalid or expired" msgid "Token invalid or expired"
msgstr "Token错误或失效" msgstr "Token错误或失效"
#: users/views/login.py:279 #: users/views/login.py:298
msgid "Password not same" msgid "Password not same"
msgstr "密码不一致" msgstr "密码不一致"
#: users/views/login.py:289 users/views/user.py:127 users/views/user.py:422 #: users/views/login.py:308 users/views/user.py:126 users/views/user.py:420
msgid "* Your password does not meet the requirements" msgid "* Your password does not meet the requirements"
msgstr "* 您的密码不符合要求" msgstr "* 您的密码不符合要求"
#: users/views/login.py:327 #: users/views/login.py:346
msgid "First login" msgid "First login"
msgstr "首次登陆" msgstr "首次登陆"
#: users/views/user.py:144 #: users/views/user.py:143
msgid "Bulk update user success" msgid "Bulk update user success"
msgstr "批量更新用户成功" msgstr "批量更新用户成功"
#: users/views/user.py:174 #: users/views/user.py:173
msgid "Bulk update user" msgid "Bulk update user"
msgstr "批量更新用户" msgstr "批量更新用户"
#: users/views/user.py:259 #: users/views/user.py:258
msgid "Invalid file." msgid "Invalid file."
msgstr "文件不合法" msgstr "文件不合法"
#: users/views/user.py:355 #: users/views/user.py:354
msgid "User granted assets" msgid "User granted assets"
msgstr "用户授权资产" msgstr "用户授权资产"
#: users/views/user.py:386 #: users/views/user.py:385
msgid "Profile setting" msgid "Profile setting"
msgstr "个人信息设置" msgstr "个人信息设置"
#: users/views/user.py:405 #: users/views/user.py:404
msgid "Password update" msgid "Password update"
msgstr "密码更新" msgstr "密码更新"
#: users/views/user.py:440 #: users/views/user.py:438
msgid "Public key update" msgid "Public key update"
msgstr "密钥更新" msgstr "密钥更新"
#: users/views/user.py:481 #: users/views/user.py:479
msgid "Password invalid" msgid "Password invalid"
msgstr "用户名或密码无效" msgstr "用户名或密码无效"
#: users/views/user.py:581 #: users/views/user.py:579
msgid "MFA enable success" msgid "MFA enable success"
msgstr "MFA 绑定成功" msgstr "MFA 绑定成功"
#: users/views/user.py:582 #: users/views/user.py:580
msgid "MFA enable success, return login page" msgid "MFA enable success, return login page"
msgstr "MFA 绑定成功,返回到登录页面" msgstr "MFA 绑定成功,返回到登录页面"
#: users/views/user.py:584 #: users/views/user.py:582
msgid "MFA disable success" msgid "MFA disable success"
msgstr "MFA 解绑成功" msgstr "MFA 解绑成功"
#: users/views/user.py:585 #: users/views/user.py:583
msgid "MFA disable success, return login page" msgid "MFA disable success, return login page"
msgstr "MFA 解绑成功,返回登录页面" msgstr "MFA 解绑成功,返回登录页面"
...@@ -4160,12 +4396,6 @@ msgstr "同步实例任务历史" ...@@ -4160,12 +4396,6 @@ msgstr "同步实例任务历史"
msgid "Instance" msgid "Instance"
msgstr "实例" msgstr "实例"
#: xpack/plugins/cloud/models.py:206
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:83
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:64
msgid "Region"
msgstr "地域"
#: xpack/plugins/cloud/providers/base.py:73 #: xpack/plugins/cloud/providers/base.py:73
msgid "任务执行开始: {}" msgid "任务执行开始: {}"
msgstr "" msgstr ""
...@@ -4339,6 +4569,24 @@ msgstr "创建组织" ...@@ -4339,6 +4569,24 @@ msgstr "创建组织"
msgid "Update org" msgid "Update org"
msgstr "更新组织" msgstr "更新组织"
#~ msgid "Bit"
#~ msgstr " 位"
#, fuzzy
#~| msgid "Delete succeed"
#~ msgid "Delete success"
#~ msgstr "删除成功"
#~ msgid ""
#~ "Set terminal storage setting, `default` is the using as default,You can "
#~ "set other storage and some terminal using"
#~ msgstr "设置终端命令存储,default是默认用的存储方式"
#~ msgid ""
#~ "Set replay storage setting, `default` is the using as default,You can set "
#~ "other storage and some terminal using"
#~ msgstr "设置终端录像存储,default是默认用的存储方式"
#~ msgid "Sync instance task detail" #~ msgid "Sync instance task detail"
#~ msgstr "同步实例任务详情" #~ msgstr "同步实例任务详情"
......
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-08-08 14:48+0800\n" "POT-Creation-Date: 2018-11-21 19:14+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -17,58 +17,58 @@ msgstr "" ...@@ -17,58 +17,58 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: static/js/jumpserver.js:158 #: static/js/jumpserver.js:168
msgid "Update is successful!" msgid "Update is successful!"
msgstr "更新成功" msgstr "更新成功"
#: static/js/jumpserver.js:160 #: static/js/jumpserver.js:170
msgid "An unknown error occurred while updating.." msgid "An unknown error occurred while updating.."
msgstr "更新时发生未知错误" msgstr "更新时发生未知错误"
#: static/js/jumpserver.js:205 static/js/jumpserver.js:247 #: static/js/jumpserver.js:236 static/js/jumpserver.js:273
#: static/js/jumpserver.js:252 #: static/js/jumpserver.js:276
msgid "Error" msgid "Error"
msgstr "错误" msgstr "错误"
#: static/js/jumpserver.js:205 #: static/js/jumpserver.js:236
msgid "Being used by the asset, please unbind the asset first." msgid "Being used by the asset, please unbind the asset first."
msgstr "正在被资产使用中,请先解除资产绑定" msgstr "正在被资产使用中,请先解除资产绑定"
#: static/js/jumpserver.js:212 static/js/jumpserver.js:260 #: static/js/jumpserver.js:242 static/js/jumpserver.js:283
msgid "Delete the success" msgid "Delete the success"
msgstr "删除成功" msgstr "删除成功"
#: static/js/jumpserver.js:219 #: static/js/jumpserver.js:248
msgid "Are you sure about deleting it?" msgid "Are you sure about deleting it?"
msgstr "你确定删除吗 ?" msgstr "你确定删除吗 ?"
#: static/js/jumpserver.js:224 static/js/jumpserver.js:273 #: static/js/jumpserver.js:252 static/js/jumpserver.js:293
msgid "Cancel" msgid "Cancel"
msgstr "取消" msgstr "取消"
#: static/js/jumpserver.js:227 static/js/jumpserver.js:276 #: static/js/jumpserver.js:254 static/js/jumpserver.js:295
msgid "Confirm" msgid "Confirm"
msgstr "确认" msgstr "确认"
#: static/js/jumpserver.js:247 #: static/js/jumpserver.js:273
msgid "" msgid ""
"The organization contains undeleted information. Please try again after " "The organization contains undeleted information. Please try again after "
"deleting" "deleting"
msgstr "组织中包含未删除信息,请删除后重试" msgstr "组织中包含未删除信息,请删除后重试"
#: static/js/jumpserver.js:252 #: static/js/jumpserver.js:276
msgid "" msgid ""
"Do not perform this operation under this organization. Try again after " "Do not perform this operation under this organization. Try again after "
"switching to another organization" "switching to another organization"
msgstr "请勿在此组织下执行此操作,切换到其他组织后重试" msgstr "请勿在此组织下执行此操作,切换到其他组织后重试"
#: static/js/jumpserver.js:267 #: static/js/jumpserver.js:289
msgid "" msgid ""
"Please ensure that the following information in the organization has been " "Please ensure that the following information in the organization has been "
"deleted" "deleted"
msgstr "请确保组织内的以下信息已删除" msgstr "请确保组织内的以下信息已删除"
#: static/js/jumpserver.js:269 #: static/js/jumpserver.js:290
msgid "" msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、" "User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission" "Labels、Asset permission"
...@@ -76,32 +76,52 @@ msgstr "" ...@@ -76,32 +76,52 @@ msgstr ""
"用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权" "用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权"
"规则" "规则"
#: static/js/jumpserver.js:311 #: static/js/jumpserver.js:329
msgid "Loading ..." msgid "Loading ..."
msgstr "加载中 ..." msgstr "加载中 ..."
#: static/js/jumpserver.js:313 #: static/js/jumpserver.js:330
msgid "Search" msgid "Search"
msgstr "搜索" msgstr "搜索"
#: static/js/jumpserver.js:317 #: static/js/jumpserver.js:333
#, javascript-format #, javascript-format
msgid "Selected item %d" msgid "Selected item %d"
msgstr "选中 %d 项" msgstr "选中 %d 项"
#: static/js/jumpserver.js:322 #: static/js/jumpserver.js:337
msgid "Per page _MENU_" msgid "Per page _MENU_"
msgstr "每页 _MENU_" msgstr "每页 _MENU_"
#: static/js/jumpserver.js:324 #: static/js/jumpserver.js:338
msgid "" msgid ""
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries" "Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项" msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项"
#: static/js/jumpserver.js:328 #: static/js/jumpserver.js:341
msgid "No match" msgid "No match"
msgstr "没有匹配项" msgstr "没有匹配项"
#: static/js/jumpserver.js:330 #: static/js/jumpserver.js:342
msgid "No record" msgid "No record"
msgstr "没有记录" msgstr "没有记录"
#: static/js/jumpserver.js:701
msgid "Password minimum length {N} bits"
msgstr "密码最小长度 {N} 位"
#: static/js/jumpserver.js:702
msgid "Must contain capital letters"
msgstr "必须包含大写字母"
#: static/js/jumpserver.js:703
msgid "Must contain lowercase letters"
msgstr "必须包含小写字母"
#: static/js/jumpserver.js:704
msgid "Must contain numeric characters"
msgstr "必须包含数字字符"
#: static/js/jumpserver.js:705
msgid "Must contain special characters"
msgstr "必须包含特殊字符"
...@@ -19,13 +19,13 @@ class TaskViewSet(viewsets.ModelViewSet): ...@@ -19,13 +19,13 @@ class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all() queryset = Task.objects.all()
serializer_class = TaskSerializer serializer_class = TaskSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
label = None # label = None
help_text = '' # help_text = ''
class TaskRun(generics.RetrieveAPIView): class TaskRun(generics.RetrieveAPIView):
queryset = Task.objects.all() queryset = Task.objects.all()
serializer_class = TaskViewSet # serializer_class = TaskViewSet
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
......
...@@ -50,7 +50,7 @@ class JMSInventory(BaseInventory): ...@@ -50,7 +50,7 @@ class JMSInventory(BaseInventory):
def convert_to_ansible(self, asset, run_as_admin=False): def convert_to_ansible(self, asset, run_as_admin=False):
info = { info = {
'id': asset.id, 'id': asset.id,
'hostname': asset.hostname, 'hostname': asset.fullname,
'ip': asset.ip, 'ip': asset.ip,
'port': asset.port, 'port': asset.port,
'vars': dict(), 'vars': dict(),
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-02 09:45
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('ops', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CeleryTask',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=1024)),
('status', models.CharField(choices=[('waiting', 'waiting'), ('running', 'running'), ('finished', 'finished')], max_length=128)),
('log_path', models.CharField(blank=True, max_length=256, null=True)),
('date_published', models.DateTimeField(auto_now_add=True)),
('date_start', models.DateTimeField(null=True)),
('date_finished', models.DateTimeField(null=True)),
],
),
]
...@@ -6,7 +6,7 @@ from django.views.generic import ListView, DetailView, TemplateView ...@@ -6,7 +6,7 @@ from django.views.generic import ListView, DetailView, TemplateView
from common.mixins import DatetimeSearchMixin from common.mixins import DatetimeSearchMixin
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask from .models import Task, AdHoc, AdHocRunHistory, CeleryTask
from common.permissions import SuperUserRequiredMixin from common.permissions import SuperUserRequiredMixin, AdminUserRequiredMixin
class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView): class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
...@@ -121,6 +121,6 @@ class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView): ...@@ -121,6 +121,6 @@ class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
class CeleryTaskLogView(SuperUserRequiredMixin, DetailView): class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
template_name = 'ops/celery_task_log.html' template_name = 'ops/celery_task_log.html'
model = CeleryTask model = CeleryTask
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import viewsets from rest_framework import status
from rest_framework.views import Response
from rest_framework_bulk import BulkModelViewSet
from common.permissions import IsSuperUserOrAppUser from common.permissions import IsSuperUserOrAppUser
from .models import Organization from .models import Organization
from .serializers import OrgSerializer from .serializers import OrgSerializer, OrgReadSerializer, \
OrgMembershipUserSerializer, OrgMembershipAdminSerializer
from users.models import User, UserGroup
from assets.models import Asset, Domain, AdminUser, SystemUser, Label
from perms.models import AssetPermission
from orgs.utils import current_org
from common.utils import get_logger
from .mixins import OrgMembershipModelViewSetMixin
logger = get_logger(__file__)
class OrgViewSet(viewsets.ModelViewSet):
class OrgViewSet(BulkModelViewSet):
queryset = Organization.objects.all() queryset = Organization.objects.all()
serializer_class = OrgSerializer serializer_class = OrgSerializer
permission_classes = (IsSuperUserOrAppUser,) permission_classes = (IsSuperUserOrAppUser,)
org = None
def get_serializer_class(self):
if self.action in ('list', 'retrieve'):
return OrgReadSerializer
else:
return super().get_serializer_class()
def get_data_from_model(self, model):
if model == User:
data = model.objects.filter(orgs__id=self.org.id)
else:
data = model.objects.filter(org_id=self.org.id)
return data
def destroy(self, request, *args, **kwargs):
self.org = self.get_object()
models = [
User, UserGroup,
Asset, Domain, AdminUser, SystemUser, Label,
AssetPermission,
]
for model in models:
data = self.get_data_from_model(model)
if data:
return Response(status=status.HTTP_400_BAD_REQUEST)
else:
if str(current_org) == str(self.org):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
self.org.delete()
return Response({'msg': True}, status=status.HTTP_200_OK)
class OrgMembershipAdminsViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet):
serializer_class = OrgMembershipAdminSerializer
membership_class = Organization.admins.through
permission_classes = (IsSuperUserOrAppUser, )
class OrgMembershipUsersViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet):
serializer_class = OrgMembershipUserSerializer
membership_class = Organization.users.through
permission_classes = (IsSuperUserOrAppUser, )
# Generated by Django 2.0.7 on 2018-08-07 03:16
from django.conf import settings
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Organization',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('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')),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
('admins', models.ManyToManyField(blank=True, related_name='admin_orgs', to=settings.AUTH_USER_MODEL)),
('users', models.ManyToManyField(blank=True, related_name='orgs', to=settings.AUTH_USER_MODEL)),
],
),
]
# Generated by Django 2.1 on 2018-09-03 03:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('orgs', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='organization',
options={'verbose_name': 'Organization'},
),
]
...@@ -9,7 +9,6 @@ from django.forms import ModelForm ...@@ -9,7 +9,6 @@ from django.forms import ModelForm
from django.http.response import HttpResponseForbidden from django.http.response import HttpResponseForbidden
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from common.utils import get_logger from common.utils import get_logger
from .utils import current_org, set_current_org, set_to_root_org from .utils import current_org, set_current_org, set_to_root_org
from .models import Organization from .models import Organization
...@@ -19,7 +18,7 @@ tl = Local() ...@@ -19,7 +18,7 @@ tl = Local()
__all__ = [ __all__ = [
'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm', 'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm',
'RootOrgViewMixin', 'RootOrgViewMixin', 'OrgMembershipSerializerMixin', 'OrgMembershipModelViewSetMixin'
] ]
...@@ -176,3 +175,29 @@ class OrgModelForm(ModelForm): ...@@ -176,3 +175,29 @@ class OrgModelForm(ModelForm):
continue continue
model = field.queryset.model model = field.queryset.model
field.queryset = model.objects.all() field.queryset = model.objects.all()
class OrgMembershipSerializerMixin:
def run_validation(self, initial_data=None):
initial_data['organization'] = str(self.context['org'].id)
return super().run_validation(initial_data)
class OrgMembershipModelViewSetMixin:
org = None
membership_class = None
lookup_field = 'user'
lookup_url_kwarg = 'user_id'
http_method_names = ['get', 'post', 'delete', 'head', 'options']
def dispatch(self, request, *args, **kwargs):
self.org = Organization.objects.get(pk=kwargs.get('org_id'))
return super().dispatch(request, *args, **kwargs)
def get_serializer_context(self):
context = super().get_serializer_context()
context['org'] = self.org
return context
def get_queryset(self):
return self.membership_class.objects.filter(organization=self.org)
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework import serializers
from rest_framework_bulk import BulkListSerializer
from users.models import User, UserGroup
from assets.models import Asset, Domain, AdminUser, SystemUser, Label
from perms.models import AssetPermission
from .utils import set_current_org, get_current_org
from .models import Organization from .models import Organization
from .mixins import OrgMembershipSerializerMixin
class OrgSerializer(ModelSerializer): class OrgSerializer(ModelSerializer):
class Meta: class Meta:
model = Organization model = Organization
list_serializer_class = BulkListSerializer
fields = '__all__' fields = '__all__'
read_only_fields = ['id', 'created_by', 'date_created'] read_only_fields = ['id', 'created_by', 'date_created']
class OrgReadSerializer(ModelSerializer):
admins = serializers.SlugRelatedField(slug_field='name', many=True, read_only=True)
users = serializers.SlugRelatedField(slug_field='name', many=True, read_only=True)
user_groups = serializers.SerializerMethodField()
assets = serializers.SerializerMethodField()
domains = serializers.SerializerMethodField()
admin_users = serializers.SerializerMethodField()
system_users = serializers.SerializerMethodField()
labels = serializers.SerializerMethodField()
perms = serializers.SerializerMethodField()
class Meta:
model = Organization
fields = '__all__'
@staticmethod
def get_data_from_model(obj, model):
current_org = get_current_org()
set_current_org(Organization.root())
if model == Asset:
data = [o.hostname for o in model.objects.filter(org_id=obj.id)]
else:
data = [o.name for o in model.objects.filter(org_id=obj.id)]
set_current_org(current_org)
return data
def get_user_groups(self, obj):
return self.get_data_from_model(obj, UserGroup)
def get_assets(self, obj):
return self.get_data_from_model(obj, Asset)
def get_domains(self, obj):
return self.get_data_from_model(obj, Domain)
def get_admin_users(self, obj):
return self.get_data_from_model(obj, AdminUser)
def get_system_users(self, obj):
return self.get_data_from_model(obj, SystemUser)
def get_labels(self, obj):
return self.get_data_from_model(obj, Label)
def get_perms(self, obj):
return self.get_data_from_model(obj, AssetPermission)
class OrgMembershipAdminSerializer(OrgMembershipSerializerMixin, ModelSerializer):
class Meta:
model = Organization.admins.through
list_serializer_class = BulkListSerializer
fields = '__all__'
class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer):
class Meta:
model = Organization.users.through
list_serializer_class = BulkListSerializer
fields = '__all__'
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.urls import path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .. import api from .. import api
app_name = 'orgs' app_name = 'orgs'
router = DefaultRouter() router = DefaultRouter()
router.register(r'org/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/admins',
api.OrgMembershipAdminsViewSet, 'membership-admins')
router.register(r'org/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/users',
api.OrgMembershipUsersViewSet, 'membership-users'),
router.register(r'orgs', api.OrgViewSet, 'org') router.register(r'orgs', api.OrgViewSet, 'org')
......
...@@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404 ...@@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404
from rest_framework.views import APIView, Response from rest_framework.views import APIView, Response
from rest_framework.generics import ListAPIView, get_object_or_404, RetrieveUpdateAPIView from rest_framework.generics import ListAPIView, get_object_or_404, RetrieveUpdateAPIView
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.pagination import LimitOffsetPagination
from common.utils import set_or_append_attr_bulk from common.utils import set_or_append_attr_bulk
from common.permissions import IsValidUser, IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsValidUser, IsOrgAdmin, IsOrgAdminOrAppUser
...@@ -15,6 +16,16 @@ from .hands import AssetGrantedSerializer, User, UserGroup, Asset, Node, \ ...@@ -15,6 +16,16 @@ from .hands import AssetGrantedSerializer, User, UserGroup, Asset, Node, \
NodeGrantedSerializer, SystemUser, NodeSerializer NodeGrantedSerializer, SystemUser, NodeSerializer
from orgs.utils import set_to_root_org from orgs.utils import set_to_root_org
from . import serializers from . import serializers
from .mixins import AssetsFilterMixin
__all__ = [
'AssetPermissionViewSet', 'UserGrantedAssetsApi', 'UserGrantedNodesApi',
'UserGrantedNodesWithAssetsApi', 'UserGrantedNodeAssetsApi', 'UserGroupGrantedAssetsApi',
'UserGroupGrantedNodesApi', 'UserGroupGrantedNodesWithAssetsApi', 'UserGroupGrantedNodeAssetsApi',
'ValidateUserAssetPermissionApi', 'AssetPermissionRemoveUserApi', 'AssetPermissionAddUserApi',
'AssetPermissionRemoveAssetApi', 'AssetPermissionAddAssetApi', 'UserGrantedNodeChildrenApi',
]
class AssetPermissionViewSet(viewsets.ModelViewSet): class AssetPermissionViewSet(viewsets.ModelViewSet):
...@@ -23,6 +34,7 @@ class AssetPermissionViewSet(viewsets.ModelViewSet): ...@@ -23,6 +34,7 @@ class AssetPermissionViewSet(viewsets.ModelViewSet):
""" """
queryset = AssetPermission.objects.all() queryset = AssetPermission.objects.all()
serializer_class = serializers.AssetPermissionCreateUpdateSerializer serializer_class = serializers.AssetPermissionCreateUpdateSerializer
pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def get_serializer_class(self): def get_serializer_class(self):
...@@ -31,10 +43,15 @@ class AssetPermissionViewSet(viewsets.ModelViewSet): ...@@ -31,10 +43,15 @@ class AssetPermissionViewSet(viewsets.ModelViewSet):
return self.serializer_class return self.serializer_class
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset().all()
search = self.request.query_params.get('search')
asset_id = self.request.query_params.get('asset') asset_id = self.request.query_params.get('asset')
node_id = self.request.query_params.get('node') node_id = self.request.query_params.get('node')
inherit_nodes = set() inherit_nodes = set()
if search:
queryset = queryset.filter(name__icontains=search)
if not asset_id and not node_id: if not asset_id and not node_id:
return queryset return queryset
...@@ -53,15 +70,17 @@ class AssetPermissionViewSet(viewsets.ModelViewSet): ...@@ -53,15 +70,17 @@ class AssetPermissionViewSet(viewsets.ModelViewSet):
_permissions = queryset.filter(nodes=n) _permissions = queryset.filter(nodes=n)
set_or_append_attr_bulk(_permissions, "inherit", n.value) set_or_append_attr_bulk(_permissions, "inherit", n.value)
permissions.update(_permissions) permissions.update(_permissions)
return permissions
return list(permissions)
class UserGrantedAssetsApi(ListAPIView): class UserGrantedAssetsApi(AssetsFilterMixin, ListAPIView):
""" """
用户授权的所有资产 用户授权的所有资产
""" """
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = AssetGrantedSerializer serializer_class = AssetGrantedSerializer
pagination_class = LimitOffsetPagination
def change_org_if_need(self): def change_org_if_need(self):
if self.request.user.is_superuser or \ if self.request.user.is_superuser or \
...@@ -84,6 +103,7 @@ class UserGrantedAssetsApi(ListAPIView): ...@@ -84,6 +103,7 @@ class UserGrantedAssetsApi(ListAPIView):
system_users_granted = [s for s in v if s.protocol == k.protocol] system_users_granted = [s for s in v if s.protocol == k.protocol]
k.system_users_granted = system_users_granted k.system_users_granted = system_users_granted
queryset.append(k) queryset.append(k)
return queryset return queryset
def get_permissions(self): def get_permissions(self):
...@@ -122,7 +142,7 @@ class UserGrantedNodesApi(ListAPIView): ...@@ -122,7 +142,7 @@ class UserGrantedNodesApi(ListAPIView):
return super().get_permissions() return super().get_permissions()
class UserGrantedNodesWithAssetsApi(ListAPIView): class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView):
""" """
用户授权的节点并带着节点下资产的api 用户授权的节点并带着节点下资产的api
""" """
...@@ -155,19 +175,25 @@ class UserGrantedNodesWithAssetsApi(ListAPIView): ...@@ -155,19 +175,25 @@ class UserGrantedNodesWithAssetsApi(ListAPIView):
queryset.append(node) queryset.append(node)
return queryset return queryset
def sort_assets(self, queryset):
for node in queryset:
node.assets_granted = super().sort_assets(node.assets_granted)
return queryset
def get_permissions(self): def get_permissions(self):
if self.kwargs.get('pk') is None: if self.kwargs.get('pk') is None:
self.permission_classes = (IsValidUser,) self.permission_classes = (IsValidUser,)
return super().get_permissions() return super().get_permissions()
class UserGrantedNodeAssetsApi(ListAPIView): class UserGrantedNodeAssetsApi(AssetsFilterMixin, ListAPIView):
""" """
查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产 查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产
""" """
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = AssetGrantedSerializer serializer_class = AssetGrantedSerializer
pagination_class = LimitOffsetPagination
def change_org_if_need(self): def change_org_if_need(self):
if self.request.user.is_superuser or \ if self.request.user.is_superuser or \
self.request.user.is_app or \ self.request.user.is_app or \
...@@ -189,6 +215,8 @@ class UserGrantedNodeAssetsApi(ListAPIView): ...@@ -189,6 +215,8 @@ class UserGrantedNodeAssetsApi(ListAPIView):
assets = nodes.get(node, []) assets = nodes.get(node, [])
for asset, system_users in assets.items(): for asset, system_users in assets.items():
asset.system_users_granted = system_users asset.system_users_granted = system_users
assets = list(assets.keys())
return assets return assets
def get_permissions(self): def get_permissions(self):
...@@ -274,7 +302,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView): ...@@ -274,7 +302,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView):
return assets return assets
class ValidateUserAssetPermissionView(RootOrgViewMixin, APIView): class ValidateUserAssetPermissionApi(RootOrgViewMixin, APIView):
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
@staticmethod @staticmethod
...@@ -367,3 +395,88 @@ class AssetPermissionAddAssetApi(RetrieveUpdateAPIView): ...@@ -367,3 +395,88 @@ class AssetPermissionAddAssetApi(RetrieveUpdateAPIView):
return Response({"msg": "ok"}) return Response({"msg": "ok"})
else: else:
return Response({"error": serializer.errors}) return Response({"error": serializer.errors})
class UserGrantedNodeChildrenApi(ListAPIView):
permission_classes = (IsValidUser,)
serializer_class = serializers.AssetPermissionNodeSerializer
def change_org_if_need(self):
if self.request.user.is_superuser or \
self.request.user.is_app or \
self.kwargs.get('pk') is None:
set_to_root_org()
def get_children_queryset(self):
util = AssetPermissionUtil(self.request.user)
node_id = self.request.query_params.get('id')
nodes_granted = util.get_nodes_with_assets()
if not nodes_granted:
return []
root_nodes = [node for node in nodes_granted.keys() if node.is_root()]
queryset = []
if node_id and node_id in [str(node.id) for node in nodes_granted]:
node = [node for node in nodes_granted if str(node.id) == node_id][0]
elif len(root_nodes) == 1:
node = root_nodes[0]
node.assets_amount = len(nodes_granted[node])
queryset.append(node)
else:
for node in root_nodes:
node.assets_amount = len(nodes_granted[node])
queryset.append(node)
return queryset
children = []
for child in node.get_children():
if child in nodes_granted:
child.assets_amount = len(nodes_granted[node])
children.append(child)
children = sorted(children, key=lambda x: x.value)
queryset.extend(children)
fake_nodes = []
for asset, system_users in nodes_granted[node].items():
fake_node = asset.as_node()
fake_node.assets_amount = 0
system_users = [s for s in system_users if s.protocol == asset.protocol]
fake_node.asset.system_users_granted = system_users
fake_node.key = node.key + ':0'
fake_nodes.append(fake_node)
fake_nodes = sorted(fake_nodes, key=lambda x: x.value)
queryset.extend(fake_nodes)
return queryset
def get_search_queryset(self, keyword):
util = AssetPermissionUtil(self.request.user)
nodes_granted = util.get_nodes_with_assets()
queryset = []
for node, assets in nodes_granted.items():
matched_assets = []
node_matched = node.value.lower().find(keyword.lower()) >= 0
asset_has_matched = False
for asset, system_users in assets.items():
asset_matched = (asset.hostname.lower().find(keyword.lower()) >= 0) \
or (asset.ip.find(keyword.lower()) >= 0)
if node_matched or asset_matched:
asset_has_matched = True
fake_node = asset.as_node()
fake_node.assets_amount = 0
system_users = [s for s in system_users if
s.protocol == asset.protocol]
fake_node.asset.system_users_granted = system_users
fake_node.key = node.key + ':0'
matched_assets.append(fake_node)
if asset_has_matched:
node.assets_amount = len(matched_assets)
queryset.append(node)
queryset.extend(sorted(matched_assets, key=lambda x: x.value))
return queryset
def get_queryset(self):
self.change_org_if_need()
keyword = self.request.query_params.get('search')
if keyword:
return self.get_search_queryset(keyword)
else:
return self.get_children_queryset()
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-12-27 16:25
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_auto_20171225_1157'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('perms', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='assetpermission',
name='user_groups',
field=models.ManyToManyField(blank=True, related_name='asset_permissions', to='users.UserGroup', verbose_name='User group'),
),
migrations.AddField(
model_name='assetpermission',
name='users',
field=models.ManyToManyField(blank=True, related_name='asset_permissions', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-02-25 10:15
from __future__ import unicode_literals
import common.utils
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('users', '0004_auto_20180125_1218'),
('assets', '0007_auto_20180225_1815'),
('perms', '0002_auto_20171228_0025'),
]
operations = [
migrations.CreateModel(
name='NodePermission',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('date_expired', models.DateTimeField(default=common.utils.date_expired_default, verbose_name='Date expired')),
('created_by', models.CharField(blank=True, max_length=128, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('node', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Node', verbose_name='Node')),
('system_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.SystemUser', verbose_name='System user')),
('user_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.UserGroup', verbose_name='User group')),
],
options={
'verbose_name': 'Asset permission',
},
),
migrations.AlterUniqueTogether(
name='nodepermission',
unique_together=set([('node', 'user_group', 'system_user')]),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-11 03:35
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('assets', '0013_auto_20180411_1135'),
('perms', '0003_auto_20180225_1815'),
]
operations = [
migrations.RemoveField(
model_name='assetpermission',
name='asset_groups',
),
migrations.AddField(
model_name='assetpermission',
name='date_start',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date start'),
),
migrations.AddField(
model_name='assetpermission',
name='nodes',
field=models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='assets.Node', verbose_name='Nodes'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-11 03:35
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
def migrate_node_permissions(apps, schema_editor):
node_perm_model = apps.get_model("perms", "NodePermission")
asset_perm_model = apps.get_model("perms", "AssetPermission")
db_alias = schema_editor.connection.alias
for old in node_perm_model.objects.using(db_alias).all():
perm = asset_perm_model.objects.using(db_alias).create(
name="{}-{}-{}".format(
old.node.value,
old.user_group.name,
old.system_user.name
),
is_active=old.is_active,
date_expired=old.date_expired,
created_by=old.date_expired,
date_created=old.date_created,
comment=old.comment,
)
perm.user_groups.add(old.user_group)
perm.nodes.add(old.node)
perm.system_users.add(old.system_user)
def migrate_system_assets_relation(apps, schema_editor):
system_user_model = apps.get_model("assets", "SystemUser")
db_alias = schema_editor.connection.alias
for s in system_user_model.objects.using(db_alias).all():
nodes = list(s.nodes.all())
s.nodes.set([])
s.nodes.set(nodes)
class Migration(migrations.Migration):
dependencies = [
('perms', '0004_auto_20180411_1135'),
]
operations = [
migrations.RunPython(migrate_node_permissions),
migrations.RunPython(migrate_system_assets_relation),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-06-06 07:05
from __future__ import unicode_literals
import common.utils
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('perms', '0005_migrate_data_20180411_1144'),
]
operations = [
migrations.AlterField(
model_name='assetpermission',
name='date_expired',
field=models.DateTimeField(db_index=True, default=common.utils.date_expired_default, verbose_name='Date expired'),
),
migrations.AlterField(
model_name='assetpermission',
name='date_start',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date start'),
),
]
# Generated by Django 2.0.7 on 2018-08-07 03:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('perms', '0006_auto_20180606_1505'),
]
operations = [
migrations.AddField(
model_name='assetpermission',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='nodepermission',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='assetpermission',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='assetpermission',
unique_together={('org_id', 'name')},
),
migrations.AlterUniqueTogether(
name='nodepermission',
unique_together=set(),
),
]
# Generated by Django 2.0.7 on 2018-08-16 08:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('perms', '0007_auto_20180807_1116'),
]
operations = [
migrations.AlterField(
model_name='assetpermission',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='nodepermission',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
]
# Generated by Django 2.1 on 2018-09-03 03:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('perms', '0008_auto_20180816_1652'),
]
operations = [
migrations.AlterModelOptions(
name='assetpermission',
options={'verbose_name': 'Asset permission'},
),
]
# ~*~ coding: utf-8 ~*~
#
class AssetsFilterMixin(object):
"""
对资产进行过滤(查询,排序)
"""
def filter_queryset(self, queryset):
queryset = self.search_assets(queryset)
queryset = self.sort_assets(queryset)
return queryset
def search_assets(self, queryset):
from perms.utils import is_obj_attr_has
value = self.request.query_params.get('search')
if not value:
return queryset
queryset = [asset for asset in queryset if is_obj_attr_has(asset, value)]
return queryset
def sort_assets(self, queryset):
from perms.utils import sort_assets
order_by = self.request.query_params.get('order')
if not order_by:
order_by = 'hostname'
if order_by.startswith('-'):
order_by = order_by.lstrip('-')
reverse = True
else:
reverse = False
queryset = sort_assets(queryset, order_by=order_by, reverse=reverse)
return queryset
...@@ -2,8 +2,11 @@ ...@@ -2,8 +2,11 @@
# #
from rest_framework import serializers from rest_framework import serializers
from .models import AssetPermission
from common.fields import StringManyToManyField from common.fields import StringManyToManyField
from .models import AssetPermission
from assets.models import Node
from assets.serializers import AssetGrantedSerializer
class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer): class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer):
...@@ -45,3 +48,29 @@ class AssetPermissionUpdateAssetSerializer(serializers.ModelSerializer): ...@@ -45,3 +48,29 @@ class AssetPermissionUpdateAssetSerializer(serializers.ModelSerializer):
model = AssetPermission model = AssetPermission
fields = ['id', 'assets'] fields = ['id', 'assets']
class AssetPermissionNodeSerializer(serializers.ModelSerializer):
asset = AssetGrantedSerializer(required=False)
assets_amount = serializers.SerializerMethodField()
tree_id = serializers.SerializerMethodField()
tree_parent = serializers.SerializerMethodField()
class Meta:
model = Node
fields = [
'id', 'key', 'value', 'asset', 'is_node', 'org_id',
'tree_id', 'tree_parent', 'assets_amount',
]
@staticmethod
def get_assets_amount(obj):
return obj.assets_amount
@staticmethod
def get_tree_id(obj):
return obj.key
@staticmethod
def get_tree_parent(obj):
return obj.parent_key
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
</li> </li>
<li class="active"> <li class="active">
<a href="{% url 'perms:asset-permission-asset-list' pk=asset_permission.id %}" class="text-center"> <a href="{% url 'perms:asset-permission-asset-list' pk=asset_permission.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'Assets and asset groups' %}</a> <i class="fa fa-bar-chart-o"></i> {% trans 'Assets and node' %}</a>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -54,9 +54,9 @@ ...@@ -54,9 +54,9 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-daterange input-group" id="datepicker"> <div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span> <span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" name="date_start" value="{{ form.date_start.value|date:'Y-m-d' }}"> <input type="text" class="input-sm form-control" id="date_start" name="date_start" value="{{ form.date_start.value|date:'Y-m-d H:i' }}">
<span class="input-group-addon">to</span> <span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" name="date_expired" value="{{ form.date_expired.value|date:'Y-m-d' }}"> <input type="text" class="input-sm form-control" id="date_expired" name="date_expired" value="{{ form.date_expired.value|date:'Y-m-d H:i' }}">
</div> </div>
<span class="help-block ">{{ form.date_expired.errors }}</span> <span class="help-block ">{{ form.date_expired.errors }}</span>
<span class="help-block ">{{ form.date_start.errors }}</span> <span class="help-block ">{{ form.date_start.errors }}</span>
...@@ -70,6 +70,7 @@ ...@@ -70,6 +70,7 @@
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button> <button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
...@@ -80,19 +81,27 @@ ...@@ -80,19 +81,27 @@
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script> <script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.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" %} />
<script> <script>
var dateOptions = {
singleDatePicker: true,
showDropdowns: true,
timePicker: true,
timePicker24Hour: true,
autoApply: true,
locale: {
format: 'YYYY-MM-DD HH:mm'
}
};
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2({ $('.select2').select2({
closeOnSelect: false closeOnSelect: false
}); });
$('#datepicker').datepicker({ $('#date_start').daterangepicker(dateOptions);
format: "yyyy-mm-dd", $('#date_expired').daterangepicker(dateOptions);
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
});
$("#id_assets").parent().find(".select2-selection").on('click', function (e) { $("#id_assets").parent().find(".select2-selection").on('click', function (e) {
if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){ if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){
e.preventDefault(); e.preventDefault();
...@@ -110,6 +119,6 @@ $(document).ready(function () { ...@@ -110,6 +119,6 @@ $(document).ready(function () {
$('.select2').val(assets).trigger('change'); $('.select2').val(assets).trigger('change');
}); });
$("#asset_list_modal").modal('hide'); $("#asset_list_modal").modal('hide');
}) });
</script> </script>
{% endblock %} {% endblock %}
\ No newline at end of file
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
</li> </li>
<li> <li>
<a href="{% url 'perms:asset-permission-asset-list' pk=object.id %}" class="text-center"> <a href="{% url 'perms:asset-permission-asset-list' pk=object.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'Assets and asset groups' %}</a> <i class="fa fa-bar-chart-o"></i> {% trans 'Assets and node' %}</a>
</li> </li>
<li class="pull-right"> <li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'perms:asset-permission-update' pk=object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a> <a class="btn btn-outline btn-default" href="{% url 'perms:asset-permission-update' pk=object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
......
...@@ -217,7 +217,7 @@ function initTable() { ...@@ -217,7 +217,7 @@ function initTable() {
select: {}, select: {},
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
table = jumpserver.initDataTable(options); table = jumpserver.initServerSideDataTable(options);
return table return table
} }
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
</li> </li>
<li> <li>
<a href="{% url 'perms:asset-permission-asset-list' pk=asset_permission.id %}" class="text-center"> <a href="{% url 'perms:asset-permission-asset-list' pk=asset_permission.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'Assets and asset groups' %}</a> <i class="fa fa-bar-chart-o"></i> {% trans 'Assets and node' %}</a>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -19,6 +19,8 @@ urlpatterns = [ ...@@ -19,6 +19,8 @@ urlpatterns = [
api.UserGrantedNodesApi.as_view(), name='user-nodes'), api.UserGrantedNodesApi.as_view(), name='user-nodes'),
path('user/nodes/', api.UserGrantedNodesApi.as_view(), path('user/nodes/', api.UserGrantedNodesApi.as_view(),
name='my-nodes'), name='my-nodes'),
path('user/nodes/children/', api.UserGrantedNodeChildrenApi.as_view(),
name='my-node-children'),
path('user/<uuid:pk>/nodes/<uuid:node_id>/assets/', path('user/<uuid:pk>/nodes/<uuid:node_id>/assets/',
api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'), api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'),
path('user/nodes/<uuid:node_id>/assets/', path('user/nodes/<uuid:node_id>/assets/',
...@@ -55,7 +57,7 @@ urlpatterns = [ ...@@ -55,7 +57,7 @@ urlpatterns = [
name='asset-permission-add-asset'), name='asset-permission-add-asset'),
# 验证用户是否有某个资产和系统用户的权限 # 验证用户是否有某个资产和系统用户的权限
path('asset-permission/user/validate/', api.ValidateUserAssetPermissionView.as_view(), path('asset-permission/user/validate/', api.ValidateUserAssetPermissionApi.as_view(),
name='validate-user-asset-permission'), name='validate-user-asset-permission'),
] ]
......
...@@ -156,3 +156,22 @@ class AssetPermissionUtil: ...@@ -156,3 +156,22 @@ class AssetPermissionUtil:
return tree.nodes return tree.nodes
def is_obj_attr_has(obj, val, attrs=("hostname", "ip", "comment")):
if not attrs:
vals = [val for val in obj.__dict__.values() if isinstance(val, (str, int))]
else:
vals = [getattr(obj, attr) for attr in attrs if
hasattr(obj, attr) and isinstance(hasattr(obj, attr), (str, int))]
for v in vals:
if str(v).find(val) != -1:
return True
return False
def sort_assets(assets, order_by='hostname', reverse=False):
if order_by == 'ip':
assets = sorted(assets, key=lambda asset: [int(d) for d in asset.ip.split('.') if d.isdigit()], reverse=reverse)
else:
assets = sorted(assets, key=lambda asset: getattr(asset, order_by), reverse=reverse)
return assets
...@@ -101,7 +101,7 @@ class AssetPermissionUserView(AdminUserRequiredMixin, ...@@ -101,7 +101,7 @@ class AssetPermissionUserView(AdminUserRequiredMixin,
ListView): ListView):
template_name = 'perms/asset_permission_user.html' template_name = 'perms/asset_permission_user.html'
context_object_name = 'asset_permission' context_object_name = 'asset_permission'
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE paginate_by = settings.DISPLAY_PER_PAGE
object = None object = None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
...@@ -133,7 +133,7 @@ class AssetPermissionAssetView(AdminUserRequiredMixin, ...@@ -133,7 +133,7 @@ class AssetPermissionAssetView(AdminUserRequiredMixin,
ListView): ListView):
template_name = 'perms/asset_permission_asset.html' template_name = 'perms/asset_permission_asset.html'
context_object_name = 'asset_permission' context_object_name = 'asset_permission'
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE paginate_by = settings.DISPLAY_PER_PAGE
object = None object = None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
......
.daterangepicker {
position: absolute;
color: inherit;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ddd;
width: 278px;
max-width: none;
padding: 0;
margin-top: 7px;
top: 100px;
left: 20px;
z-index: 3001;
display: none;
font-family: arial;
font-size: 15px;
line-height: 1em;
}
.daterangepicker:before, .daterangepicker:after {
position: absolute;
display: inline-block;
border-bottom-color: rgba(0, 0, 0, 0.2);
content: '';
}
.daterangepicker:before {
top: -7px;
border-right: 7px solid transparent;
border-left: 7px solid transparent;
border-bottom: 7px solid #ccc;
}
.daterangepicker:after {
top: -6px;
border-right: 6px solid transparent;
border-bottom: 6px solid #fff;
border-left: 6px solid transparent;
}
.daterangepicker.opensleft:before {
right: 9px;
}
.daterangepicker.opensleft:after {
right: 10px;
}
.daterangepicker.openscenter:before {
left: 0;
right: 0;
width: 0;
margin-left: auto;
margin-right: auto;
}
.daterangepicker.openscenter:after {
left: 0;
right: 0;
width: 0;
margin-left: auto;
margin-right: auto;
}
.daterangepicker.opensright:before {
left: 9px;
}
.daterangepicker.opensright:after {
left: 10px;
}
.daterangepicker.drop-up {
margin-top: -7px;
}
.daterangepicker.drop-up:before {
top: initial;
bottom: -7px;
border-bottom: initial;
border-top: 7px solid #ccc;
}
.daterangepicker.drop-up:after {
top: initial;
bottom: -6px;
border-bottom: initial;
border-top: 6px solid #fff;
}
.daterangepicker.single .daterangepicker .ranges, .daterangepicker.single .drp-calendar {
float: none;
}
.daterangepicker.single .drp-selected {
display: none;
}
.daterangepicker.show-calendar .drp-calendar {
display: block;
}
.daterangepicker.show-calendar .drp-buttons {
display: block;
}
.daterangepicker.auto-apply .drp-buttons {
display: none;
}
.daterangepicker .drp-calendar {
display: none;
max-width: 270px;
}
.daterangepicker .drp-calendar.left {
padding: 8px 0 8px 8px;
}
.daterangepicker .drp-calendar.right {
padding: 8px;
}
.daterangepicker .drp-calendar.single .calendar-table {
border: none;
}
.daterangepicker .calendar-table .next span, .daterangepicker .calendar-table .prev span {
color: #fff;
border: solid black;
border-width: 0 2px 2px 0;
border-radius: 0;
display: inline-block;
padding: 3px;
}
.daterangepicker .calendar-table .next span {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
}
.daterangepicker .calendar-table .prev span {
transform: rotate(135deg);
-webkit-transform: rotate(135deg);
}
.daterangepicker .calendar-table th, .daterangepicker .calendar-table td {
white-space: nowrap;
text-align: center;
vertical-align: middle;
min-width: 32px;
width: 32px;
height: 24px;
line-height: 24px;
font-size: 12px;
border-radius: 4px;
border: 1px solid transparent;
white-space: nowrap;
cursor: pointer;
}
.daterangepicker .calendar-table {
border: 1px solid #fff;
border-radius: 4px;
background-color: #fff;
}
.daterangepicker .calendar-table table {
width: 100%;
margin: 0;
border-spacing: 0;
border-collapse: collapse;
}
.daterangepicker td.available:hover, .daterangepicker th.available:hover {
background-color: #eee;
border-color: transparent;
color: inherit;
}
.daterangepicker td.week, .daterangepicker th.week {
font-size: 80%;
color: #ccc;
}
.daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date {
background-color: #fff;
border-color: transparent;
color: #999;
}
.daterangepicker td.in-range {
background-color: #ebf4f8;
border-color: transparent;
color: #000;
border-radius: 0;
}
.daterangepicker td.start-date {
border-radius: 4px 0 0 4px;
}
.daterangepicker td.end-date {
border-radius: 0 4px 4px 0;
}
.daterangepicker td.start-date.end-date {
border-radius: 4px;
}
.daterangepicker td.active, .daterangepicker td.active:hover {
background-color: #357ebd;
border-color: transparent;
color: #fff;
}
.daterangepicker th.month {
width: auto;
}
.daterangepicker td.disabled, .daterangepicker option.disabled {
color: #999;
cursor: not-allowed;
text-decoration: line-through;
}
.daterangepicker select.monthselect, .daterangepicker select.yearselect {
font-size: 12px;
padding: 1px;
height: auto;
margin: 0;
cursor: default;
}
.daterangepicker select.monthselect {
margin-right: 2%;
width: 56%;
}
.daterangepicker select.yearselect {
width: 40%;
}
.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect {
width: 50px;
margin: 0 auto;
background: #eee;
border: 1px solid #eee;
padding: 2px;
outline: 0;
font-size: 12px;
}
.daterangepicker .calendar-time {
text-align: center;
margin: 4px auto 0 auto;
line-height: 30px;
position: relative;
}
.daterangepicker .calendar-time select.disabled {
color: #ccc;
cursor: not-allowed;
}
.daterangepicker .drp-buttons {
clear: both;
text-align: right;
padding: 8px;
border-top: 1px solid #ddd;
display: none;
line-height: 12px;
vertical-align: middle;
}
.daterangepicker .drp-selected {
display: inline-block;
font-size: 12px;
padding-right: 8px;
}
.daterangepicker .drp-buttons .btn {
margin-left: 8px;
font-size: 12px;
font-weight: bold;
padding: 4px 8px;
}
.daterangepicker.show-ranges .drp-calendar.left {
border-left: 1px solid #ddd;
}
.daterangepicker .ranges {
float: none;
text-align: left;
margin: 0;
}
.daterangepicker.show-calendar .ranges {
margin-top: 8px;
}
.daterangepicker .ranges ul {
list-style: none;
margin: 0 auto;
padding: 0;
width: 100%;
}
.daterangepicker .ranges li {
font-size: 12px;
padding: 8px 12px;
cursor: pointer;
}
.daterangepicker .ranges li:hover {
background-color: #eee;
}
.daterangepicker .ranges li.active {
background-color: #08c;
color: #fff;
}
/* Larger Screen Styling */
@media (min-width: 564px) {
.daterangepicker {
width: auto; }
.daterangepicker .ranges ul {
width: 140px; }
.daterangepicker.single .ranges ul {
width: 100%; }
.daterangepicker.single .drp-calendar.left {
clear: none; }
.daterangepicker.single.ltr .ranges, .daterangepicker.single.ltr .drp-calendar {
float: left; }
.daterangepicker.single.rtl .ranges, .daterangepicker.single.rtl .drp-calendar {
float: right; }
.daterangepicker.ltr {
direction: ltr;
text-align: left; }
.daterangepicker.ltr .drp-calendar.left {
clear: left;
margin-right: 0; }
.daterangepicker.ltr .drp-calendar.left .calendar-table {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0; }
.daterangepicker.ltr .drp-calendar.right {
margin-left: 0; }
.daterangepicker.ltr .drp-calendar.right .calendar-table {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0; }
.daterangepicker.ltr .drp-calendar.left .calendar-table {
padding-right: 8px; }
.daterangepicker.ltr .ranges, .daterangepicker.ltr .drp-calendar {
float: left; }
.daterangepicker.rtl {
direction: rtl;
text-align: right; }
.daterangepicker.rtl .drp-calendar.left {
clear: right;
margin-left: 0; }
.daterangepicker.rtl .drp-calendar.left .calendar-table {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0; }
.daterangepicker.rtl .drp-calendar.right {
margin-right: 0; }
.daterangepicker.rtl .drp-calendar.right .calendar-table {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0; }
.daterangepicker.rtl .drp-calendar.left .calendar-table {
padding-left: 12px; }
.daterangepicker.rtl .ranges, .daterangepicker.rtl .drp-calendar {
text-align: right;
float: right; } }
@media (min-width: 730px) {
.daterangepicker .ranges {
width: auto; }
.daterangepicker.ltr .ranges {
float: left; }
.daterangepicker.rtl .ranges {
float: right; }
.daterangepicker .drp-calendar.left {
clear: none !important; } }
\ No newline at end of file
...@@ -146,12 +146,15 @@ function activeNav() { ...@@ -146,12 +146,15 @@ function activeNav() {
if (app === ''){ if (app === ''){
$('#index').addClass('active'); $('#index').addClass('active');
} }
else if (app === 'xpack') { else if (app === 'xpack' && resource === 'cloud') {
var item = url_array[3]; var item = url_array[3];
$("#" + app).addClass('active'); $("#" + app).addClass('active');
$('#' + app + ' #' + resource).addClass('active'); $('#' + app + ' #' + resource).addClass('active');
$('#' + app + ' #' + resource + ' #' + item + ' a').css('color', '#ffffff'); $('#' + app + ' #' + resource + ' #' + item + ' a').css('color', '#ffffff');
} }
else if (app === 'settings'){
$("#" + app).addClass('active');
}
else { else {
$("#" + app).addClass('active'); $("#" + app).addClass('active');
$('#' + app + ' #' + resource).addClass('active'); $('#' + app + ' #' + resource).addClass('active');
...@@ -505,7 +508,7 @@ jumpserver.initServerSideDataTable = function (options) { ...@@ -505,7 +508,7 @@ jumpserver.initServerSideDataTable = function (options) {
if (data.order !== null && data.order.length === 1) { if (data.order !== null && data.order.length === 1) {
var col = data.order[0].column; var col = data.order[0].column;
var order = options.columns[col].data; var order = options.columns[col].data;
if (data.order[0].dir = "desc") { if (data.order[0].dir === "desc") {
order = "-" + order; order = "-" + order;
} }
data.order = order; data.order = order;
...@@ -685,41 +688,69 @@ function setUrlParam(url, name, value) { ...@@ -685,41 +688,69 @@ function setUrlParam(url, name, value) {
return url return url
} }
// Password check rules
var rules_short_map_id = {
'min': 'id_security_password_min_length',
'upper': 'id_security_password_upper_case',
'lower': 'id_security_password_lower_case',
'number': 'id_security_password_number',
'special': 'id_security_password_special_char'
};
var rules_id_map_label = {
'id_security_password_min_length': gettext('Password minimum length {N} bits'),
'id_security_password_upper_case': gettext('Must contain capital letters'),
'id_security_password_lower_case': gettext('Must contain lowercase letters'),
'id_security_password_number': gettext('Must contain numeric characters'),
'id_security_password_special_char': gettext('Must contain special characters')
};
function getRuleLabel(rule){
var label = '';
if (rule.key === rules_short_map_id['min']){
label = rules_id_map_label[rule.key].replace('{N}', rule.value)
}
else{
label = rules_id_map_label[rule.key]
}
return label
}
// 校验密码-改变规则颜色 // 校验密码-改变规则颜色
function checkPasswordRules(password, minLength) { function checkPasswordRules(password, minLength) {
if (wordMinLength(password, minLength)) { if (wordMinLength(password, minLength)) {
$('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', 'green') $('#'+rules_short_map_id['min']).css('color', 'green')
} }
else { else {
$('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', '#908a8a') $('#'+rules_short_map_id['min']).css('color', '#908a8a')
} }
if (wordUpperCase(password)) { if (wordUpperCase(password)) {
$('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', 'green'); $('#'+rules_short_map_id['upper']).css('color', 'green')
} }
else { else {
$('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', '#908a8a') $('#'+rules_short_map_id['upper']).css('color', '#908a8a')
} }
if (wordLowerCase(password)) { if (wordLowerCase(password)) {
$('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', 'green') $('#'+rules_short_map_id['lower']).css('color', 'green')
} }
else { else {
$('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', '#908a8a') $('#'+rules_short_map_id['lower']).css('color', '#908a8a')
} }
if (wordNumber(password)) { if (wordNumber(password)) {
$('#rule_SECURITY_PASSWORD_NUMBER').css('color', 'green') $('#'+rules_short_map_id['number']).css('color', 'green')
} }
else { else {
$('#rule_SECURITY_PASSWORD_NUMBER').css('color', '#908a8a') $('#'+rules_short_map_id['number']).css('color', '#908a8a')
} }
if (wordSpecialChar(password)) { if (wordSpecialChar(password)) {
$('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', 'green') $('#'+rules_short_map_id['special']).css('color', 'green')
} }
else { else {
$('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', '#908a8a') $('#'+rules_short_map_id['special']).css('color', '#908a8a')
} }
} }
...@@ -746,11 +777,12 @@ function wordSpecialChar(word) { ...@@ -746,11 +777,12 @@ function wordSpecialChar(word) {
return word.match(/[`,~,!,@,#,\$,%,\^,&,\*,\(,\),\-,_,=,\+,\{,\},\[,\],\|,\\,;,',:,",\,,\.,<,>,\/,\?]+/) return word.match(/[`,~,!,@,#,\$,%,\^,&,\*,\(,\),\-,_,=,\+,\{,\},\[,\],\|,\\,;,',:,",\,,\.,<,>,\/,\?]+/)
} }
// 显示弹窗密码规则 // 显示弹窗密码规则
function popoverPasswordRules(password_check_rules, $el) { function popoverPasswordRules(password_check_rules, $el) {
var message = ""; var message = "";
jQuery.each(password_check_rules, function (idx, rules) { jQuery.each(password_check_rules, function (idx, rule) {
message += "<li id=" + rules.id + " style='list-style-type:none;'> <i class='fa fa-check-circle-o' style='margin-right:10px;' ></i>" + rules.label + "</li>"; message += "<li id=" + rule.key + " style='list-style-type:none;'> <i class='fa fa-check-circle-o' style='margin-right:10px;' ></i>" + getRuleLabel(rule) + "</li>";
}); });
//$('#id_password_rules').html(message); //$('#id_password_rules').html(message);
$el.html(message) $el.html(message)
......
/**
* Minified by jsDelivr using UglifyJS v3.4.5.
* Original file: /npm/daterangepicker@3.0.3/daterangepicker.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!function(t,a){if("function"==typeof define&&define.amd)define(["moment","jquery"],function(t,e){return e.fn||(e.fn={}),a(t,e)});else if("object"==typeof module&&module.exports){var e="undefined"!=typeof window?window.jQuery:void 0;e||(e=require("jquery")).fn||(e.fn={});var i="undefined"!=typeof window&&void 0!==window.moment?window.moment:require("moment");module.exports=a(i,e)}else t.daterangepicker=a(t.moment,t.jQuery)}(this,function(H,R){var i=function(t,e,a){if(this.parentEl="body",this.element=R(t),this.startDate=H().startOf("day"),this.endDate=H().endOf("day"),this.minDate=!1,this.maxDate=!1,this.maxSpan=!1,this.autoApply=!1,this.singleDatePicker=!1,this.showDropdowns=!1,this.minYear=H().subtract(100,"year").format("YYYY"),this.maxYear=H().add(100,"year").format("YYYY"),this.showWeekNumbers=!1,this.showISOWeekNumbers=!1,this.showCustomRangeLabel=!0,this.timePicker=!1,this.timePicker24Hour=!1,this.timePickerIncrement=1,this.timePickerSeconds=!1,this.linkedCalendars=!0,this.autoUpdateInput=!0,this.alwaysShowCalendars=!1,this.ranges={},this.opens="right",this.element.hasClass("pull-right")&&(this.opens="left"),this.drops="down",this.element.hasClass("dropup")&&(this.drops="up"),this.buttonClasses="btn btn-sm",this.applyButtonClasses="btn-primary",this.cancelButtonClasses="btn-default",this.locale={direction:"ltr",format:H.localeData().longDateFormat("L"),separator:" - ",applyLabel:"Apply",cancelLabel:"Cancel",weekLabel:"W",customRangeLabel:"Custom Range",daysOfWeek:H.weekdaysMin(),monthNames:H.monthsShort(),firstDay:H.localeData().firstDayOfWeek()},this.callback=function(){},this.isShowing=!1,this.leftCalendar={},this.rightCalendar={},"object"==typeof e&&null!==e||(e={}),"string"==typeof(e=R.extend(this.element.data(),e)).template||e.template instanceof R||(e.template='<div class="daterangepicker"><div class="ranges"></div><div class="drp-calendar left"><div class="calendar-table"></div><div class="calendar-time"></div></div><div class="drp-calendar right"><div class="calendar-table"></div><div class="calendar-time"></div></div><div class="drp-buttons"><span class="drp-selected"></span><button class="cancelBtn" type="button"></button><button class="applyBtn" disabled="disabled" type="button"></button> </div></div>'),this.parentEl=e.parentEl&&R(e.parentEl).length?R(e.parentEl):R(this.parentEl),this.container=R(e.template).appendTo(this.parentEl),"object"==typeof e.locale&&("string"==typeof e.locale.direction&&(this.locale.direction=e.locale.direction),"string"==typeof e.locale.format&&(this.locale.format=e.locale.format),"string"==typeof e.locale.separator&&(this.locale.separator=e.locale.separator),"object"==typeof e.locale.daysOfWeek&&(this.locale.daysOfWeek=e.locale.daysOfWeek.slice()),"object"==typeof e.locale.monthNames&&(this.locale.monthNames=e.locale.monthNames.slice()),"number"==typeof e.locale.firstDay&&(this.locale.firstDay=e.locale.firstDay),"string"==typeof e.locale.applyLabel&&(this.locale.applyLabel=e.locale.applyLabel),"string"==typeof e.locale.cancelLabel&&(this.locale.cancelLabel=e.locale.cancelLabel),"string"==typeof e.locale.weekLabel&&(this.locale.weekLabel=e.locale.weekLabel),"string"==typeof e.locale.customRangeLabel)){(d=document.createElement("textarea")).innerHTML=e.locale.customRangeLabel;var i=d.value;this.locale.customRangeLabel=i}if(this.container.addClass(this.locale.direction),"string"==typeof e.startDate&&(this.startDate=H(e.startDate,this.locale.format)),"string"==typeof e.endDate&&(this.endDate=H(e.endDate,this.locale.format)),"string"==typeof e.minDate&&(this.minDate=H(e.minDate,this.locale.format)),"string"==typeof e.maxDate&&(this.maxDate=H(e.maxDate,this.locale.format)),"object"==typeof e.startDate&&(this.startDate=H(e.startDate)),"object"==typeof e.endDate&&(this.endDate=H(e.endDate)),"object"==typeof e.minDate&&(this.minDate=H(e.minDate)),"object"==typeof e.maxDate&&(this.maxDate=H(e.maxDate)),this.minDate&&this.startDate.isBefore(this.minDate)&&(this.startDate=this.minDate.clone()),this.maxDate&&this.endDate.isAfter(this.maxDate)&&(this.endDate=this.maxDate.clone()),"string"==typeof e.applyButtonClasses&&(this.applyButtonClasses=e.applyButtonClasses),"string"==typeof e.applyClass&&(this.applyButtonClasses=e.applyClass),"string"==typeof e.cancelButtonClasses&&(this.cancelButtonClasses=e.cancelButtonClasses),"string"==typeof e.cancelClass&&(this.cancelButtonClasses=e.cancelClass),"object"==typeof e.maxSpan&&(this.maxSpan=e.maxSpan),"object"==typeof e.dateLimit&&(this.maxSpan=e.dateLimit),"string"==typeof e.opens&&(this.opens=e.opens),"string"==typeof e.drops&&(this.drops=e.drops),"boolean"==typeof e.showWeekNumbers&&(this.showWeekNumbers=e.showWeekNumbers),"boolean"==typeof e.showISOWeekNumbers&&(this.showISOWeekNumbers=e.showISOWeekNumbers),"string"==typeof e.buttonClasses&&(this.buttonClasses=e.buttonClasses),"object"==typeof e.buttonClasses&&(this.buttonClasses=e.buttonClasses.join(" ")),"boolean"==typeof e.showDropdowns&&(this.showDropdowns=e.showDropdowns),"number"==typeof e.minYear&&(this.minYear=e.minYear),"number"==typeof e.maxYear&&(this.maxYear=e.maxYear),"boolean"==typeof e.showCustomRangeLabel&&(this.showCustomRangeLabel=e.showCustomRangeLabel),"boolean"==typeof e.singleDatePicker&&(this.singleDatePicker=e.singleDatePicker,this.singleDatePicker&&(this.endDate=this.startDate.clone())),"boolean"==typeof e.timePicker&&(this.timePicker=e.timePicker),"boolean"==typeof e.timePickerSeconds&&(this.timePickerSeconds=e.timePickerSeconds),"number"==typeof e.timePickerIncrement&&(this.timePickerIncrement=e.timePickerIncrement),"boolean"==typeof e.timePicker24Hour&&(this.timePicker24Hour=e.timePicker24Hour),"boolean"==typeof e.autoApply&&(this.autoApply=e.autoApply),"boolean"==typeof e.autoUpdateInput&&(this.autoUpdateInput=e.autoUpdateInput),"boolean"==typeof e.linkedCalendars&&(this.linkedCalendars=e.linkedCalendars),"function"==typeof e.isInvalidDate&&(this.isInvalidDate=e.isInvalidDate),"function"==typeof e.isCustomDate&&(this.isCustomDate=e.isCustomDate),"boolean"==typeof e.alwaysShowCalendars&&(this.alwaysShowCalendars=e.alwaysShowCalendars),0!=this.locale.firstDay)for(var s=this.locale.firstDay;0<s;)this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()),s--;var n,r,o;if(void 0===e.startDate&&void 0===e.endDate&&R(this.element).is(":text")){var h=R(this.element).val(),l=h.split(this.locale.separator);n=r=null,2==l.length?(n=H(l[0],this.locale.format),r=H(l[1],this.locale.format)):this.singleDatePicker&&""!==h&&(n=H(h,this.locale.format),r=H(h,this.locale.format)),null!==n&&null!==r&&(this.setStartDate(n),this.setEndDate(r))}if("object"==typeof e.ranges){for(o in e.ranges){n="string"==typeof e.ranges[o][0]?H(e.ranges[o][0],this.locale.format):H(e.ranges[o][0]),r="string"==typeof e.ranges[o][1]?H(e.ranges[o][1],this.locale.format):H(e.ranges[o][1]),this.minDate&&n.isBefore(this.minDate)&&(n=this.minDate.clone());var c=this.maxDate;if(this.maxSpan&&c&&n.clone().add(this.maxSpan).isAfter(c)&&(c=n.clone().add(this.maxSpan)),c&&r.isAfter(c)&&(r=c.clone()),!(this.minDate&&r.isBefore(this.minDate,this.timepicker?"minute":"day")||c&&n.isAfter(c,this.timepicker?"minute":"day"))){var d;(d=document.createElement("textarea")).innerHTML=o;i=d.value;this.ranges[i]=[n,r]}}var m="<ul>";for(o in this.ranges)m+='<li data-range-key="'+o+'">'+o+"</li>";this.showCustomRangeLabel&&(m+='<li data-range-key="'+this.locale.customRangeLabel+'">'+this.locale.customRangeLabel+"</li>"),m+="</ul>",this.container.find(".ranges").prepend(m)}"function"==typeof a&&(this.callback=a),this.timePicker||(this.startDate=this.startDate.startOf("day"),this.endDate=this.endDate.endOf("day"),this.container.find(".calendar-time").hide()),this.timePicker&&this.autoApply&&(this.autoApply=!1),this.autoApply&&this.container.addClass("auto-apply"),"object"==typeof e.ranges&&this.container.addClass("show-ranges"),this.singleDatePicker&&(this.container.addClass("single"),this.container.find(".drp-calendar.left").addClass("single"),this.container.find(".drp-calendar.left").show(),this.container.find(".drp-calendar.right").hide(),this.timePicker||this.container.addClass("auto-apply")),(void 0===e.ranges&&!this.singleDatePicker||this.alwaysShowCalendars)&&this.container.addClass("show-calendar"),this.container.addClass("opens"+this.opens),this.container.find(".applyBtn, .cancelBtn").addClass(this.buttonClasses),this.applyButtonClasses.length&&this.container.find(".applyBtn").addClass(this.applyButtonClasses),this.cancelButtonClasses.length&&this.container.find(".cancelBtn").addClass(this.cancelButtonClasses),this.container.find(".applyBtn").html(this.locale.applyLabel),this.container.find(".cancelBtn").html(this.locale.cancelLabel),this.container.find(".drp-calendar").on("click.daterangepicker",".prev",R.proxy(this.clickPrev,this)).on("click.daterangepicker",".next",R.proxy(this.clickNext,this)).on("mousedown.daterangepicker","td.available",R.proxy(this.clickDate,this)).on("mouseenter.daterangepicker","td.available",R.proxy(this.hoverDate,this)).on("change.daterangepicker","select.yearselect",R.proxy(this.monthOrYearChanged,this)).on("change.daterangepicker","select.monthselect",R.proxy(this.monthOrYearChanged,this)).on("change.daterangepicker","select.hourselect,select.minuteselect,select.secondselect,select.ampmselect",R.proxy(this.timeChanged,this)),this.container.find(".ranges").on("click.daterangepicker","li",R.proxy(this.clickRange,this)),this.container.find(".drp-buttons").on("click.daterangepicker","button.applyBtn",R.proxy(this.clickApply,this)).on("click.daterangepicker","button.cancelBtn",R.proxy(this.clickCancel,this)),this.element.is("input")||this.element.is("button")?this.element.on({"click.daterangepicker":R.proxy(this.show,this),"focus.daterangepicker":R.proxy(this.show,this),"keyup.daterangepicker":R.proxy(this.elementChanged,this),"keydown.daterangepicker":R.proxy(this.keydown,this)}):(this.element.on("click.daterangepicker",R.proxy(this.toggle,this)),this.element.on("keydown.daterangepicker",R.proxy(this.toggle,this))),this.updateElement()};return i.prototype={constructor:i,setStartDate:function(t){"string"==typeof t&&(this.startDate=H(t,this.locale.format)),"object"==typeof t&&(this.startDate=H(t)),this.timePicker||(this.startDate=this.startDate.startOf("day")),this.timePicker&&this.timePickerIncrement&&this.startDate.minute(Math.round(this.startDate.minute()/this.timePickerIncrement)*this.timePickerIncrement),this.minDate&&this.startDate.isBefore(this.minDate)&&(this.startDate=this.minDate.clone(),this.timePicker&&this.timePickerIncrement&&this.startDate.minute(Math.round(this.startDate.minute()/this.timePickerIncrement)*this.timePickerIncrement)),this.maxDate&&this.startDate.isAfter(this.maxDate)&&(this.startDate=this.maxDate.clone(),this.timePicker&&this.timePickerIncrement&&this.startDate.minute(Math.floor(this.startDate.minute()/this.timePickerIncrement)*this.timePickerIncrement)),this.isShowing||this.updateElement(),this.updateMonthsInView()},setEndDate:function(t){"string"==typeof t&&(this.endDate=H(t,this.locale.format)),"object"==typeof t&&(this.endDate=H(t)),this.timePicker||(this.endDate=this.endDate.add(1,"d").startOf("day").subtract(1,"second")),this.timePicker&&this.timePickerIncrement&&this.endDate.minute(Math.round(this.endDate.minute()/this.timePickerIncrement)*this.timePickerIncrement),this.endDate.isBefore(this.startDate)&&(this.endDate=this.startDate.clone()),this.maxDate&&this.endDate.isAfter(this.maxDate)&&(this.endDate=this.maxDate.clone()),this.maxSpan&&this.startDate.clone().add(this.maxSpan).isBefore(this.endDate)&&(this.endDate=this.startDate.clone().add(this.maxSpan)),this.previousRightTime=this.endDate.clone(),this.container.find(".drp-selected").html(this.startDate.format(this.locale.format)+this.locale.separator+this.endDate.format(this.locale.format)),this.isShowing||this.updateElement(),this.updateMonthsInView()},isInvalidDate:function(){return!1},isCustomDate:function(){return!1},updateView:function(){this.timePicker&&(this.renderTimePicker("left"),this.renderTimePicker("right"),this.endDate?this.container.find(".right .calendar-time select").removeAttr("disabled").removeClass("disabled"):this.container.find(".right .calendar-time select").attr("disabled","disabled").addClass("disabled")),this.endDate&&this.container.find(".drp-selected").html(this.startDate.format(this.locale.format)+this.locale.separator+this.endDate.format(this.locale.format)),this.updateMonthsInView(),this.updateCalendars(),this.updateFormInputs()},updateMonthsInView:function(){if(this.endDate){if(!this.singleDatePicker&&this.leftCalendar.month&&this.rightCalendar.month&&(this.startDate.format("YYYY-MM")==this.leftCalendar.month.format("YYYY-MM")||this.startDate.format("YYYY-MM")==this.rightCalendar.month.format("YYYY-MM"))&&(this.endDate.format("YYYY-MM")==this.leftCalendar.month.format("YYYY-MM")||this.endDate.format("YYYY-MM")==this.rightCalendar.month.format("YYYY-MM")))return;this.leftCalendar.month=this.startDate.clone().date(2),this.linkedCalendars||this.endDate.month()==this.startDate.month()&&this.endDate.year()==this.startDate.year()?this.rightCalendar.month=this.startDate.clone().date(2).add(1,"month"):this.rightCalendar.month=this.endDate.clone().date(2)}else this.leftCalendar.month.format("YYYY-MM")!=this.startDate.format("YYYY-MM")&&this.rightCalendar.month.format("YYYY-MM")!=this.startDate.format("YYYY-MM")&&(this.leftCalendar.month=this.startDate.clone().date(2),this.rightCalendar.month=this.startDate.clone().date(2).add(1,"month"));this.maxDate&&this.linkedCalendars&&!this.singleDatePicker&&this.rightCalendar.month>this.maxDate&&(this.rightCalendar.month=this.maxDate.clone().date(2),this.leftCalendar.month=this.maxDate.clone().date(2).subtract(1,"month"))},updateCalendars:function(){if(this.timePicker){var t,e,a,i;if(this.endDate){if(t=parseInt(this.container.find(".left .hourselect").val(),10),e=parseInt(this.container.find(".left .minuteselect").val(),10),a=this.timePickerSeconds?parseInt(this.container.find(".left .secondselect").val(),10):0,!this.timePicker24Hour)"PM"===(i=this.container.find(".left .ampmselect").val())&&t<12&&(t+=12),"AM"===i&&12===t&&(t=0)}else if(t=parseInt(this.container.find(".right .hourselect").val(),10),e=parseInt(this.container.find(".right .minuteselect").val(),10),a=this.timePickerSeconds?parseInt(this.container.find(".right .secondselect").val(),10):0,!this.timePicker24Hour)"PM"===(i=this.container.find(".right .ampmselect").val())&&t<12&&(t+=12),"AM"===i&&12===t&&(t=0);this.leftCalendar.month.hour(t).minute(e).second(a),this.rightCalendar.month.hour(t).minute(e).second(a)}this.renderCalendar("left"),this.renderCalendar("right"),this.container.find(".ranges li").removeClass("active"),null!=this.endDate&&this.calculateChosenLabel()},renderCalendar:function(t){var e,a=(e="left"==t?this.leftCalendar:this.rightCalendar).month.month(),i=e.month.year(),s=e.month.hour(),n=e.month.minute(),r=e.month.second(),o=H([i,a]).daysInMonth(),h=H([i,a,1]),l=H([i,a,o]),c=H(h).subtract(1,"month").month(),d=H(h).subtract(1,"month").year(),m=H([d,c]).daysInMonth(),f=h.day();(e=[]).firstDay=h,e.lastDay=l;for(var p=0;p<6;p++)e[p]=[];var u=m-f+this.locale.firstDay+1;m<u&&(u-=7),f==this.locale.firstDay&&(u=m-6);for(var D=H([d,c,u,12,n,r]),g=(p=0,0),y=0;p<42;p++,g++,D=H(D).add(24,"hour"))0<p&&g%7==0&&(g=0,y++),e[y][g]=D.clone().hour(s).minute(n).second(r),D.hour(12),this.minDate&&e[y][g].format("YYYY-MM-DD")==this.minDate.format("YYYY-MM-DD")&&e[y][g].isBefore(this.minDate)&&"left"==t&&(e[y][g]=this.minDate.clone()),this.maxDate&&e[y][g].format("YYYY-MM-DD")==this.maxDate.format("YYYY-MM-DD")&&e[y][g].isAfter(this.maxDate)&&"right"==t&&(e[y][g]=this.maxDate.clone());"left"==t?this.leftCalendar.calendar=e:this.rightCalendar.calendar=e;var k="left"==t?this.minDate:this.startDate,b=this.maxDate,C=("left"==t?this.startDate:this.endDate,this.locale.direction,'<table class="table-condensed">');C+="<thead>",C+="<tr>",(this.showWeekNumbers||this.showISOWeekNumbers)&&(C+="<th></th>"),k&&!k.isBefore(e.firstDay)||this.linkedCalendars&&"left"!=t?C+="<th></th>":C+='<th class="prev available"><span></span></th>';var v=this.locale.monthNames[e[1][1].month()]+e[1][1].format(" YYYY");if(this.showDropdowns){for(var Y=e[1][1].month(),w=e[1][1].year(),P=b&&b.year()||this.maxYear,x=k&&k.year()||this.minYear,M=w==x,S=w==P,I='<select class="monthselect">',B=0;B<12;B++)(!M||B>=k.month())&&(!S||B<=b.month())?I+="<option value='"+B+"'"+(B===Y?" selected='selected'":"")+">"+this.locale.monthNames[B]+"</option>":I+="<option value='"+B+"'"+(B===Y?" selected='selected'":"")+" disabled='disabled'>"+this.locale.monthNames[B]+"</option>";I+="</select>";for(var A='<select class="yearselect">',L=x;L<=P;L++)A+='<option value="'+L+'"'+(L===w?' selected="selected"':"")+">"+L+"</option>";v=I+(A+="</select>")}if(C+='<th colspan="5" class="month">'+v+"</th>",b&&!b.isAfter(e.lastDay)||this.linkedCalendars&&"right"!=t&&!this.singleDatePicker?C+="<th></th>":C+='<th class="next available"><span></span></th>',C+="</tr>",C+="<tr>",(this.showWeekNumbers||this.showISOWeekNumbers)&&(C+='<th class="week">'+this.locale.weekLabel+"</th>"),R.each(this.locale.daysOfWeek,function(t,e){C+="<th>"+e+"</th>"}),C+="</tr>",C+="</thead>",C+="<tbody>",null==this.endDate&&this.maxSpan){var E=this.startDate.clone().add(this.maxSpan).endOf("day");b&&!E.isBefore(b)||(b=E)}for(y=0;y<6;y++){C+="<tr>",this.showWeekNumbers?C+='<td class="week">'+e[y][0].week()+"</td>":this.showISOWeekNumbers&&(C+='<td class="week">'+e[y][0].isoWeek()+"</td>");for(g=0;g<7;g++){var W=[];e[y][g].isSame(new Date,"day")&&W.push("today"),5<e[y][g].isoWeekday()&&W.push("weekend"),e[y][g].month()!=e[1][1].month()&&W.push("off"),this.minDate&&e[y][g].isBefore(this.minDate,"day")&&W.push("off","disabled"),b&&e[y][g].isAfter(b,"day")&&W.push("off","disabled"),this.isInvalidDate(e[y][g])&&W.push("off","disabled"),e[y][g].format("YYYY-MM-DD")==this.startDate.format("YYYY-MM-DD")&&W.push("active","start-date"),null!=this.endDate&&e[y][g].format("YYYY-MM-DD")==this.endDate.format("YYYY-MM-DD")&&W.push("active","end-date"),null!=this.endDate&&e[y][g]>this.startDate&&e[y][g]<this.endDate&&W.push("in-range");var O=this.isCustomDate(e[y][g]);!1!==O&&("string"==typeof O?W.push(O):Array.prototype.push.apply(W,O));var N="",j=!1;for(p=0;p<W.length;p++)N+=W[p]+" ","disabled"==W[p]&&(j=!0);j||(N+="available"),C+='<td class="'+N.replace(/^\s+|\s+$/g,"")+'" data-title="r'+y+"c"+g+'">'+e[y][g].date()+"</td>"}C+="</tr>"}C+="</tbody>",C+="</table>",this.container.find(".drp-calendar."+t+" .calendar-table").html(C)},renderTimePicker:function(t){if("right"!=t||this.endDate){var e,a,i,s=this.maxDate;if(!this.maxSpan||this.maxDate&&!this.startDate.clone().add(this.maxSpan).isAfter(this.maxDate)||(s=this.startDate.clone().add(this.maxSpan)),"left"==t)a=this.startDate.clone(),i=this.minDate;else if("right"==t){a=this.endDate.clone(),i=this.startDate;var n=this.container.find(".drp-calendar.right .calendar-time");if(""!=n.html()&&(a.hour(a.hour()||n.find(".hourselect option:selected").val()),a.minute(a.minute()||n.find(".minuteselect option:selected").val()),a.second(a.second()||n.find(".secondselect option:selected").val()),!this.timePicker24Hour)){var r=n.find(".ampmselect option:selected").val();"PM"===r&&a.hour()<12&&a.hour(a.hour()+12),"AM"===r&&12===a.hour()&&a.hour(0)}a.isBefore(this.startDate)&&(a=this.startDate.clone()),s&&a.isAfter(s)&&(a=s.clone())}e='<select class="hourselect">';for(var o=this.timePicker24Hour?0:1,h=this.timePicker24Hour?23:12,l=o;l<=h;l++){var c=l;this.timePicker24Hour||(c=12<=a.hour()?12==l?12:l+12:12==l?0:l);var d=a.clone().hour(c),m=!1;i&&d.minute(59).isBefore(i)&&(m=!0),s&&d.minute(0).isAfter(s)&&(m=!0),c!=a.hour()||m?e+=m?'<option value="'+l+'" disabled="disabled" class="disabled">'+l+"</option>":'<option value="'+l+'">'+l+"</option>":e+='<option value="'+l+'" selected="selected">'+l+"</option>"}e+="</select> ",e+=': <select class="minuteselect">';for(l=0;l<60;l+=this.timePickerIncrement){var f=l<10?"0"+l:l;d=a.clone().minute(l),m=!1;i&&d.second(59).isBefore(i)&&(m=!0),s&&d.second(0).isAfter(s)&&(m=!0),a.minute()!=l||m?e+=m?'<option value="'+l+'" disabled="disabled" class="disabled">'+f+"</option>":'<option value="'+l+'">'+f+"</option>":e+='<option value="'+l+'" selected="selected">'+f+"</option>"}if(e+="</select> ",this.timePickerSeconds){e+=': <select class="secondselect">';for(l=0;l<60;l++){f=l<10?"0"+l:l,d=a.clone().second(l),m=!1;i&&d.isBefore(i)&&(m=!0),s&&d.isAfter(s)&&(m=!0),a.second()!=l||m?e+=m?'<option value="'+l+'" disabled="disabled" class="disabled">'+f+"</option>":'<option value="'+l+'">'+f+"</option>":e+='<option value="'+l+'" selected="selected">'+f+"</option>"}e+="</select> "}if(!this.timePicker24Hour){e+='<select class="ampmselect">';var p="",u="";i&&a.clone().hour(12).minute(0).second(0).isBefore(i)&&(p=' disabled="disabled" class="disabled"'),s&&a.clone().hour(0).minute(0).second(0).isAfter(s)&&(u=' disabled="disabled" class="disabled"'),12<=a.hour()?e+='<option value="AM"'+p+'>AM</option><option value="PM" selected="selected"'+u+">PM</option>":e+='<option value="AM" selected="selected"'+p+'>AM</option><option value="PM"'+u+">PM</option>",e+="</select>"}this.container.find(".drp-calendar."+t+" .calendar-time").html(e)}},updateFormInputs:function(){this.singleDatePicker||this.endDate&&(this.startDate.isBefore(this.endDate)||this.startDate.isSame(this.endDate))?this.container.find("button.applyBtn").removeAttr("disabled"):this.container.find("button.applyBtn").attr("disabled","disabled")},move:function(){var t,e={top:0,left:0},a=R(window).width();this.parentEl.is("body")||(e={top:this.parentEl.offset().top-this.parentEl.scrollTop(),left:this.parentEl.offset().left-this.parentEl.scrollLeft()},a=this.parentEl[0].clientWidth+this.parentEl.offset().left),t="up"==this.drops?this.element.offset().top-this.container.outerHeight()-e.top:this.element.offset().top+this.element.outerHeight()-e.top,this.container["up"==this.drops?"addClass":"removeClass"]("drop-up"),"left"==this.opens?(this.container.css({top:t,right:a-this.element.offset().left-this.element.outerWidth(),left:"auto"}),this.container.offset().left<0&&this.container.css({right:"auto",left:9})):"center"==this.opens?(this.container.css({top:t,left:this.element.offset().left-e.left+this.element.outerWidth()/2-this.container.outerWidth()/2,right:"auto"}),this.container.offset().left<0&&this.container.css({right:"auto",left:9})):(this.container.css({top:t,left:this.element.offset().left-e.left,right:"auto"}),this.container.offset().left+this.container.outerWidth()>R(window).width()&&this.container.css({left:"auto",right:0}))},show:function(t){this.isShowing||(this._outsideClickProxy=R.proxy(function(t){this.outsideClick(t)},this),R(document).on("mousedown.daterangepicker",this._outsideClickProxy).on("touchend.daterangepicker",this._outsideClickProxy).on("click.daterangepicker","[data-toggle=dropdown]",this._outsideClickProxy).on("focusin.daterangepicker",this._outsideClickProxy),R(window).on("resize.daterangepicker",R.proxy(function(t){this.move(t)},this)),this.oldStartDate=this.startDate.clone(),this.oldEndDate=this.endDate.clone(),this.previousRightTime=this.endDate.clone(),this.updateView(),this.container.show(),this.move(),this.element.trigger("show.daterangepicker",this),this.isShowing=!0)},hide:function(t){this.isShowing&&(this.endDate||(this.startDate=this.oldStartDate.clone(),this.endDate=this.oldEndDate.clone()),this.startDate.isSame(this.oldStartDate)&&this.endDate.isSame(this.oldEndDate)||this.callback(this.startDate.clone(),this.endDate.clone(),this.chosenLabel),this.updateElement(),R(document).off(".daterangepicker"),R(window).off(".daterangepicker"),this.container.hide(),this.element.trigger("hide.daterangepicker",this),this.isShowing=!1)},toggle:function(t){this.isShowing?this.hide():this.show()},outsideClick:function(t){var e=R(t.target);"focusin"==t.type||e.closest(this.element).length||e.closest(this.container).length||e.closest(".calendar-table").length||(this.hide(),this.element.trigger("outsideClick.daterangepicker",this))},showCalendars:function(){this.container.addClass("show-calendar"),this.move(),this.element.trigger("showCalendar.daterangepicker",this)},hideCalendars:function(){this.container.removeClass("show-calendar"),this.element.trigger("hideCalendar.daterangepicker",this)},clickRange:function(t){var e=t.target.getAttribute("data-range-key");if((this.chosenLabel=e)==this.locale.customRangeLabel)this.showCalendars();else{var a=this.ranges[e];this.startDate=a[0],this.endDate=a[1],this.timePicker||(this.startDate.startOf("day"),this.endDate.endOf("day")),this.alwaysShowCalendars||this.hideCalendars(),this.clickApply()}},clickPrev:function(t){R(t.target).parents(".drp-calendar").hasClass("left")?(this.leftCalendar.month.subtract(1,"month"),this.linkedCalendars&&this.rightCalendar.month.subtract(1,"month")):this.rightCalendar.month.subtract(1,"month"),this.updateCalendars()},clickNext:function(t){R(t.target).parents(".drp-calendar").hasClass("left")?this.leftCalendar.month.add(1,"month"):(this.rightCalendar.month.add(1,"month"),this.linkedCalendars&&this.leftCalendar.month.add(1,"month")),this.updateCalendars()},hoverDate:function(t){if(R(t.target).hasClass("available")){var e=R(t.target).attr("data-title"),a=e.substr(1,1),i=e.substr(3,1),r=R(t.target).parents(".drp-calendar").hasClass("left")?this.leftCalendar.calendar[a][i]:this.rightCalendar.calendar[a][i],o=this.leftCalendar,h=this.rightCalendar,l=this.startDate;this.endDate||this.container.find(".drp-calendar tbody td").each(function(t,e){if(!R(e).hasClass("week")){var a=R(e).attr("data-title"),i=a.substr(1,1),s=a.substr(3,1),n=R(e).parents(".drp-calendar").hasClass("left")?o.calendar[i][s]:h.calendar[i][s];n.isAfter(l)&&n.isBefore(r)||n.isSame(r,"day")?R(e).addClass("in-range"):R(e).removeClass("in-range")}})}},clickDate:function(t){if(R(t.target).hasClass("available")){var e=R(t.target).attr("data-title"),a=e.substr(1,1),i=e.substr(3,1),s=R(t.target).parents(".drp-calendar").hasClass("left")?this.leftCalendar.calendar[a][i]:this.rightCalendar.calendar[a][i];if(this.endDate||s.isBefore(this.startDate,"day")){if(this.timePicker){var n=parseInt(this.container.find(".left .hourselect").val(),10);if(!this.timePicker24Hour)"PM"===(h=this.container.find(".left .ampmselect").val())&&n<12&&(n+=12),"AM"===h&&12===n&&(n=0);var r=parseInt(this.container.find(".left .minuteselect").val(),10),o=this.timePickerSeconds?parseInt(this.container.find(".left .secondselect").val(),10):0;s=s.clone().hour(n).minute(r).second(o)}this.endDate=null,this.setStartDate(s.clone())}else if(!this.endDate&&s.isBefore(this.startDate))this.setEndDate(this.startDate.clone());else{if(this.timePicker){var h;n=parseInt(this.container.find(".right .hourselect").val(),10);if(!this.timePicker24Hour)"PM"===(h=this.container.find(".right .ampmselect").val())&&n<12&&(n+=12),"AM"===h&&12===n&&(n=0);r=parseInt(this.container.find(".right .minuteselect").val(),10),o=this.timePickerSeconds?parseInt(this.container.find(".right .secondselect").val(),10):0;s=s.clone().hour(n).minute(r).second(o)}this.setEndDate(s.clone()),this.autoApply&&(this.calculateChosenLabel(),this.clickApply())}this.singleDatePicker&&(this.setEndDate(this.startDate),this.timePicker||this.clickApply()),this.updateView(),t.stopPropagation()}},calculateChosenLabel:function(){var t=!0,e=0;for(var a in this.ranges){if(this.timePicker){var i=this.timePickerSeconds?"YYYY-MM-DD hh:mm:ss":"YYYY-MM-DD hh:mm";if(this.startDate.format(i)==this.ranges[a][0].format(i)&&this.endDate.format(i)==this.ranges[a][1].format(i)){t=!1,this.chosenLabel=this.container.find(".ranges li:eq("+e+")").addClass("active").attr("data-range-key");break}}else if(this.startDate.format("YYYY-MM-DD")==this.ranges[a][0].format("YYYY-MM-DD")&&this.endDate.format("YYYY-MM-DD")==this.ranges[a][1].format("YYYY-MM-DD")){t=!1,this.chosenLabel=this.container.find(".ranges li:eq("+e+")").addClass("active").attr("data-range-key");break}e++}t&&(this.showCustomRangeLabel?this.chosenLabel=this.container.find(".ranges li:last").addClass("active").attr("data-range-key"):this.chosenLabel=null,this.showCalendars())},clickApply:function(t){this.hide(),this.element.trigger("apply.daterangepicker",this)},clickCancel:function(t){this.startDate=this.oldStartDate,this.endDate=this.oldEndDate,this.hide(),this.element.trigger("cancel.daterangepicker",this)},monthOrYearChanged:function(t){var e=R(t.target).closest(".drp-calendar").hasClass("left"),a=e?"left":"right",i=this.container.find(".drp-calendar."+a),s=parseInt(i.find(".monthselect").val(),10),n=i.find(".yearselect").val();e||(n<this.startDate.year()||n==this.startDate.year()&&s<this.startDate.month())&&(s=this.startDate.month(),n=this.startDate.year()),this.minDate&&(n<this.minDate.year()||n==this.minDate.year()&&s<this.minDate.month())&&(s=this.minDate.month(),n=this.minDate.year()),this.maxDate&&(n>this.maxDate.year()||n==this.maxDate.year()&&s>this.maxDate.month())&&(s=this.maxDate.month(),n=this.maxDate.year()),e?(this.leftCalendar.month.month(s).year(n),this.linkedCalendars&&(this.rightCalendar.month=this.leftCalendar.month.clone().add(1,"month"))):(this.rightCalendar.month.month(s).year(n),this.linkedCalendars&&(this.leftCalendar.month=this.rightCalendar.month.clone().subtract(1,"month"))),this.updateCalendars()},timeChanged:function(t){var e=R(t.target).closest(".drp-calendar"),a=e.hasClass("left"),i=parseInt(e.find(".hourselect").val(),10),s=parseInt(e.find(".minuteselect").val(),10),n=this.timePickerSeconds?parseInt(e.find(".secondselect").val(),10):0;if(!this.timePicker24Hour){var r=e.find(".ampmselect").val();"PM"===r&&i<12&&(i+=12),"AM"===r&&12===i&&(i=0)}if(a){var o=this.startDate.clone();o.hour(i),o.minute(s),o.second(n),this.setStartDate(o),this.singleDatePicker?this.endDate=this.startDate.clone():this.endDate&&this.endDate.format("YYYY-MM-DD")==o.format("YYYY-MM-DD")&&this.endDate.isBefore(o)&&this.setEndDate(o.clone())}else if(this.endDate){var h=this.endDate.clone();h.hour(i),h.minute(s),h.second(n),this.setEndDate(h)}this.updateCalendars(),this.updateFormInputs(),this.renderTimePicker("left"),this.renderTimePicker("right")},elementChanged:function(){if(this.element.is("input")&&this.element.val().length){var t=this.element.val().split(this.locale.separator),e=null,a=null;2===t.length&&(e=H(t[0],this.locale.format),a=H(t[1],this.locale.format)),(this.singleDatePicker||null===e||null===a)&&(a=e=H(this.element.val(),this.locale.format)),e.isValid()&&a.isValid()&&(this.setStartDate(e),this.setEndDate(a),this.updateView())}},keydown:function(t){9!==t.keyCode&&13!==t.keyCode||this.hide(),27===t.keyCode&&(t.preventDefault(),t.stopPropagation(),this.hide())},updateElement:function(){if(this.element.is("input")&&this.autoUpdateInput){var t=this.startDate.format(this.locale.format);this.singleDatePicker||(t+=this.locale.separator+this.endDate.format(this.locale.format)),t!==this.element.val()&&this.element.val(t).trigger("change")}},remove:function(){this.container.remove(),this.element.off(".daterangepicker"),this.element.removeData()}},R.fn.daterangepicker=function(t,e){var a=R.extend(!0,{},R.fn.daterangepicker.defaultOptions,t);return this.each(function(){var t=R(this);t.data("daterangepicker")&&t.data("daterangepicker").remove(),t.data("daterangepicker",new i(t,a,e))}),this},i});
//# sourceMappingURL=/sm/8cfffddf058dc09b67d92f8d849675e6b459dfb8ede5136cf5c98d10acf78cc3.map
\ No newline at end of file
//! moment.js
//! version : 2.18.1
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
//! license : MIT
//! momentjs.com
!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return sd.apply(null,arguments)}function b(a){sd=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){return null!=a&&"[object Object]"===Object.prototype.toString.call(a)}function e(a){var b;for(b in a)return!1;return!0}function f(a){return void 0===a}function g(a){return"number"==typeof a||"[object Number]"===Object.prototype.toString.call(a)}function h(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function i(a,b){var c,d=[];for(c=0;c<a.length;++c)d.push(b(a[c],c));return d}function j(a,b){return Object.prototype.hasOwnProperty.call(a,b)}function k(a,b){for(var c in b)j(b,c)&&(a[c]=b[c]);return j(b,"toString")&&(a.toString=b.toString),j(b,"valueOf")&&(a.valueOf=b.valueOf),a}function l(a,b,c,d){return sb(a,b,c,d,!0).utc()}function m(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null,rfc2822:!1,weekdayMismatch:!1}}function n(a){return null==a._pf&&(a._pf=m()),a._pf}function o(a){if(null==a._isValid){var b=n(a),c=ud.call(b.parsedDateParts,function(a){return null!=a}),d=!isNaN(a._d.getTime())&&b.overflow<0&&!b.empty&&!b.invalidMonth&&!b.invalidWeekday&&!b.nullInput&&!b.invalidFormat&&!b.userInvalidated&&(!b.meridiem||b.meridiem&&c);if(a._strict&&(d=d&&0===b.charsLeftOver&&0===b.unusedTokens.length&&void 0===b.bigHour),null!=Object.isFrozen&&Object.isFrozen(a))return d;a._isValid=d}return a._isValid}function p(a){var b=l(NaN);return null!=a?k(n(b),a):n(b).userInvalidated=!0,b}function q(a,b){var c,d,e;if(f(b._isAMomentObject)||(a._isAMomentObject=b._isAMomentObject),f(b._i)||(a._i=b._i),f(b._f)||(a._f=b._f),f(b._l)||(a._l=b._l),f(b._strict)||(a._strict=b._strict),f(b._tzm)||(a._tzm=b._tzm),f(b._isUTC)||(a._isUTC=b._isUTC),f(b._offset)||(a._offset=b._offset),f(b._pf)||(a._pf=n(b)),f(b._locale)||(a._locale=b._locale),vd.length>0)for(c=0;c<vd.length;c++)d=vd[c],e=b[d],f(e)||(a[d]=e);return a}function r(b){q(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),wd===!1&&(wd=!0,a.updateOffset(this),wd=!1)}function s(a){return a instanceof r||null!=a&&null!=a._isAMomentObject}function t(a){return a<0?Math.ceil(a)||0:Math.floor(a)}function u(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=t(b)),c}function v(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;d<e;d++)(c&&a[d]!==b[d]||!c&&u(a[d])!==u(b[d]))&&g++;return g+f}function w(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function x(b,c){var d=!0;return k(function(){if(null!=a.deprecationHandler&&a.deprecationHandler(null,b),d){for(var e,f=[],g=0;g<arguments.length;g++){if(e="","object"==typeof arguments[g]){e+="\n["+g+"] ";for(var h in arguments[0])e+=h+": "+arguments[0][h]+", ";e=e.slice(0,-2)}else e=arguments[g];f.push(e)}w(b+"\nArguments: "+Array.prototype.slice.call(f).join("")+"\n"+(new Error).stack),d=!1}return c.apply(this,arguments)},c)}function y(b,c){null!=a.deprecationHandler&&a.deprecationHandler(b,c),xd[b]||(w(c),xd[b]=!0)}function z(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}function A(a){var b,c;for(c in a)b=a[c],z(b)?this[c]=b:this["_"+c]=b;this._config=a,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)}function B(a,b){var c,e=k({},a);for(c in b)j(b,c)&&(d(a[c])&&d(b[c])?(e[c]={},k(e[c],a[c]),k(e[c],b[c])):null!=b[c]?e[c]=b[c]:delete e[c]);for(c in a)j(a,c)&&!j(b,c)&&d(a[c])&&(e[c]=k({},e[c]));return e}function C(a){null!=a&&this.set(a)}function D(a,b,c){var d=this._calendar[a]||this._calendar.sameElse;return z(d)?d.call(b,c):d}function E(a){var b=this._longDateFormat[a],c=this._longDateFormat[a.toUpperCase()];return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a])}function F(){return this._invalidDate}function G(a){return this._ordinal.replace("%d",a)}function H(a,b,c,d){var e=this._relativeTime[c];return z(e)?e(a,b,c,d):e.replace(/%d/i,a)}function I(a,b){var c=this._relativeTime[a>0?"future":"past"];return z(c)?c(b):c.replace(/%s/i,b)}function J(a,b){var c=a.toLowerCase();Hd[c]=Hd[c+"s"]=Hd[b]=a}function K(a){return"string"==typeof a?Hd[a]||Hd[a.toLowerCase()]:void 0}function L(a){var b,c,d={};for(c in a)j(a,c)&&(b=K(c),b&&(d[b]=a[c]));return d}function M(a,b){Id[a]=b}function N(a){var b=[];for(var c in a)b.push({unit:c,priority:Id[c]});return b.sort(function(a,b){return a.priority-b.priority}),b}function O(b,c){return function(d){return null!=d?(Q(this,b,d),a.updateOffset(this,c),this):P(this,b)}}function P(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function Q(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)}function R(a){return a=K(a),z(this[a])?this[a]():this}function S(a,b){if("object"==typeof a){a=L(a);for(var c=N(a),d=0;d<c.length;d++)this[c[d].unit](a[c[d].unit])}else if(a=K(a),z(this[a]))return this[a](b);return this}function T(a,b,c){var d=""+Math.abs(a),e=b-d.length,f=a>=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d}function U(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(Md[a]=e),b&&(Md[b[0]]=function(){return T(e.apply(this,arguments),b[1],b[2])}),c&&(Md[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function V(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function W(a){var b,c,d=a.match(Jd);for(b=0,c=d.length;b<c;b++)Md[d[b]]?d[b]=Md[d[b]]:d[b]=V(d[b]);return function(b){var e,f="";for(e=0;e<c;e++)f+=z(d[e])?d[e].call(b,a):d[e];return f}}function X(a,b){return a.isValid()?(b=Y(b,a.localeData()),Ld[b]=Ld[b]||W(b),Ld[b](a)):a.localeData().invalidDate()}function Y(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Kd.lastIndex=0;d>=0&&Kd.test(a);)a=a.replace(Kd,c),Kd.lastIndex=0,d-=1;return a}function Z(a,b,c){ce[a]=z(b)?b:function(a,d){return a&&c?c:b}}function $(a,b){return j(ce,a)?ce[a](b._strict,b._locale):new RegExp(_(a))}function _(a){return aa(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function aa(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function ba(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),g(b)&&(d=function(a,c){c[b]=u(a)}),c=0;c<a.length;c++)de[a[c]]=d}function ca(a,b){ba(a,function(a,c,d,e){d._w=d._w||{},b(a,d._w,d,e)})}function da(a,b,c){null!=b&&j(de,a)&&de[a](b,c._a,c,a)}function ea(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function fa(a,b){return a?c(this._months)?this._months[a.month()]:this._months[(this._months.isFormat||oe).test(b)?"format":"standalone"][a.month()]:c(this._months)?this._months:this._months.standalone}function ga(a,b){return a?c(this._monthsShort)?this._monthsShort[a.month()]:this._monthsShort[oe.test(b)?"format":"standalone"][a.month()]:c(this._monthsShort)?this._monthsShort:this._monthsShort.standalone}function ha(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],d=0;d<12;++d)f=l([2e3,d]),this._shortMonthsParse[d]=this.monthsShort(f,"").toLocaleLowerCase(),this._longMonthsParse[d]=this.months(f,"").toLocaleLowerCase();return c?"MMM"===b?(e=ne.call(this._shortMonthsParse,g),e!==-1?e:null):(e=ne.call(this._longMonthsParse,g),e!==-1?e:null):"MMM"===b?(e=ne.call(this._shortMonthsParse,g),e!==-1?e:(e=ne.call(this._longMonthsParse,g),e!==-1?e:null)):(e=ne.call(this._longMonthsParse,g),e!==-1?e:(e=ne.call(this._shortMonthsParse,g),e!==-1?e:null))}function ia(a,b,c){var d,e,f;if(this._monthsParseExact)return ha.call(this,a,b,c);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;d<12;d++){if(e=l([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}function ja(a,b){var c;if(!a.isValid())return a;if("string"==typeof b)if(/^\d+$/.test(b))b=u(b);else if(b=a.localeData().monthsParse(b),!g(b))return a;return c=Math.min(a.date(),ea(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a}function ka(b){return null!=b?(ja(this,b),a.updateOffset(this,!0),this):P(this,"Month")}function la(){return ea(this.year(),this.month())}function ma(a){return this._monthsParseExact?(j(this,"_monthsRegex")||oa.call(this),a?this._monthsShortStrictRegex:this._monthsShortRegex):(j(this,"_monthsShortRegex")||(this._monthsShortRegex=re),this._monthsShortStrictRegex&&a?this._monthsShortStrictRegex:this._monthsShortRegex)}function na(a){return this._monthsParseExact?(j(this,"_monthsRegex")||oa.call(this),a?this._monthsStrictRegex:this._monthsRegex):(j(this,"_monthsRegex")||(this._monthsRegex=se),this._monthsStrictRegex&&a?this._monthsStrictRegex:this._monthsRegex)}function oa(){function a(a,b){return b.length-a.length}var b,c,d=[],e=[],f=[];for(b=0;b<12;b++)c=l([2e3,b]),d.push(this.monthsShort(c,"")),e.push(this.months(c,"")),f.push(this.months(c,"")),f.push(this.monthsShort(c,""));for(d.sort(a),e.sort(a),f.sort(a),b=0;b<12;b++)d[b]=aa(d[b]),e[b]=aa(e[b]);for(b=0;b<24;b++)f[b]=aa(f[b]);this._monthsRegex=new RegExp("^("+f.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+e.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+d.join("|")+")","i")}function pa(a){return qa(a)?366:365}function qa(a){return a%4===0&&a%100!==0||a%400===0}function ra(){return qa(this.year())}function sa(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return a<100&&a>=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function ta(a){var b=new Date(Date.UTC.apply(null,arguments));return a<100&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b}function ua(a,b,c){var d=7+b-c,e=(7+ta(a,0,d).getUTCDay()-b)%7;return-e+d-1}function va(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ua(a,d,e),j=1+7*(b-1)+h+i;return j<=0?(f=a-1,g=pa(f)+j):j>pa(a)?(f=a+1,g=j-pa(a)):(f=a,g=j),{year:f,dayOfYear:g}}function wa(a,b,c){var d,e,f=ua(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return g<1?(e=a.year()-1,d=g+xa(e,b,c)):g>xa(a.year(),b,c)?(d=g-xa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function xa(a,b,c){var d=ua(a,b,c),e=ua(a+1,b,c);return(pa(a)-d+e)/7}function ya(a){return wa(a,this._week.dow,this._week.doy).week}function za(){return this._week.dow}function Aa(){return this._week.doy}function Ba(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function Ca(a){var b=wa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}function Da(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function Ea(a,b){return"string"==typeof a?b.weekdaysParse(a)%7||7:isNaN(a)?null:a}function Fa(a,b){return a?c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]:c(this._weekdays)?this._weekdays:this._weekdays.standalone}function Ga(a){return a?this._weekdaysShort[a.day()]:this._weekdaysShort}function Ha(a){return a?this._weekdaysMin[a.day()]:this._weekdaysMin}function Ia(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;d<7;++d)f=l([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=ne.call(this._weekdaysParse,g),e!==-1?e:null):"ddd"===b?(e=ne.call(this._shortWeekdaysParse,g),e!==-1?e:null):(e=ne.call(this._minWeekdaysParse,g),e!==-1?e:null):"dddd"===b?(e=ne.call(this._weekdaysParse,g),e!==-1?e:(e=ne.call(this._shortWeekdaysParse,g),e!==-1?e:(e=ne.call(this._minWeekdaysParse,g),e!==-1?e:null))):"ddd"===b?(e=ne.call(this._shortWeekdaysParse,g),e!==-1?e:(e=ne.call(this._weekdaysParse,g),e!==-1?e:(e=ne.call(this._minWeekdaysParse,g),e!==-1?e:null))):(e=ne.call(this._minWeekdaysParse,g),e!==-1?e:(e=ne.call(this._weekdaysParse,g),e!==-1?e:(e=ne.call(this._shortWeekdaysParse,g),e!==-1?e:null)))}function Ja(a,b,c){var d,e,f;if(this._weekdaysParseExact)return Ia.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;d<7;d++){if(e=l([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}}function Ka(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Da(a,this.localeData()),this.add(a-b,"d")):b}function La(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function Ma(a){if(!this.isValid())return null!=a?this:NaN;if(null!=a){var b=Ea(a,this.localeData());return this.day(this.day()%7?b:b-7)}return this.day()||7}function Na(a){return this._weekdaysParseExact?(j(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):(j(this,"_weekdaysRegex")||(this._weekdaysRegex=ye),this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex)}function Oa(a){return this._weekdaysParseExact?(j(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(j(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ze),this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Pa(a){return this._weekdaysParseExact?(j(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(j(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Ae),this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Qa(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],h=[],i=[],j=[];for(b=0;b<7;b++)c=l([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),h.push(e),i.push(f),j.push(d),j.push(e),j.push(f);for(g.sort(a),h.sort(a),i.sort(a),j.sort(a),b=0;b<7;b++)h[b]=aa(h[b]),i[b]=aa(i[b]),j[b]=aa(j[b]);this._weekdaysRegex=new RegExp("^("+j.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+h.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")}function Ra(){return this.hours()%12||12}function Sa(){return this.hours()||24}function Ta(a,b){U(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}function Ua(a,b){return b._meridiemParse}function Va(a){return"p"===(a+"").toLowerCase().charAt(0)}function Wa(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function Xa(a){return a?a.toLowerCase().replace("_","-"):a}function Ya(a){for(var b,c,d,e,f=0;f<a.length;){for(e=Xa(a[f]).split("-"),b=e.length,c=Xa(a[f+1]),c=c?c.split("-"):null;b>0;){if(d=Za(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&v(e,c,!0)>=b-1)break;b--}f++}return null}function Za(a){var b=null;if(!Fe[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=Be._abbr,require("./locale/"+a),$a(b)}catch(a){}return Fe[a]}function $a(a,b){var c;return a&&(c=f(b)?bb(a):_a(a,b),c&&(Be=c)),Be._abbr}function _a(a,b){if(null!==b){var c=Ee;if(b.abbr=a,null!=Fe[a])y("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),c=Fe[a]._config;else if(null!=b.parentLocale){if(null==Fe[b.parentLocale])return Ge[b.parentLocale]||(Ge[b.parentLocale]=[]),Ge[b.parentLocale].push({name:a,config:b}),null;c=Fe[b.parentLocale]._config}return Fe[a]=new C(B(c,b)),Ge[a]&&Ge[a].forEach(function(a){_a(a.name,a.config)}),$a(a),Fe[a]}return delete Fe[a],null}function ab(a,b){if(null!=b){var c,d=Ee;null!=Fe[a]&&(d=Fe[a]._config),b=B(d,b),c=new C(b),c.parentLocale=Fe[a],Fe[a]=c,$a(a)}else null!=Fe[a]&&(null!=Fe[a].parentLocale?Fe[a]=Fe[a].parentLocale:null!=Fe[a]&&delete Fe[a]);return Fe[a]}function bb(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return Be;if(!c(a)){if(b=Za(a))return b;a=[a]}return Ya(a)}function cb(){return Ad(Fe)}function db(a){var b,c=a._a;return c&&n(a).overflow===-2&&(b=c[fe]<0||c[fe]>11?fe:c[ge]<1||c[ge]>ea(c[ee],c[fe])?ge:c[he]<0||c[he]>24||24===c[he]&&(0!==c[ie]||0!==c[je]||0!==c[ke])?he:c[ie]<0||c[ie]>59?ie:c[je]<0||c[je]>59?je:c[ke]<0||c[ke]>999?ke:-1,n(a)._overflowDayOfYear&&(b<ee||b>ge)&&(b=ge),n(a)._overflowWeeks&&b===-1&&(b=le),n(a)._overflowWeekday&&b===-1&&(b=me),n(a).overflow=b),a}function eb(a){var b,c,d,e,f,g,h=a._i,i=He.exec(h)||Ie.exec(h);if(i){for(n(a).iso=!0,b=0,c=Ke.length;b<c;b++)if(Ke[b][1].exec(i[1])){e=Ke[b][0],d=Ke[b][2]!==!1;break}if(null==e)return void(a._isValid=!1);if(i[3]){for(b=0,c=Le.length;b<c;b++)if(Le[b][1].exec(i[3])){f=(i[2]||" ")+Le[b][0];break}if(null==f)return void(a._isValid=!1)}if(!d&&null!=f)return void(a._isValid=!1);if(i[4]){if(!Je.exec(i[4]))return void(a._isValid=!1);g="Z"}a._f=e+(f||"")+(g||""),lb(a)}else a._isValid=!1}function fb(a){var b,c,d,e,f,g,h,i,j={" GMT":" +0000"," EDT":" -0400"," EST":" -0500"," CDT":" -0500"," CST":" -0600"," MDT":" -0600"," MST":" -0700"," PDT":" -0700"," PST":" -0800"},k="YXWVUTSRQPONZABCDEFGHIKLM";if(b=a._i.replace(/\([^\)]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").replace(/^\s|\s$/g,""),c=Ne.exec(b)){if(d=c[1]?"ddd"+(5===c[1].length?", ":" "):"",e="D MMM "+(c[2].length>10?"YYYY ":"YY "),f="HH:mm"+(c[4]?":ss":""),c[1]){var l=new Date(c[2]),m=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][l.getDay()];if(c[1].substr(0,3)!==m)return n(a).weekdayMismatch=!0,void(a._isValid=!1)}switch(c[5].length){case 2:0===i?h=" +0000":(i=k.indexOf(c[5][1].toUpperCase())-12,h=(i<0?" -":" +")+(""+i).replace(/^-?/,"0").match(/..$/)[0]+"00");break;case 4:h=j[c[5]];break;default:h=j[" GMT"]}c[5]=h,a._i=c.splice(1).join(""),g=" ZZ",a._f=d+e+f+g,lb(a),n(a).rfc2822=!0}else a._isValid=!1}function gb(b){var c=Me.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(eb(b),void(b._isValid===!1&&(delete b._isValid,fb(b),b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b)))))}function hb(a,b,c){return null!=a?a:null!=b?b:c}function ib(b){var c=new Date(a.now());return b._useUTC?[c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()]:[c.getFullYear(),c.getMonth(),c.getDate()]}function jb(a){var b,c,d,e,f=[];if(!a._d){for(d=ib(a),a._w&&null==a._a[ge]&&null==a._a[fe]&&kb(a),null!=a._dayOfYear&&(e=hb(a._a[ee],d[ee]),(a._dayOfYear>pa(e)||0===a._dayOfYear)&&(n(a)._overflowDayOfYear=!0),c=ta(e,0,a._dayOfYear),a._a[fe]=c.getUTCMonth(),a._a[ge]=c.getUTCDate()),b=0;b<3&&null==a._a[b];++b)a._a[b]=f[b]=d[b];for(;b<7;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];24===a._a[he]&&0===a._a[ie]&&0===a._a[je]&&0===a._a[ke]&&(a._nextDay=!0,a._a[he]=0),a._d=(a._useUTC?ta:sa).apply(null,f),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[he]=24)}}function kb(a){var b,c,d,e,f,g,h,i;if(b=a._w,null!=b.GG||null!=b.W||null!=b.E)f=1,g=4,c=hb(b.GG,a._a[ee],wa(tb(),1,4).year),d=hb(b.W,1),e=hb(b.E,1),(e<1||e>7)&&(i=!0);else{f=a._locale._week.dow,g=a._locale._week.doy;var j=wa(tb(),f,g);c=hb(b.gg,a._a[ee],j.year),d=hb(b.w,j.week),null!=b.d?(e=b.d,(e<0||e>6)&&(i=!0)):null!=b.e?(e=b.e+f,(b.e<0||b.e>6)&&(i=!0)):e=f}d<1||d>xa(c,f,g)?n(a)._overflowWeeks=!0:null!=i?n(a)._overflowWeekday=!0:(h=va(c,d,e,f,g),a._a[ee]=h.year,a._dayOfYear=h.dayOfYear)}function lb(b){if(b._f===a.ISO_8601)return void eb(b);if(b._f===a.RFC_2822)return void fb(b);b._a=[],n(b).empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=Y(b._f,b._locale).match(Jd)||[],c=0;c<e.length;c++)f=e[c],d=(h.match($(f,b))||[])[0],d&&(g=h.substr(0,h.indexOf(d)),g.length>0&&n(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length),Md[f]?(d?n(b).empty=!1:n(b).unusedTokens.push(f),da(f,d,b)):b._strict&&!d&&n(b).unusedTokens.push(f);n(b).charsLeftOver=i-j,h.length>0&&n(b).unusedInput.push(h),b._a[he]<=12&&n(b).bigHour===!0&&b._a[he]>0&&(n(b).bigHour=void 0),n(b).parsedDateParts=b._a.slice(0),n(b).meridiem=b._meridiem,b._a[he]=mb(b._locale,b._a[he],b._meridiem),jb(b),db(b)}function mb(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&b<12&&(b+=12),d||12!==b||(b=0),b):b}function nb(a){var b,c,d,e,f;if(0===a._f.length)return n(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;e<a._f.length;e++)f=0,b=q({},a),null!=a._useUTC&&(b._useUTC=a._useUTC),b._f=a._f[e],lb(b),o(b)&&(f+=n(b).charsLeftOver,f+=10*n(b).unusedTokens.length,n(b).score=f,(null==d||f<d)&&(d=f,c=b));k(a,c||b)}function ob(a){if(!a._d){var b=L(a._i);a._a=i([b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],function(a){return a&&parseInt(a,10)}),jb(a)}}function pb(a){var b=new r(db(qb(a)));return b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b}function qb(a){var b=a._i,d=a._f;return a._locale=a._locale||bb(a._l),null===b||void 0===d&&""===b?p({nullInput:!0}):("string"==typeof b&&(a._i=b=a._locale.preparse(b)),s(b)?new r(db(b)):(h(b)?a._d=b:c(d)?nb(a):d?lb(a):rb(a),o(a)||(a._d=null),a))}function rb(b){var e=b._i;f(e)?b._d=new Date(a.now()):h(e)?b._d=new Date(e.valueOf()):"string"==typeof e?gb(b):c(e)?(b._a=i(e.slice(0),function(a){return parseInt(a,10)}),jb(b)):d(e)?ob(b):g(e)?b._d=new Date(e):a.createFromInputFallback(b)}function sb(a,b,f,g,h){var i={};return f!==!0&&f!==!1||(g=f,f=void 0),(d(a)&&e(a)||c(a)&&0===a.length)&&(a=void 0),i._isAMomentObject=!0,i._useUTC=i._isUTC=h,i._l=f,i._i=a,i._f=b,i._strict=g,pb(i)}function tb(a,b,c,d){return sb(a,b,c,d,!1)}function ub(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return tb();for(d=b[0],e=1;e<b.length;++e)b[e].isValid()&&!b[e][a](d)||(d=b[e]);return d}function vb(){var a=[].slice.call(arguments,0);return ub("isBefore",a)}function wb(){var a=[].slice.call(arguments,0);return ub("isAfter",a)}function xb(a){for(var b in a)if(Re.indexOf(b)===-1||null!=a[b]&&isNaN(a[b]))return!1;for(var c=!1,d=0;d<Re.length;++d)if(a[Re[d]]){if(c)return!1;parseFloat(a[Re[d]])!==u(a[Re[d]])&&(c=!0)}return!0}function yb(){return this._isValid}function zb(){return Sb(NaN)}function Ab(a){var b=L(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._isValid=xb(b),this._milliseconds=+k+1e3*j+6e4*i+1e3*h*60*60,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._locale=bb(),this._bubble()}function Bb(a){return a instanceof Ab}function Cb(a){return a<0?Math.round(-1*a)*-1:Math.round(a)}function Db(a,b){U(a,0,0,function(){var a=this.utcOffset(),c="+";return a<0&&(a=-a,c="-"),c+T(~~(a/60),2)+b+T(~~a%60,2)})}function Eb(a,b){var c=(b||"").match(a);if(null===c)return null;var d=c[c.length-1]||[],e=(d+"").match(Se)||["-",0,0],f=+(60*e[1])+u(e[2]);return 0===f?0:"+"===e[0]?f:-f}function Fb(b,c){var d,e;return c._isUTC?(d=c.clone(),e=(s(b)||h(b)?b.valueOf():tb(b).valueOf())-d.valueOf(),d._d.setTime(d._d.valueOf()+e),a.updateOffset(d,!1),d):tb(b).local()}function Gb(a){return 15*-Math.round(a._d.getTimezoneOffset()/15)}function Hb(b,c,d){var e,f=this._offset||0;if(!this.isValid())return null!=b?this:NaN;if(null!=b){if("string"==typeof b){if(b=Eb(_d,b),null===b)return this}else Math.abs(b)<16&&!d&&(b=60*b);return!this._isUTC&&c&&(e=Gb(this)),this._offset=b,this._isUTC=!0,null!=e&&this.add(e,"m"),f!==b&&(!c||this._changeInProgress?Xb(this,Sb(b-f,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?f:Gb(this)}function Ib(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Jb(a){return this.utcOffset(0,a)}function Kb(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Gb(this),"m")),this}function Lb(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var a=Eb($d,this._i);null!=a?this.utcOffset(a):this.utcOffset(0,!0)}return this}function Mb(a){return!!this.isValid()&&(a=a?tb(a).utcOffset():0,(this.utcOffset()-a)%60===0)}function Nb(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Ob(){if(!f(this._isDSTShifted))return this._isDSTShifted;var a={};if(q(a,this),a=qb(a),a._a){var b=a._isUTC?l(a._a):tb(a._a);this._isDSTShifted=this.isValid()&&v(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Pb(){return!!this.isValid()&&!this._isUTC}function Qb(){return!!this.isValid()&&this._isUTC}function Rb(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}function Sb(a,b){var c,d,e,f=a,h=null;return Bb(a)?f={ms:a._milliseconds,d:a._days,M:a._months}:g(a)?(f={},b?f[b]=a:f.milliseconds=a):(h=Te.exec(a))?(c="-"===h[1]?-1:1,f={y:0,d:u(h[ge])*c,h:u(h[he])*c,m:u(h[ie])*c,s:u(h[je])*c,ms:u(Cb(1e3*h[ke]))*c}):(h=Ue.exec(a))?(c="-"===h[1]?-1:1,f={y:Tb(h[2],c),M:Tb(h[3],c),w:Tb(h[4],c),d:Tb(h[5],c),h:Tb(h[6],c),m:Tb(h[7],c),s:Tb(h[8],c)}):null==f?f={}:"object"==typeof f&&("from"in f||"to"in f)&&(e=Vb(tb(f.from),tb(f.to)),f={},f.ms=e.milliseconds,f.M=e.months),d=new Ab(f),Bb(a)&&j(a,"_locale")&&(d._locale=a._locale),d}function Tb(a,b){var c=a&&parseFloat(a.replace(",","."));return(isNaN(c)?0:c)*b}function Ub(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function Vb(a,b){var c;return a.isValid()&&b.isValid()?(b=Fb(b,a),a.isBefore(b)?c=Ub(a,b):(c=Ub(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}}function Wb(a,b){return function(c,d){var e,f;return null===d||isNaN(+d)||(y(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Sb(c,d),Xb(this,e,a),this}}function Xb(b,c,d,e){var f=c._milliseconds,g=Cb(c._days),h=Cb(c._months);b.isValid()&&(e=null==e||e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&Q(b,"Date",P(b,"Date")+g*d),h&&ja(b,P(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function Yb(a,b){var c=a.diff(b,"days",!0);return c<-6?"sameElse":c<-1?"lastWeek":c<0?"lastDay":c<1?"sameDay":c<2?"nextDay":c<7?"nextWeek":"sameElse"}function Zb(b,c){var d=b||tb(),e=Fb(d,this).startOf("day"),f=a.calendarFormat(this,e)||"sameElse",g=c&&(z(c[f])?c[f].call(this,d):c[f]);return this.format(g||this.localeData().calendar(f,this,tb(d)))}function $b(){return new r(this)}function _b(a,b){var c=s(a)?a:tb(a);return!(!this.isValid()||!c.isValid())&&(b=K(f(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()<this.clone().startOf(b).valueOf())}function ac(a,b){var c=s(a)?a:tb(a);return!(!this.isValid()||!c.isValid())&&(b=K(f(b)?"millisecond":b),"millisecond"===b?this.valueOf()<c.valueOf():this.clone().endOf(b).valueOf()<c.valueOf())}function bc(a,b,c,d){return d=d||"()",("("===d[0]?this.isAfter(a,c):!this.isBefore(a,c))&&(")"===d[1]?this.isBefore(b,c):!this.isAfter(b,c))}function cc(a,b){var c,d=s(a)?a:tb(a);return!(!this.isValid()||!d.isValid())&&(b=K(b||"millisecond"),"millisecond"===b?this.valueOf()===d.valueOf():(c=d.valueOf(),this.clone().startOf(b).valueOf()<=c&&c<=this.clone().endOf(b).valueOf()))}function dc(a,b){return this.isSame(a,b)||this.isAfter(a,b)}function ec(a,b){return this.isSame(a,b)||this.isBefore(a,b)}function fc(a,b,c){var d,e,f,g;return this.isValid()?(d=Fb(a,this),d.isValid()?(e=6e4*(d.utcOffset()-this.utcOffset()),b=K(b),"year"===b||"month"===b||"quarter"===b?(g=gc(this,d),"quarter"===b?g/=3:"year"===b&&(g/=12)):(f=this-d,g="second"===b?f/1e3:"minute"===b?f/6e4:"hour"===b?f/36e5:"day"===b?(f-e)/864e5:"week"===b?(f-e)/6048e5:f),c?g:t(g)):NaN):NaN}function gc(a,b){var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),f=a.clone().add(e,"months");return b-f<0?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)||0}function hc(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function ic(){if(!this.isValid())return null;var a=this.clone().utc();return a.year()<0||a.year()>9999?X(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):z(Date.prototype.toISOString)?this.toDate().toISOString():X(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")}function jc(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var a="moment",b="";this.isLocal()||(a=0===this.utcOffset()?"moment.utc":"moment.parseZone",b="Z");var c="["+a+'("]',d=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",e="-MM-DD[T]HH:mm:ss.SSS",f=b+'[")]';return this.format(c+d+e+f)}function kc(b){b||(b=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var c=X(this,b);return this.localeData().postformat(c)}function lc(a,b){return this.isValid()&&(s(a)&&a.isValid()||tb(a).isValid())?Sb({to:this,from:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function mc(a){return this.from(tb(),a)}function nc(a,b){return this.isValid()&&(s(a)&&a.isValid()||tb(a).isValid())?Sb({from:this,to:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function oc(a){return this.to(tb(),a)}function pc(a){var b;return void 0===a?this._locale._abbr:(b=bb(a),null!=b&&(this._locale=b),this)}function qc(){return this._locale}function rc(a){switch(a=K(a)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":case"date":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a&&this.weekday(0),"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this}function sc(a){return a=K(a),void 0===a||"millisecond"===a?this:("date"===a&&(a="day"),this.startOf(a).add(1,"isoWeek"===a?"week":a).subtract(1,"ms"))}function tc(){return this._d.valueOf()-6e4*(this._offset||0)}function uc(){return Math.floor(this.valueOf()/1e3)}function vc(){return new Date(this.valueOf())}function wc(){var a=this;return[a.year(),a.month(),a.date(),a.hour(),a.minute(),a.second(),a.millisecond()]}function xc(){var a=this;return{years:a.year(),months:a.month(),date:a.date(),hours:a.hours(),minutes:a.minutes(),seconds:a.seconds(),milliseconds:a.milliseconds()}}function yc(){return this.isValid()?this.toISOString():null}function zc(){return o(this)}function Ac(){
return k({},n(this))}function Bc(){return n(this).overflow}function Cc(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}}function Dc(a,b){U(0,[a,a.length],0,b)}function Ec(a){return Ic.call(this,a,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)}function Fc(a){return Ic.call(this,a,this.isoWeek(),this.isoWeekday(),1,4)}function Gc(){return xa(this.year(),1,4)}function Hc(){var a=this.localeData()._week;return xa(this.year(),a.dow,a.doy)}function Ic(a,b,c,d,e){var f;return null==a?wa(this,d,e).year:(f=xa(a,d,e),b>f&&(b=f),Jc.call(this,a,b,c,d,e))}function Jc(a,b,c,d,e){var f=va(a,b,c,d,e),g=ta(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this}function Kc(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)}function Lc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function Mc(a,b){b[ke]=u(1e3*("0."+a))}function Nc(){return this._isUTC?"UTC":""}function Oc(){return this._isUTC?"Coordinated Universal Time":""}function Pc(a){return tb(1e3*a)}function Qc(){return tb.apply(null,arguments).parseZone()}function Rc(a){return a}function Sc(a,b,c,d){var e=bb(),f=l().set(d,b);return e[c](f,a)}function Tc(a,b,c){if(g(a)&&(b=a,a=void 0),a=a||"",null!=b)return Sc(a,b,c,"month");var d,e=[];for(d=0;d<12;d++)e[d]=Sc(a,d,c,"month");return e}function Uc(a,b,c,d){"boolean"==typeof a?(g(b)&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,g(b)&&(c=b,b=void 0),b=b||"");var e=bb(),f=a?e._week.dow:0;if(null!=c)return Sc(b,(c+f)%7,d,"day");var h,i=[];for(h=0;h<7;h++)i[h]=Sc(b,(h+f)%7,d,"day");return i}function Vc(a,b){return Tc(a,b,"months")}function Wc(a,b){return Tc(a,b,"monthsShort")}function Xc(a,b,c){return Uc(a,b,c,"weekdays")}function Yc(a,b,c){return Uc(a,b,c,"weekdaysShort")}function Zc(a,b,c){return Uc(a,b,c,"weekdaysMin")}function $c(){var a=this._data;return this._milliseconds=df(this._milliseconds),this._days=df(this._days),this._months=df(this._months),a.milliseconds=df(a.milliseconds),a.seconds=df(a.seconds),a.minutes=df(a.minutes),a.hours=df(a.hours),a.months=df(a.months),a.years=df(a.years),this}function _c(a,b,c,d){var e=Sb(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}function ad(a,b){return _c(this,a,b,1)}function bd(a,b){return _c(this,a,b,-1)}function cd(a){return a<0?Math.floor(a):Math.ceil(a)}function dd(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data;return f>=0&&g>=0&&h>=0||f<=0&&g<=0&&h<=0||(f+=864e5*cd(fd(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=t(f/1e3),i.seconds=a%60,b=t(a/60),i.minutes=b%60,c=t(b/60),i.hours=c%24,g+=t(c/24),e=t(ed(g)),h+=e,g-=cd(fd(e)),d=t(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function ed(a){return 4800*a/146097}function fd(a){return 146097*a/4800}function gd(a){if(!this.isValid())return NaN;var b,c,d=this._milliseconds;if(a=K(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+ed(b),"month"===a?c:c/12;switch(b=this._days+Math.round(fd(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}function hd(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*u(this._months/12):NaN}function id(a){return function(){return this.as(a)}}function jd(a){return a=K(a),this.isValid()?this[a+"s"]():NaN}function kd(a){return function(){return this.isValid()?this._data[a]:NaN}}function ld(){return t(this.days()/7)}function md(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function nd(a,b,c){var d=Sb(a).abs(),e=uf(d.as("s")),f=uf(d.as("m")),g=uf(d.as("h")),h=uf(d.as("d")),i=uf(d.as("M")),j=uf(d.as("y")),k=e<=vf.ss&&["s",e]||e<vf.s&&["ss",e]||f<=1&&["m"]||f<vf.m&&["mm",f]||g<=1&&["h"]||g<vf.h&&["hh",g]||h<=1&&["d"]||h<vf.d&&["dd",h]||i<=1&&["M"]||i<vf.M&&["MM",i]||j<=1&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,md.apply(null,k)}function od(a){return void 0===a?uf:"function"==typeof a&&(uf=a,!0)}function pd(a,b){return void 0!==vf[a]&&(void 0===b?vf[a]:(vf[a]=b,"s"===a&&(vf.ss=b-1),!0))}function qd(a){if(!this.isValid())return this.localeData().invalidDate();var b=this.localeData(),c=nd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function rd(){if(!this.isValid())return this.localeData().invalidDate();var a,b,c,d=wf(this._milliseconds)/1e3,e=wf(this._days),f=wf(this._months);a=t(d/60),b=t(a/60),d%=60,a%=60,c=t(f/12),f%=12;var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(m<0?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var sd,td;td=Array.prototype.some?Array.prototype.some:function(a){for(var b=Object(this),c=b.length>>>0,d=0;d<c;d++)if(d in b&&a.call(this,b[d],d,b))return!0;return!1};var ud=td,vd=a.momentProperties=[],wd=!1,xd={};a.suppressDeprecationWarnings=!1,a.deprecationHandler=null;var yd;yd=Object.keys?Object.keys:function(a){var b,c=[];for(b in a)j(a,b)&&c.push(b);return c};var zd,Ad=yd,Bd={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Cd={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},Dd="Invalid date",Ed="%d",Fd=/\d{1,2}/,Gd={future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Hd={},Id={},Jd=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Kd=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Ld={},Md={},Nd=/\d/,Od=/\d\d/,Pd=/\d{3}/,Qd=/\d{4}/,Rd=/[+-]?\d{6}/,Sd=/\d\d?/,Td=/\d\d\d\d?/,Ud=/\d\d\d\d\d\d?/,Vd=/\d{1,3}/,Wd=/\d{1,4}/,Xd=/[+-]?\d{1,6}/,Yd=/\d+/,Zd=/[+-]?\d+/,$d=/Z|[+-]\d\d:?\d\d/gi,_d=/Z|[+-]\d\d(?::?\d\d)?/gi,ae=/[+-]?\d+(\.\d{1,3})?/,be=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,ce={},de={},ee=0,fe=1,ge=2,he=3,ie=4,je=5,ke=6,le=7,me=8;zd=Array.prototype.indexOf?Array.prototype.indexOf:function(a){var b;for(b=0;b<this.length;++b)if(this[b]===a)return b;return-1};var ne=zd;U("M",["MM",2],"Mo",function(){return this.month()+1}),U("MMM",0,0,function(a){return this.localeData().monthsShort(this,a)}),U("MMMM",0,0,function(a){return this.localeData().months(this,a)}),J("month","M"),M("month",8),Z("M",Sd),Z("MM",Sd,Od),Z("MMM",function(a,b){return b.monthsShortRegex(a)}),Z("MMMM",function(a,b){return b.monthsRegex(a)}),ba(["M","MM"],function(a,b){b[fe]=u(a)-1}),ba(["MMM","MMMM"],function(a,b,c,d){var e=c._locale.monthsParse(a,d,c._strict);null!=e?b[fe]=e:n(c).invalidMonth=a});var oe=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,pe="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),qe="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),re=be,se=be;U("Y",0,0,function(){var a=this.year();return a<=9999?""+a:"+"+a}),U(0,["YY",2],0,function(){return this.year()%100}),U(0,["YYYY",4],0,"year"),U(0,["YYYYY",5],0,"year"),U(0,["YYYYYY",6,!0],0,"year"),J("year","y"),M("year",1),Z("Y",Zd),Z("YY",Sd,Od),Z("YYYY",Wd,Qd),Z("YYYYY",Xd,Rd),Z("YYYYYY",Xd,Rd),ba(["YYYYY","YYYYYY"],ee),ba("YYYY",function(b,c){c[ee]=2===b.length?a.parseTwoDigitYear(b):u(b)}),ba("YY",function(b,c){c[ee]=a.parseTwoDigitYear(b)}),ba("Y",function(a,b){b[ee]=parseInt(a,10)}),a.parseTwoDigitYear=function(a){return u(a)+(u(a)>68?1900:2e3)};var te=O("FullYear",!0);U("w",["ww",2],"wo","week"),U("W",["WW",2],"Wo","isoWeek"),J("week","w"),J("isoWeek","W"),M("week",5),M("isoWeek",5),Z("w",Sd),Z("ww",Sd,Od),Z("W",Sd),Z("WW",Sd,Od),ca(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=u(a)});var ue={dow:0,doy:6};U("d",0,"do","day"),U("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),U("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),U("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),U("e",0,0,"weekday"),U("E",0,0,"isoWeekday"),J("day","d"),J("weekday","e"),J("isoWeekday","E"),M("day",11),M("weekday",11),M("isoWeekday",11),Z("d",Sd),Z("e",Sd),Z("E",Sd),Z("dd",function(a,b){return b.weekdaysMinRegex(a)}),Z("ddd",function(a,b){return b.weekdaysShortRegex(a)}),Z("dddd",function(a,b){return b.weekdaysRegex(a)}),ca(["dd","ddd","dddd"],function(a,b,c,d){var e=c._locale.weekdaysParse(a,d,c._strict);null!=e?b.d=e:n(c).invalidWeekday=a}),ca(["d","e","E"],function(a,b,c,d){b[d]=u(a)});var ve="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),we="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),xe="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),ye=be,ze=be,Ae=be;U("H",["HH",2],0,"hour"),U("h",["hh",2],0,Ra),U("k",["kk",2],0,Sa),U("hmm",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)}),U("hmmss",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)+T(this.seconds(),2)}),U("Hmm",0,0,function(){return""+this.hours()+T(this.minutes(),2)}),U("Hmmss",0,0,function(){return""+this.hours()+T(this.minutes(),2)+T(this.seconds(),2)}),Ta("a",!0),Ta("A",!1),J("hour","h"),M("hour",13),Z("a",Ua),Z("A",Ua),Z("H",Sd),Z("h",Sd),Z("k",Sd),Z("HH",Sd,Od),Z("hh",Sd,Od),Z("kk",Sd,Od),Z("hmm",Td),Z("hmmss",Ud),Z("Hmm",Td),Z("Hmmss",Ud),ba(["H","HH"],he),ba(["k","kk"],function(a,b,c){var d=u(a);b[he]=24===d?0:d}),ba(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),ba(["h","hh"],function(a,b,c){b[he]=u(a),n(c).bigHour=!0}),ba("hmm",function(a,b,c){var d=a.length-2;b[he]=u(a.substr(0,d)),b[ie]=u(a.substr(d)),n(c).bigHour=!0}),ba("hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[he]=u(a.substr(0,d)),b[ie]=u(a.substr(d,2)),b[je]=u(a.substr(e)),n(c).bigHour=!0}),ba("Hmm",function(a,b,c){var d=a.length-2;b[he]=u(a.substr(0,d)),b[ie]=u(a.substr(d))}),ba("Hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[he]=u(a.substr(0,d)),b[ie]=u(a.substr(d,2)),b[je]=u(a.substr(e))});var Be,Ce=/[ap]\.?m?\.?/i,De=O("Hours",!0),Ee={calendar:Bd,longDateFormat:Cd,invalidDate:Dd,ordinal:Ed,dayOfMonthOrdinalParse:Fd,relativeTime:Gd,months:pe,monthsShort:qe,week:ue,weekdays:ve,weekdaysMin:xe,weekdaysShort:we,meridiemParse:Ce},Fe={},Ge={},He=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ie=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Je=/Z|[+-]\d\d(?::?\d\d)?/,Ke=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Le=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Me=/^\/?Date\((\-?\d+)/i,Ne=/^((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d?\d\s(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(?:\d\d)?\d\d\s)(\d\d:\d\d)(\:\d\d)?(\s(?:UT|GMT|[ECMP][SD]T|[A-IK-Za-ik-z]|[+-]\d{4}))$/;a.createFromInputFallback=x("value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),a.ISO_8601=function(){},a.RFC_2822=function(){};var Oe=x("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=tb.apply(null,arguments);return this.isValid()&&a.isValid()?a<this?this:a:p()}),Pe=x("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=tb.apply(null,arguments);return this.isValid()&&a.isValid()?a>this?this:a:p()}),Qe=function(){return Date.now?Date.now():+new Date},Re=["year","quarter","month","week","day","hour","minute","second","millisecond"];Db("Z",":"),Db("ZZ",""),Z("Z",_d),Z("ZZ",_d),ba(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Eb(_d,a)});var Se=/([\+\-]|\d\d)/gi;a.updateOffset=function(){};var Te=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Ue=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;Sb.fn=Ab.prototype,Sb.invalid=zb;var Ve=Wb(1,"add"),We=Wb(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Xe=x("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)});U(0,["gg",2],0,function(){return this.weekYear()%100}),U(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Dc("gggg","weekYear"),Dc("ggggg","weekYear"),Dc("GGGG","isoWeekYear"),Dc("GGGGG","isoWeekYear"),J("weekYear","gg"),J("isoWeekYear","GG"),M("weekYear",1),M("isoWeekYear",1),Z("G",Zd),Z("g",Zd),Z("GG",Sd,Od),Z("gg",Sd,Od),Z("GGGG",Wd,Qd),Z("gggg",Wd,Qd),Z("GGGGG",Xd,Rd),Z("ggggg",Xd,Rd),ca(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=u(a)}),ca(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}),U("Q",0,"Qo","quarter"),J("quarter","Q"),M("quarter",7),Z("Q",Nd),ba("Q",function(a,b){b[fe]=3*(u(a)-1)}),U("D",["DD",2],"Do","date"),J("date","D"),M("date",9),Z("D",Sd),Z("DD",Sd,Od),Z("Do",function(a,b){return a?b._dayOfMonthOrdinalParse||b._ordinalParse:b._dayOfMonthOrdinalParseLenient}),ba(["D","DD"],ge),ba("Do",function(a,b){b[ge]=u(a.match(Sd)[0],10)});var Ye=O("Date",!0);U("DDD",["DDDD",3],"DDDo","dayOfYear"),J("dayOfYear","DDD"),M("dayOfYear",4),Z("DDD",Vd),Z("DDDD",Pd),ba(["DDD","DDDD"],function(a,b,c){c._dayOfYear=u(a)}),U("m",["mm",2],0,"minute"),J("minute","m"),M("minute",14),Z("m",Sd),Z("mm",Sd,Od),ba(["m","mm"],ie);var Ze=O("Minutes",!1);U("s",["ss",2],0,"second"),J("second","s"),M("second",15),Z("s",Sd),Z("ss",Sd,Od),ba(["s","ss"],je);var $e=O("Seconds",!1);U("S",0,0,function(){return~~(this.millisecond()/100)}),U(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),U(0,["SSS",3],0,"millisecond"),U(0,["SSSS",4],0,function(){return 10*this.millisecond()}),U(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),U(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),U(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),U(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),U(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),J("millisecond","ms"),M("millisecond",16),Z("S",Vd,Nd),Z("SS",Vd,Od),Z("SSS",Vd,Pd);var _e;for(_e="SSSS";_e.length<=9;_e+="S")Z(_e,Yd);for(_e="S";_e.length<=9;_e+="S")ba(_e,Mc);var af=O("Milliseconds",!1);U("z",0,0,"zoneAbbr"),U("zz",0,0,"zoneName");var bf=r.prototype;bf.add=Ve,bf.calendar=Zb,bf.clone=$b,bf.diff=fc,bf.endOf=sc,bf.format=kc,bf.from=lc,bf.fromNow=mc,bf.to=nc,bf.toNow=oc,bf.get=R,bf.invalidAt=Bc,bf.isAfter=_b,bf.isBefore=ac,bf.isBetween=bc,bf.isSame=cc,bf.isSameOrAfter=dc,bf.isSameOrBefore=ec,bf.isValid=zc,bf.lang=Xe,bf.locale=pc,bf.localeData=qc,bf.max=Pe,bf.min=Oe,bf.parsingFlags=Ac,bf.set=S,bf.startOf=rc,bf.subtract=We,bf.toArray=wc,bf.toObject=xc,bf.toDate=vc,bf.toISOString=ic,bf.inspect=jc,bf.toJSON=yc,bf.toString=hc,bf.unix=uc,bf.valueOf=tc,bf.creationData=Cc,bf.year=te,bf.isLeapYear=ra,bf.weekYear=Ec,bf.isoWeekYear=Fc,bf.quarter=bf.quarters=Kc,bf.month=ka,bf.daysInMonth=la,bf.week=bf.weeks=Ba,bf.isoWeek=bf.isoWeeks=Ca,bf.weeksInYear=Hc,bf.isoWeeksInYear=Gc,bf.date=Ye,bf.day=bf.days=Ka,bf.weekday=La,bf.isoWeekday=Ma,bf.dayOfYear=Lc,bf.hour=bf.hours=De,bf.minute=bf.minutes=Ze,bf.second=bf.seconds=$e,bf.millisecond=bf.milliseconds=af,bf.utcOffset=Hb,bf.utc=Jb,bf.local=Kb,bf.parseZone=Lb,bf.hasAlignedHourOffset=Mb,bf.isDST=Nb,bf.isLocal=Pb,bf.isUtcOffset=Qb,bf.isUtc=Rb,bf.isUTC=Rb,bf.zoneAbbr=Nc,bf.zoneName=Oc,bf.dates=x("dates accessor is deprecated. Use date instead.",Ye),bf.months=x("months accessor is deprecated. Use month instead",ka),bf.years=x("years accessor is deprecated. Use year instead",te),bf.zone=x("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Ib),bf.isDSTShifted=x("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Ob);var cf=C.prototype;cf.calendar=D,cf.longDateFormat=E,cf.invalidDate=F,cf.ordinal=G,cf.preparse=Rc,cf.postformat=Rc,cf.relativeTime=H,cf.pastFuture=I,cf.set=A,cf.months=fa,cf.monthsShort=ga,cf.monthsParse=ia,cf.monthsRegex=na,cf.monthsShortRegex=ma,cf.week=ya,cf.firstDayOfYear=Aa,cf.firstDayOfWeek=za,cf.weekdays=Fa,cf.weekdaysMin=Ha,cf.weekdaysShort=Ga,cf.weekdaysParse=Ja,cf.weekdaysRegex=Na,cf.weekdaysShortRegex=Oa,cf.weekdaysMinRegex=Pa,cf.isPM=Va,cf.meridiem=Wa,$a("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===u(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),a.lang=x("moment.lang is deprecated. Use moment.locale instead.",$a),a.langData=x("moment.langData is deprecated. Use moment.localeData instead.",bb);var df=Math.abs,ef=id("ms"),ff=id("s"),gf=id("m"),hf=id("h"),jf=id("d"),kf=id("w"),lf=id("M"),mf=id("y"),nf=kd("milliseconds"),of=kd("seconds"),pf=kd("minutes"),qf=kd("hours"),rf=kd("days"),sf=kd("months"),tf=kd("years"),uf=Math.round,vf={ss:44,s:45,m:45,h:22,d:26,M:11},wf=Math.abs,xf=Ab.prototype;return xf.isValid=yb,xf.abs=$c,xf.add=ad,xf.subtract=bd,xf.as=gd,xf.asMilliseconds=ef,xf.asSeconds=ff,xf.asMinutes=gf,xf.asHours=hf,xf.asDays=jf,xf.asWeeks=kf,xf.asMonths=lf,xf.asYears=mf,xf.valueOf=hd,xf._bubble=dd,xf.get=jd,xf.milliseconds=nf,xf.seconds=of,xf.minutes=pf,xf.hours=qf,xf.days=rf,xf.weeks=ld,xf.months=sf,xf.years=tf,xf.humanize=qd,xf.toISOString=rd,xf.toString=rd,xf.toJSON=rd,xf.locale=pc,xf.localeData=qc,xf.toIsoString=x("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",rd),xf.lang=Xe,U("X",0,0,"unix"),U("x",0,0,"valueOf"),Z("x",Zd),Z("X",ae),ba("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),ba("x",function(a,b,c){c._d=new Date(u(a))}),a.version="2.18.1",b(tb),a.fn=bf,a.min=vb,a.max=wb,a.now=Qe,a.utc=l,a.unix=Pc,a.months=Vc,a.isDate=h,a.locale=$a,a.invalid=p,a.duration=Sb,a.isMoment=s,a.weekdays=Xc,a.parseZone=Qc,a.localeData=bb,a.isDuration=Bb,a.monthsShort=Wc,a.weekdaysMin=Zc,a.defineLocale=_a,a.updateLocale=ab,a.locales=cb,a.weekdaysShort=Yc,a.normalizeUnits=K,a.relativeTimeRounding=od,a.relativeTimeThreshold=pd,a.calendarFormat=Yb,a.prototype=bf,a});
\ No newline at end of file
...@@ -31,8 +31,8 @@ ...@@ -31,8 +31,8 @@
<div class="ibox-content"> <div class="ibox-content">
{% if form.errors.all %} {% if form.errors.all %}
<div class="alert alert-danger" style="margin: 20px auto 0px"> <div class="alert alert-danger" style="margin: 20px auto 0px">
{{ form.errors.all }} {{ form.errors.all }}
</div> </div>
{% endif %} {% endif %}
{% block form %} {% block form %}
{% endblock %} {% endblock %}
......
{% load i18n %} {% load i18n %}
<div class="footer fixed"> <div class="footer fixed">
<div class="pull-right"> <div class="pull-right">
Version <strong>1.4.3-{% include '_build.html' %}</strong> GPLv2. Version <strong>1.4.4-{% include '_build.html' %}</strong> GPLv2.
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">--> <!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
</div> </div>
<div> <div>
......
{% load i18n %} {% load i18n %}
{% block password_expired_message %}
{% url 'users:user-password-update' as user_password_update_url %}
{% if request.user.password_has_expired %}
<div class="alert alert-danger help-message alert-dismissable">
{% blocktrans %}
Your password has expired, please click <a href="{{ user_password_update_url }}"> this link </a> update password.
{% endblocktrans %}
<button aria-hidden="true" data-dismiss="alert" class="close" type="button" style="outline: none;">×</button>
</div>
{% elif request.user.password_will_expired %}
<div class="alert alert-danger help-message alert-dismissable">
{% trans 'Your password will at' %} {{ request.user.date_password_expired }} {% trans 'expired. ' %}
{% blocktrans %}
please click <a href="{{ user_password_update_url }}"> this link </a> to update your password.
{% endblocktrans %}
<button aria-hidden="true" data-dismiss="alert" class="close" type="button" style="outline: none;">×</button>
</div>
{% endif %}
{% endblock %}
{% block first_login_message %} {% block first_login_message %}
{% if request.user.is_authenticated and request.user.is_first_login %} {% if request.user.is_authenticated and request.user.is_first_login %}
<div class="alert alert-danger help-message alert-dismissable"> <div class="alert alert-danger help-message alert-dismissable">
...@@ -6,7 +27,6 @@ ...@@ -6,7 +27,6 @@
{% blocktrans %} {% blocktrans %}
Your information was incomplete. Please click <a href="{{ first_login_url }}"> this link </a>to complete your information. Your information was incomplete. Please click <a href="{{ first_login_url }}"> this link </a>to complete your information.
{% endblocktrans %} {% endblocktrans %}
<button aria-hidden="true" data-dismiss="alert" class="close" type="button" style="outline: none;">×</button> <button aria-hidden="true" data-dismiss="alert" class="close" type="button" style="outline: none;">×</button>
</div> </div>
{% endif %} {% endif %}
......
# -*- coding: utf-8 -*-
#
# -*- coding: utf-8 -*-
#
from .terminal import *
from .session import *
from .task import *
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from collections import OrderedDict
import logging import logging
import os import os
import uuid
from django.core.cache import cache from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, redirect
from django.utils import timezone
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.http.response import HttpResponseRedirectBase
from django.http import HttpResponseNotFound from django.http import HttpResponseNotFound
from django.conf import settings from django.conf import settings
import jms_storage
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.views import APIView, Response from rest_framework.views import Response
from rest_framework.permissions import AllowAny
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
import jms_storage
from common.utils import get_object_or_none, is_uuid from common.utils import is_uuid
from .hands import SystemUser from common.permissions import IsOrgAdminOrAppUser
from .models import Terminal, Status, Session, Task from ...hands import SystemUser
from .serializers import TerminalSerializer, StatusSerializer, \ from ...models import Terminal, Session
SessionSerializer, TaskSerializer, ReplaySerializer from ...serializers import v1 as serializers
from common.permissions import IsAppUser, IsOrgAdminOrAppUser from ...backends import get_command_storage, get_multi_command_storage, \
from .backends import get_command_storage, get_multi_command_storage, \
SessionCommandSerializer SessionCommandSerializer
__all__ = ['SessionViewSet', 'SessionReplayViewSet', 'CommandViewSet']
logger = logging.getLogger(__file__) logger = logging.getLogger(__file__)
class TerminalViewSet(viewsets.ModelViewSet):
queryset = Terminal.objects.filter(is_deleted=False)
serializer_class = TerminalSerializer
permission_classes = (AllowAny,)
def create(self, request, *args, **kwargs):
name = request.data.get('name')
remote_ip = request.META.get('REMOTE_ADDR')
x_real_ip = request.META.get('X-Real-IP')
remote_addr = x_real_ip or remote_ip
terminal = get_object_or_none(Terminal, name=name, is_deleted=False)
if terminal:
msg = 'Terminal name %s already used' % name
return Response({'msg': msg}, status=409)
serializer = self.serializer_class(data={
'name': name, 'remote_addr': remote_addr
})
if serializer.is_valid():
terminal = serializer.save()
# App should use id, token get access key, if accepted
token = uuid.uuid4().hex
cache.set(token, str(terminal.id), 3600)
data = {"id": str(terminal.id), "token": token, "msg": "Need accept"}
return Response(data, status=201)
else:
data = serializer.errors
logger.error("Register terminal error: {}".format(data))
return Response(data, status=400)
def get_permissions(self):
if self.action == "create":
self.permission_classes = (AllowAny,)
return super().get_permissions()
class TerminalTokenApi(APIView):
permission_classes = (AllowAny,)
queryset = Terminal.objects.filter(is_deleted=False)
def get(self, request, *args, **kwargs):
try:
terminal = self.queryset.get(id=kwargs.get('terminal'))
except Terminal.DoesNotExist:
terminal = None
token = request.query_params.get("token")
if terminal is None:
return Response('May be reject by administrator', status=401)
if token is None or cache.get(token, "") != str(terminal.id):
return Response('Token is not valid', status=401)
if not terminal.is_accepted:
return Response("Terminal was not accepted yet", status=400)
if not terminal.user or not terminal.user.access_key.all():
return Response("No access key generate", status=401)
access_key = terminal.user.access_key.first()
data = OrderedDict()
data['access_key'] = {'id': access_key.id, 'secret': access_key.secret}
return Response(data, status=200)
class StatusViewSet(viewsets.ModelViewSet):
queryset = Status.objects.all()
serializer_class = StatusSerializer
permission_classes = (IsOrgAdminOrAppUser,)
session_serializer_class = SessionSerializer
task_serializer_class = TaskSerializer
def create(self, request, *args, **kwargs):
from_gua = self.request.query_params.get("from_guacamole", None)
if not from_gua:
self.handle_sessions()
super().create(request, *args, **kwargs)
tasks = self.request.user.terminal.task_set.filter(is_finished=False)
serializer = self.task_serializer_class(tasks, many=True)
return Response(serializer.data, status=201)
def handle_sessions(self):
sessions_active = []
for session_data in self.request.data.get("sessions", []):
self.create_or_update_session(session_data)
if not session_data["is_finished"]:
sessions_active.append(session_data["id"])
sessions_in_db_active = Session.objects.filter(
is_finished=False,
terminal=self.request.user.terminal.id
)
for session in sessions_in_db_active:
if str(session.id) not in sessions_active:
session.is_finished = True
session.date_end = timezone.now()
session.save()
def create_or_update_session(self, session_data):
session_data["terminal"] = self.request.user.terminal.id
_id = session_data["id"]
session = get_object_or_none(Session, id=_id)
if session:
serializer = SessionSerializer(
data=session_data, instance=session
)
else:
serializer = SessionSerializer(data=session_data)
if serializer.is_valid():
session = serializer.save()
return session
else:
msg = "session data is not valid {}: {}".format(
serializer.errors, str(serializer.data)
)
logger.error(msg)
return None
def get_queryset(self):
terminal_id = self.kwargs.get("terminal", None)
if terminal_id:
terminal = get_object_or_404(Terminal, id=terminal_id)
self.queryset = terminal.status_set.all()
return self.queryset
def perform_create(self, serializer):
serializer.validated_data["terminal"] = self.request.user.terminal
return super().perform_create(serializer)
def get_permissions(self):
if self.action == "create":
self.permission_classes = (IsAppUser,)
return super().get_permissions()
class SessionViewSet(BulkModelViewSet): class SessionViewSet(BulkModelViewSet):
queryset = Session.objects.all() queryset = Session.objects.all()
serializer_class = SessionSerializer serializer_class = serializers.SessionSerializer
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
...@@ -198,29 +50,6 @@ class SessionViewSet(BulkModelViewSet): ...@@ -198,29 +50,6 @@ class SessionViewSet(BulkModelViewSet):
return super().perform_create(serializer) return super().perform_create(serializer)
class TaskViewSet(BulkModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
permission_classes = (IsOrgAdminOrAppUser,)
class KillSessionAPI(APIView):
permission_classes = (IsOrgAdminOrAppUser,)
model = Task
def post(self, request, *args, **kwargs):
validated_session = []
for session_id in request.data:
session = get_object_or_none(Session, id=session_id)
if session and not session.is_finished:
validated_session.append(session_id)
self.model.objects.create(
name="kill_session", args=session.id,
terminal=session.terminal,
)
return Response({"ok": validated_session})
class CommandViewSet(viewsets.ViewSet): class CommandViewSet(viewsets.ViewSet):
"""接受app发送来的command log, 格式如下 """接受app发送来的command log, 格式如下
{ {
...@@ -262,7 +91,7 @@ class CommandViewSet(viewsets.ViewSet): ...@@ -262,7 +91,7 @@ class CommandViewSet(viewsets.ViewSet):
class SessionReplayViewSet(viewsets.ViewSet): class SessionReplayViewSet(viewsets.ViewSet):
serializer_class = ReplaySerializer serializer_class = serializers.ReplaySerializer
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
session = None session = None
upload_to = 'replay' # 仅添加到本地存储中 upload_to = 'replay' # 仅添加到本地存储中
...@@ -318,6 +147,11 @@ class SessionReplayViewSet(viewsets.ViewSet): ...@@ -318,6 +147,11 @@ class SessionReplayViewSet(viewsets.ViewSet):
session_id = kwargs.get('pk') session_id = kwargs.get('pk')
self.session = get_object_or_404(Session, id=session_id) self.session = get_object_or_404(Session, id=session_id)
data = {
'type': 'guacamole' if self.session.protocol == 'rdp' else 'json',
'src': '',
}
# 新版本和老版本的文件后缀不同 # 新版本和老版本的文件后缀不同
session_path = self.get_session_path() # 存在外部存储上的路径 session_path = self.get_session_path() # 存在外部存储上的路径
local_path = self.get_local_path() local_path = self.get_local_path()
...@@ -327,7 +161,8 @@ class SessionReplayViewSet(viewsets.ViewSet): ...@@ -327,7 +161,8 @@ class SessionReplayViewSet(viewsets.ViewSet):
for _local_path in (local_path, local_path_v1, session_path): for _local_path in (local_path, local_path_v1, session_path):
if default_storage.exists(_local_path): if default_storage.exists(_local_path):
url = default_storage.url(_local_path) url = default_storage.url(_local_path)
return redirect(url) data['src'] = url
return Response(data)
# 去定义的外部storage查找 # 去定义的外部storage查找
configs = settings.TERMINAL_REPLAY_STORAGE configs = settings.TERMINAL_REPLAY_STORAGE
...@@ -344,31 +179,6 @@ class SessionReplayViewSet(viewsets.ViewSet): ...@@ -344,31 +179,6 @@ class SessionReplayViewSet(viewsets.ViewSet):
if not ok: if not ok:
logger.error("Failed download replay file: {}".format(err)) logger.error("Failed download replay file: {}".format(err))
return HttpResponseNotFound() return HttpResponseNotFound()
return redirect(default_storage.url(local_path)) data['src'] = default_storage.url(local_path)
return Response(data)
class SessionReplayV2ViewSet(SessionReplayViewSet):
serializer_class = ReplaySerializer
permission_classes = (IsOrgAdminOrAppUser,)
session = None
def retrieve(self, request, *args, **kwargs):
response = super().retrieve(request, *args, **kwargs)
data = {
'type': 'guacamole' if self.session.protocol == 'rdp' else 'json',
'src': '',
}
if isinstance(response, HttpResponseRedirectBase):
data['src'] = response.url
return Response(data)
return HttpResponseNotFound()
class TerminalConfig(APIView):
permission_classes = (IsAppUser,)
def get(self, request):
user = request.user
terminal = user.terminal
configs = terminal.config
return Response(configs, status=200)
# -*- coding: utf-8 -*-
#
import logging
from rest_framework.views import APIView, Response
from rest_framework_bulk import BulkModelViewSet
from common.utils import get_object_or_none
from common.permissions import IsOrgAdminOrAppUser
from ...models import Session, Task
from ...serializers import v1 as serializers
__all__ = ['TaskViewSet', 'KillSessionAPI']
logger = logging.getLogger(__file__)
class TaskViewSet(BulkModelViewSet):
queryset = Task.objects.all()
serializer_class = serializers.TaskSerializer
permission_classes = (IsOrgAdminOrAppUser,)
class KillSessionAPI(APIView):
permission_classes = (IsOrgAdminOrAppUser,)
model = Task
def post(self, request, *args, **kwargs):
validated_session = []
for session_id in request.data:
session = get_object_or_none(Session, id=session_id)
if session and not session.is_finished:
validated_session.append(session_id)
self.model.objects.create(
name="kill_session", args=session.id,
terminal=session.terminal,
)
return Response({"ok": validated_session})
# -*- coding: utf-8 -*-
#
from collections import OrderedDict
import logging
import uuid
from django.core.cache import cache
from django.shortcuts import get_object_or_404, redirect
from django.utils import timezone
from rest_framework import viewsets
from rest_framework.views import APIView, Response
from rest_framework.permissions import AllowAny
from common.utils import get_object_or_none
from common.permissions import IsAppUser, IsOrgAdminOrAppUser, IsSuperUser
from ...models import Terminal, Status, Session
from ...serializers import v1 as serializers
__all__ = [
'TerminalViewSet', 'TerminalTokenApi', 'StatusViewSet', 'TerminalConfig',
]
logger = logging.getLogger(__file__)
class TerminalViewSet(viewsets.ModelViewSet):
queryset = Terminal.objects.filter(is_deleted=False)
serializer_class = serializers.TerminalSerializer
permission_classes = (IsSuperUser,)
def create(self, request, *args, **kwargs):
name = request.data.get('name')
remote_ip = request.META.get('REMOTE_ADDR')
x_real_ip = request.META.get('X-Real-IP')
remote_addr = x_real_ip or remote_ip
terminal = get_object_or_none(Terminal, name=name, is_deleted=False)
if terminal:
msg = 'Terminal name %s already used' % name
return Response({'msg': msg}, status=409)
serializer = self.serializer_class(data={
'name': name, 'remote_addr': remote_addr
})
if serializer.is_valid():
terminal = serializer.save()
# App should use id, token get access key, if accepted
token = uuid.uuid4().hex
cache.set(token, str(terminal.id), 3600)
data = {"id": str(terminal.id), "token": token, "msg": "Need accept"}
return Response(data, status=201)
else:
data = serializer.errors
logger.error("Register terminal error: {}".format(data))
return Response(data, status=400)
def get_permissions(self):
if self.action == "create":
self.permission_classes = (AllowAny,)
return super().get_permissions()
class TerminalTokenApi(APIView):
permission_classes = (AllowAny,)
queryset = Terminal.objects.filter(is_deleted=False)
def get(self, request, *args, **kwargs):
try:
terminal = self.queryset.get(id=kwargs.get('terminal'))
except Terminal.DoesNotExist:
terminal = None
token = request.query_params.get("token")
if terminal is None:
return Response('May be reject by administrator', status=401)
if token is None or cache.get(token, "") != str(terminal.id):
return Response('Token is not valid', status=401)
if not terminal.is_accepted:
return Response("Terminal was not accepted yet", status=400)
if not terminal.user or not terminal.user.access_key.all():
return Response("No access key generate", status=401)
access_key = terminal.user.access_key.first()
data = OrderedDict()
data['access_key'] = {'id': access_key.id, 'secret': access_key.secret}
return Response(data, status=200)
class StatusViewSet(viewsets.ModelViewSet):
queryset = Status.objects.all()
serializer_class = serializers.StatusSerializer
permission_classes = (IsOrgAdminOrAppUser,)
session_serializer_class = serializers.SessionSerializer
task_serializer_class = serializers.TaskSerializer
def create(self, request, *args, **kwargs):
from_gua = self.request.query_params.get("from_guacamole", None)
if not from_gua:
self.handle_sessions()
super().create(request, *args, **kwargs)
tasks = self.request.user.terminal.task_set.filter(is_finished=False)
serializer = self.task_serializer_class(tasks, many=True)
return Response(serializer.data, status=201)
def handle_sessions(self):
sessions_active = []
for session_data in self.request.data.get("sessions", []):
self.create_or_update_session(session_data)
if not session_data["is_finished"]:
sessions_active.append(session_data["id"])
sessions_in_db_active = Session.objects.filter(
is_finished=False,
terminal=self.request.user.terminal.id
)
for session in sessions_in_db_active:
if str(session.id) not in sessions_active:
session.is_finished = True
session.date_end = timezone.now()
session.save()
def create_or_update_session(self, session_data):
session_data["terminal"] = self.request.user.terminal.id
_id = session_data["id"]
session = get_object_or_none(Session, id=_id)
if session:
serializer = serializers.SessionSerializer(
data=session_data, instance=session
)
else:
serializer = serializers.SessionSerializer(data=session_data)
if serializer.is_valid():
session = serializer.save()
return session
else:
msg = "session data is not valid {}: {}".format(
serializer.errors, str(serializer.data)
)
logger.error(msg)
return None
def get_queryset(self):
terminal_id = self.kwargs.get("terminal", None)
if terminal_id:
terminal = get_object_or_404(Terminal, id=terminal_id)
self.queryset = terminal.status_set.all()
return self.queryset
def perform_create(self, serializer):
serializer.validated_data["terminal"] = self.request.user.terminal
return super().perform_create(serializer)
def get_permissions(self):
if self.action == "create":
self.permission_classes = (IsAppUser,)
return super().get_permissions()
class TerminalConfig(APIView):
permission_classes = (IsAppUser,)
def get(self, request):
user = request.user
terminal = user.terminal
configs = terminal.config
return Response(configs, status=200)
\ No newline at end of file
# -*- coding: utf-8 -*-
#
from .terminal import *
# -*- coding: utf-8 -*-
#
from rest_framework import viewsets
from common.permissions import IsSuperUser, WithBootstrapToken
from ...models import Terminal
from ...serializers import v2 as serializers
__all__ = ['TerminalViewSet', 'TerminalRegistrationViewSet']
class TerminalViewSet(viewsets.ModelViewSet):
queryset = Terminal.objects.filter(is_deleted=False)
serializer_class = serializers.TerminalSerializer
permission_classes = [IsSuperUser]
class TerminalRegistrationViewSet(viewsets.ModelViewSet):
queryset = Terminal.objects.filter(is_deleted=False)
serializer_class = serializers.TerminalRegistrationSerializer
permission_classes = [WithBootstrapToken]
http_method_names = ['post']
...@@ -2,6 +2,8 @@ from importlib import import_module ...@@ -2,6 +2,8 @@ from importlib import import_module
from django.conf import settings from django.conf import settings
from .command.serializers import SessionCommandSerializer from .command.serializers import SessionCommandSerializer
from common import utils
TYPE_ENGINE_MAPPING = { TYPE_ENGINE_MAPPING = {
'elasticsearch': 'terminal.backends.command.es', 'elasticsearch': 'terminal.backends.command.es',
} }
...@@ -16,7 +18,9 @@ def get_command_storage(): ...@@ -16,7 +18,9 @@ def get_command_storage():
def get_terminal_command_storages(): def get_terminal_command_storages():
storage_list = {} storage_list = {}
for name, params in settings.TERMINAL_COMMAND_STORAGE.items(): command_storage = utils.get_command_storage_setting()
for name, params in command_storage.items():
tp = params['TYPE'] tp = params['TYPE']
if tp == 'server': if tp == 'server':
storage = get_command_storage() storage = get_command_storage()
......
...@@ -2,36 +2,33 @@ ...@@ -2,36 +2,33 @@
# #
from django import forms from django import forms
from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .models import Terminal from .models import Terminal
def get_all_command_storage(): def get_all_command_storage():
# storage_choices = [] from common import utils
from common.models import Setting command_storage = utils.get_command_storage_setting()
Setting.refresh_all_settings() for k, v in command_storage.items():
for k, v in settings.TERMINAL_COMMAND_STORAGE.items():
yield (k, k) yield (k, k)
def get_all_replay_storage(): def get_all_replay_storage():
# storage_choices = [] from common import utils
from common.models import Setting replay_storage = utils.get_replay_storage_setting()
Setting.refresh_all_settings() for k, v in replay_storage.items():
for k, v in settings.TERMINAL_REPLAY_STORAGE.items():
yield (k, k) yield (k, k)
class TerminalForm(forms.ModelForm): class TerminalForm(forms.ModelForm):
command_storage = forms.ChoiceField( command_storage = forms.ChoiceField(
choices=get_all_command_storage(), choices=get_all_command_storage,
label=_("Command storage"), label=_("Command storage"),
help_text=_("Command can store in server db or ES, default to server, more see docs"), help_text=_("Command can store in server db or ES, default to server, more see docs"),
) )
replay_storage = forms.ChoiceField( replay_storage = forms.ChoiceField(
choices=get_all_replay_storage(), choices=get_all_replay_storage,
label=_("Replay storage"), label=_("Replay storage"),
help_text=_("Replay file can store in server disk, AWS S3, Aliyun OSS, default to server, more see docs"), help_text=_("Replay file can store in server disk, AWS S3, Aliyun OSS, default to server, more see docs"),
) )
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-12-27 16:25
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('terminal', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='session',
name='terminal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='terminal.Terminal'),
),
migrations.AddField(
model_name='status',
name='terminal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='terminal.Terminal'),
),
migrations.AddField(
model_name='task',
name='terminal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='terminal.Terminal'),
),
migrations.AddField(
model_name='terminal',
name='user',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='terminal', to=settings.AUTH_USER_MODEL, verbose_name='Application User'),
),
migrations.AlterField(
model_name='terminal',
name='name',
field=models.CharField(max_length=32, verbose_name='Name'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-12-29 19:08
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0002_auto_20171228_0025'),
]
operations = [
migrations.AlterField(
model_name='command',
name='asset',
field=models.CharField(db_index=True, max_length=128, verbose_name='Asset'),
),
migrations.AlterField(
model_name='command',
name='system_user',
field=models.CharField(db_index=True, max_length=64, verbose_name='System user'),
),
migrations.AlterField(
model_name='command',
name='user',
field=models.CharField(db_index=True, max_length=64, verbose_name='User'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-05 10:07
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0003_auto_20171230_0308'),
]
operations = [
migrations.AddField(
model_name='session',
name='remote_addr',
field=models.CharField(blank=True, max_length=15, null=True, verbose_name='Remote addr'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-22 03:54
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0004_session_remote_addr'),
]
operations = [
migrations.AddField(
model_name='terminal',
name='command_storage',
field=models.CharField(choices=[('default', 'default'), ('elk', 'elk')], default='default', max_length=128, verbose_name='Command storage'),
),
migrations.AddField(
model_name='terminal',
name='replay_storage',
field=models.CharField(default='default', max_length=128, verbose_name='Replay storage'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-23 02:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0005_auto_20180122_1154'),
]
operations = [
migrations.AlterField(
model_name='terminal',
name='command_storage',
field=models.CharField(default='default', max_length=128, verbose_name='Command storage'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-02-27 09:13
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('terminal', '0006_auto_20180123_1037'),
]
operations = [
migrations.AddField(
model_name='session',
name='date_last_active',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date last active'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-07 08:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0007_session_date_last_active'),
]
operations = [
migrations.AlterField(
model_name='session',
name='date_start',
field=models.DateTimeField(db_index=True, verbose_name='Date start'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-26 01:57
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('terminal', '0008_auto_20180307_1603'),
]
operations = [
migrations.AlterField(
model_name='session',
name='terminal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='terminal.Terminal'),
),
migrations.AlterField(
model_name='task',
name='terminal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='terminal.Terminal'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-23 03:40
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('terminal', '0009_auto_20180326_0957'),
]
operations = [
migrations.AddField(
model_name='session',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=8),
),
migrations.AlterField(
model_name='session',
name='date_start',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date start'),
),
]
# Generated by Django 2.0.7 on 2018-08-07 03:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0010_auto_20180423_1140'),
]
operations = [
migrations.AddField(
model_name='command',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='session',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
]
# Generated by Django 2.0.7 on 2018-08-16 08:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0011_auto_20180807_1116'),
]
operations = [
migrations.AlterField(
model_name='command',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='session',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
]
# Generated by Django 2.1.1 on 2018-11-23 03:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0012_auto_20180816_1652'),
]
operations = [
migrations.AlterField(
model_name='terminal',
name='remote_addr',
field=models.CharField(blank=True, max_length=128, verbose_name='Remote Address'),
),
]
...@@ -9,14 +9,14 @@ from django.conf import settings ...@@ -9,14 +9,14 @@ from django.conf import settings
from users.models import User from users.models import User
from orgs.mixins import OrgModelMixin from orgs.mixins import OrgModelMixin
from common.models import common_settings from common.utils import get_command_storage_setting, get_replay_storage_setting
from .backends.command.models import AbstractSessionCommand from .backends.command.models import AbstractSessionCommand
class Terminal(models.Model): class Terminal(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=32, verbose_name=_('Name')) name = models.CharField(max_length=32, verbose_name=_('Name'))
remote_addr = models.CharField(max_length=128, verbose_name=_('Remote Address')) remote_addr = models.CharField(max_length=128, blank=True, verbose_name=_('Remote Address'))
ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222) ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222)
http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000) http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000)
command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default') command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default')
...@@ -40,7 +40,7 @@ class Terminal(models.Model): ...@@ -40,7 +40,7 @@ class Terminal(models.Model):
self.user.save() self.user.save()
def get_common_storage(self): def get_common_storage(self):
storage_all = settings.TERMINAL_COMMAND_STORAGE storage_all = get_command_storage_setting()
if self.command_storage in storage_all: if self.command_storage in storage_all:
storage = storage_all.get(self.command_storage) storage = storage_all.get(self.command_storage)
else: else:
...@@ -48,7 +48,7 @@ class Terminal(models.Model): ...@@ -48,7 +48,7 @@ class Terminal(models.Model):
return {"TERMINAL_COMMAND_STORAGE": storage} return {"TERMINAL_COMMAND_STORAGE": storage}
def get_replay_storage(self): def get_replay_storage(self):
storage_all = settings.TERMINAL_REPLAY_STORAGE storage_all = get_replay_storage_setting()
if self.replay_storage in storage_all: if self.replay_storage in storage_all:
storage = storage_all.get(self.replay_storage) storage = storage_all.get(self.replay_storage)
else: else:
...@@ -64,14 +64,19 @@ class Terminal(models.Model): ...@@ -64,14 +64,19 @@ class Terminal(models.Model):
configs.update(self.get_common_storage()) configs.update(self.get_common_storage())
configs.update(self.get_replay_storage()) configs.update(self.get_replay_storage())
configs.update({ configs.update({
'SECURITY_MAX_IDLE_TIME': common_settings.SECURITY_MAX_IDLE_TIME or 'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME
settings.DEFAULT_SECURITY_MAX_IDLE_TIME,
}) })
return configs return configs
@property
def service_account(self):
return self.user
def create_app_user(self): def create_app_user(self):
random = uuid.uuid4().hex[:6] random = uuid.uuid4().hex[:6]
user, access_key = User.create_app_user(name="{}-{}".format(self.name, random), comment=self.comment) user, access_key = User.create_app_user(
name="{}-{}".format(self.name, random), comment=self.comment
)
self.user = user self.user = user
self.save() self.save()
return user, access_key return user, access_key
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.core.cache import cache from django.core.cache import cache
from django.utils import timezone
from rest_framework import serializers from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin
from .models import Terminal, Status, Session, Task from ..models import Terminal, Status, Session, Task
from .backends import get_multi_command_storage from ..backends import get_multi_command_storage
class TerminalSerializer(serializers.ModelSerializer): class TerminalSerializer(serializers.ModelSerializer):
...@@ -33,6 +31,8 @@ class TerminalSerializer(serializers.ModelSerializer): ...@@ -33,6 +31,8 @@ class TerminalSerializer(serializers.ModelSerializer):
return cache.get(key) return cache.get(key)
class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer): class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer):
command_amount = serializers.SerializerMethodField() command_amount = serializers.SerializerMethodField()
command_store = get_multi_command_storage() command_store = get_multi_command_storage()
...@@ -71,3 +71,4 @@ class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer): ...@@ -71,3 +71,4 @@ class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class ReplaySerializer(serializers.Serializer): class ReplaySerializer(serializers.Serializer):
file = serializers.FileField() file = serializers.FileField()
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from common.utils import get_request_ip
from users.serializers.v2 import ServiceAccountRegistrationSerializer
from ..models import Terminal
__all__ = ['TerminalSerializer', 'TerminalRegistrationSerializer']
class TerminalSerializer(serializers.ModelSerializer):
class Meta:
model = Terminal
fields = [
'id', 'name', 'remote_addr', 'comment',
]
read_only_fields = ['id', 'remote_addr']
class TerminalRegistrationSerializer(serializers.ModelSerializer):
service_account = ServiceAccountRegistrationSerializer(read_only=True)
service_account_serializer = None
class Meta:
model = Terminal
fields = [
'id', 'name', 'remote_addr', 'comment', 'service_account'
]
read_only_fields = ['id', 'remote_addr', 'service_account']
def validate(self, attrs):
self.service_account_serializer = ServiceAccountRegistrationSerializer(data=attrs)
self.service_account_serializer.is_valid(raise_exception=True)
return attrs
def create(self, validated_data):
request = self.context.get('request')
sa = self.service_account_serializer.save()
instance = super().create(validated_data)
instance.is_accepted = True
instance.user = sa
instance.remote_addr = get_request_ip(request)
instance.save()
return instance
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.urls import path from django.urls import path, include
from rest_framework_bulk.routes import BulkRouter from rest_framework_bulk.routes import BulkRouter
from .. import api from ..api import v1 as api
app_name = 'terminal' app_name = 'terminal'
...@@ -20,7 +20,7 @@ router.register(r'status', api.StatusViewSet, 'status') ...@@ -20,7 +20,7 @@ router.register(r'status', api.StatusViewSet, 'status')
urlpatterns = [ urlpatterns = [
path('sessions/<uuid:pk>/replay/', path('sessions/<uuid:pk>/replay/',
api.SessionReplayV2ViewSet.as_view({'get': 'retrieve', 'post': 'create'}), api.SessionReplayViewSet.as_view({'get': 'retrieve', 'post': 'create'}),
name='session-replay'), name='session-replay'),
path('tasks/kill-session/', api.KillSessionAPI.as_view(), name='kill-session'), path('tasks/kill-session/', api.KillSessionAPI.as_view(), name='kill-session'),
path('terminal/<uuid:terminal>/access-key/', api.TerminalTokenApi.as_view(), path('terminal/<uuid:terminal>/access-key/', api.TerminalTokenApi.as_view(),
...@@ -33,3 +33,6 @@ urlpatterns = [ ...@@ -33,3 +33,6 @@ urlpatterns = [
] ]
urlpatterns += router.urls urlpatterns += router.urls
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
from django.urls import path
from rest_framework_bulk.routes import BulkRouter
from ..api import v2 as api
app_name = 'terminal'
router = BulkRouter()
router.register(r'terminal', api.TerminalViewSet, 'terminal')
router.register(r'terminal-registrations', api.TerminalRegistrationViewSet, 'terminal-registration')
urlpatterns = [
]
urlpatterns += router.urls
...@@ -17,8 +17,8 @@ from orgs.mixins import RootOrgViewMixin ...@@ -17,8 +17,8 @@ from orgs.mixins import RootOrgViewMixin
from ..serializers import UserSerializer from ..serializers import UserSerializer
from ..tasks import write_login_log_async from ..tasks import write_login_log_async
from ..models import User, LoginLog from ..models import User, LoginLog
from ..utils import check_user_valid, generate_token, \ from ..utils import check_user_valid, check_otp_code, \
check_otp_code, increase_login_failed_count, is_block_login, \ increase_login_failed_count, is_block_login, \
clean_failed_count clean_failed_count
from ..hands import Asset, SystemUser from ..hands import Asset, SystemUser
...@@ -43,16 +43,32 @@ class UserAuthApi(RootOrgViewMixin, APIView): ...@@ -43,16 +43,32 @@ class UserAuthApi(RootOrgViewMixin, APIView):
user, msg = self.check_user_valid(request) user, msg = self.check_user_valid(request)
if not user: if not user:
username = request.data.get('username', '')
exist = User.objects.filter(username=username).first()
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
data = { data = {
'username': request.data.get('username', ''), 'username': username,
'mfa': LoginLog.MFA_UNKNOWN, 'mfa': LoginLog.MFA_UNKNOWN,
'reason': LoginLog.REASON_PASSWORD, 'reason': reason,
'status': False 'status': False
} }
self.write_login_log(request, data) self.write_login_log(request, data)
increase_login_failed_count(username, ip) increase_login_failed_count(username, ip)
return Response({'msg': msg}, status=401) return Response({'msg': msg}, status=401)
if user.password_has_expired:
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_PASSWORD_EXPIRED,
'status': False
}
self.write_login_log(request, data)
msg = _("The user {} password has expired, please update.".format(
user.username))
logger.info(msg)
return Response({'msg': msg}, status=401)
if not user.otp_enabled: if not user.otp_enabled:
data = { data = {
'username': user.username, 'username': user.username,
...@@ -63,12 +79,9 @@ class UserAuthApi(RootOrgViewMixin, APIView): ...@@ -63,12 +79,9 @@ class UserAuthApi(RootOrgViewMixin, APIView):
self.write_login_log(request, data) self.write_login_log(request, data)
# 登陆成功,清除原来的缓存计数 # 登陆成功,清除原来的缓存计数
clean_failed_count(username, ip) clean_failed_count(username, ip)
token = generate_token(request, user) token = user.create_bearer_token(request)
return Response( return Response(
{ {'token': token, 'user': self.serializer_class(user).data}
'token': token,
'user': self.serializer_class(user).data
}
) )
seed = uuid.uuid4().hex seed = uuid.uuid4().hex
...@@ -110,7 +123,6 @@ class UserAuthApi(RootOrgViewMixin, APIView): ...@@ -110,7 +123,6 @@ class UserAuthApi(RootOrgViewMixin, APIView):
'user_agent': user_agent, 'user_agent': user_agent,
} }
data.update(tmp_data) data.update(tmp_data)
write_login_log_async.delay(**data) write_login_log_async.delay(**data)
...@@ -172,7 +184,7 @@ class UserToken(APIView): ...@@ -172,7 +184,7 @@ class UserToken(APIView):
user = request.user user = request.user
msg = None msg = None
if user: if user:
token = generate_token(request, user) token = user.create_bearer_token(request)
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200) return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
else: else:
return Response({'error': msg}, status=406) return Response({'error': msg}, status=406)
...@@ -210,7 +222,7 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView): ...@@ -210,7 +222,7 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView):
'status': True 'status': True
} }
self.write_login_log(request, data) self.write_login_log(request, data)
token = generate_token(request, user) token = user.create_bearer_token(request)
return Response( return Response(
{ {
'token': token, 'token': token,
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
from rest_framework import generics from rest_framework import generics
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from ..serializers import UserGroupSerializer, \ from ..serializers import UserGroupSerializer, \
UserGroupUpdateMemeberSerializer UserGroupUpdateMemeberSerializer
...@@ -15,9 +16,12 @@ __all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi'] ...@@ -15,9 +16,12 @@ __all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi']
class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet): class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
filter_fields = ("name",)
search_fields = filter_fields
queryset = UserGroup.objects.all() queryset = UserGroup.objects.all()
serializer_class = UserGroupSerializer serializer_class = UserGroupSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
pagination_class = LimitOffsetPagination
class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView): class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
......
...@@ -9,14 +9,16 @@ from rest_framework import generics ...@@ -9,14 +9,16 @@ from rest_framework import generics
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from common.permissions import IsOrgAdmin, IsCurrentUserOrReadOnly, \
IsOrgAdminOrAppUser
from common.mixins import IDInFilterMixin
from common.utils import get_logger
from orgs.utils import current_org
from ..serializers import UserSerializer, UserPKUpdateSerializer, \ from ..serializers import UserSerializer, UserPKUpdateSerializer, \
UserUpdateGroupSerializer, ChangeUserPasswordSerializer UserUpdateGroupSerializer, ChangeUserPasswordSerializer
from ..models import User from ..models import User
from orgs.utils import current_org
from common.permissions import IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser
from common.mixins import IDInFilterMixin
from common.utils import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
...@@ -28,10 +30,12 @@ __all__ = [ ...@@ -28,10 +30,12 @@ __all__ = [
class UserViewSet(IDInFilterMixin, BulkModelViewSet): class UserViewSet(IDInFilterMixin, BulkModelViewSet):
queryset = User.objects.exclude(role="App") filter_fields = ('username', 'email', 'name', 'id')
search_fields = filter_fields
queryset = User.objects.exclude(role=User.ROLE_APP)
serializer_class = UserSerializer serializer_class = UserSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
filter_fields = ('username', 'email', 'name', 'id') pagination_class = LimitOffsetPagination
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
......
# -*- coding: utf-8 -*-
#
from .user import *
# -*- coding: utf-8 -*-
#
from rest_framework import viewsets
from common.permissions import WithBootstrapToken
from ...serializers import v2 as serializers
class ServiceAccountRegistrationViewSet(viewsets.ModelViewSet):
serializer_class = serializers.ServiceAccountRegistrationSerializer
permission_classes = (WithBootstrapToken,)
http_method_names = ['post']
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import base64
import uuid import uuid
import hashlib
import time import time
from django.core.cache import cache from django.core.cache import cache
...@@ -12,11 +10,10 @@ from django.utils.translation import ugettext as _ ...@@ -12,11 +10,10 @@ from django.utils.translation import ugettext as _
from django.utils.six import text_type from django.utils.six import text_type
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import HTTP_HEADER_ENCODING from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import authentication, exceptions, permissions from rest_framework import authentication, exceptions
from rest_framework.authentication import CSRFCheck from rest_framework.authentication import CSRFCheck
from common.utils import get_object_or_none, make_signature, http_to_unixtime from common.utils import get_object_or_none, make_signature, http_to_unixtime
from .utils import refresh_token
from .models import User, AccessKey, PrivateToken from .models import User, AccessKey, PrivateToken
...@@ -144,7 +141,6 @@ class AccessTokenAuthentication(authentication.BaseAuthentication): ...@@ -144,7 +141,6 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
if not user: if not user:
msg = _('Invalid token or cache refreshed.') msg = _('Invalid token or cache refreshed.')
raise exceptions.AuthenticationFailed(msg) raise exceptions.AuthenticationFailed(msg)
refresh_token(token, user)
return user, None return user, None
......
...@@ -120,8 +120,7 @@ class UserCreateUpdateForm(OrgModelForm): ...@@ -120,8 +120,7 @@ class UserCreateUpdateForm(OrgModelForm):
public_key = self.cleaned_data.get('public_key') public_key = self.cleaned_data.get('public_key')
user = super().save(commit=commit) user = super().save(commit=commit)
if password: if password:
user.set_password(password) user.reset_password(password)
user.save()
if otp_level: if otp_level:
user.otp_level = otp_level user.otp_level = otp_level
user.save() user.save()
...@@ -217,8 +216,7 @@ class UserPasswordForm(forms.Form): ...@@ -217,8 +216,7 @@ class UserPasswordForm(forms.Form):
def save(self): def save(self):
password = self.cleaned_data['new_password'] password = self.cleaned_data['new_password']
self.instance.set_password(password) self.instance.reset_password(new_password=password)
self.instance.save()
return self.instance return self.instance
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-12-31 16:46
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_auto_20171225_1157'),
]
operations = [
migrations.AlterField(
model_name='user',
name='is_first_login',
field=models.BooleanField(default=True),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-25 04:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0003_auto_20180101_0046'),
]
operations = [
migrations.AlterField(
model_name='usergroup',
name='created_by',
field=models.CharField(blank=True, max_length=100, null=True),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-06 10:04
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0004_auto_20180125_1218'),
]
operations = [
migrations.AlterModelOptions(
name='user',
options={'ordering': ['username'], 'verbose_name': 'User'},
),
migrations.AlterModelOptions(
name='usergroup',
options={'ordering': ['name'], 'verbose_name': 'User group'},
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-11 03:35
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0005_auto_20180306_1804'),
]
operations = [
migrations.RenameField(
model_name='user',
old_name='secret_key_otp',
new_name='otp_secret_key',
),
migrations.RemoveField(
model_name='user',
name='enable_otp',
),
migrations.AddField(
model_name='user',
name='otp_level',
field=models.SmallIntegerField(choices=[(0, 'Disable'), (1, 'Enable'), (2, 'Force enable')], default=0, verbose_name='Enable OTP'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-19 02:36
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0006_auto_20180411_1135'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='otp_secret_key',
),
migrations.AddField(
model_name='user',
name='_otp_secret_key',
field=models.CharField(blank=True, max_length=128, null=True),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-25 07:16
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0007_auto_20180419_1036'),
]
operations = [
migrations.AlterField(
model_name='user',
name='otp_level',
field=models.SmallIntegerField(choices=[(0, 'Disable'), (1, 'Enable'), (2, 'Force enable')], default=0, verbose_name='MFA'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-05-17 07:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0008_auto_20180425_1516'),
]
operations = [
migrations.AlterField(
model_name='usergroup',
name='name',
field=models.CharField(max_length=128, unique=True, verbose_name='Name'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-06-06 07:05
from __future__ import unicode_literals
import common.utils
from django.db import migrations, models
def remove_deleted_group(apps, schema_editor):
db_alias = schema_editor.connection.alias
group_model = apps.get_model("users", "UserGroup")
group_model.objects.using(db_alias).filter(is_discard=True).delete()
class Migration(migrations.Migration):
dependencies = [
('users', '0009_auto_20180517_1537'),
]
operations = [
migrations.RunPython(
remove_deleted_group
),
migrations.RemoveField(
model_name='usergroup',
name='discard_time',
),
migrations.RemoveField(
model_name='usergroup',
name='is_discard',
),
migrations.AlterField(
model_name='user',
name='date_expired',
field=models.DateTimeField(blank=True, db_index=True, default=common.utils.date_expired_default, null=True, verbose_name='Date expired'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-06-07 04:58
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0010_auto_20180606_1505'),
]
operations = [
migrations.AddField(
model_name='user',
name='source',
field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD')], default='local', max_length=30, verbose_name='Source'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-07-10 08:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0011_user_source'),
]
operations = [
migrations.AddField(
model_name='loginlog',
name='mfa',
field=models.SmallIntegerField(choices=[(0, 'Disabled'), (1, 'Enabled'), (2, '-')], default=2, verbose_name='MFA'),
),
migrations.AddField(
model_name='loginlog',
name='reason',
field=models.SmallIntegerField(choices=[(0, '-'), (1, 'Username/password check failed'), (2, 'MFA authentication failed')], default=0, verbose_name='Reason'),
),
migrations.AddField(
model_name='loginlog',
name='status',
field=models.BooleanField(choices=[(True, 'Success'), (False, 'Failed')], default=True, max_length=2, verbose_name='Status'),
),
]
# Generated by Django 2.0.7 on 2018-08-07 03:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0012_auto_20180710_1641'),
]
operations = [
migrations.AddField(
model_name='usergroup',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='user',
name='last_name',
field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
),
migrations.AlterField(
model_name='usergroup',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='usergroup',
unique_together={('org_id', 'name')},
),
]
# Generated by Django 2.0.7 on 2018-08-16 08:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0013_auto_20180807_1116'),
]
operations = [
migrations.AlterField(
model_name='usergroup',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
),
]
# Generated by Django 2.1.1 on 2018-11-05 03:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0014_auto_20180816_1652'),
]
operations = [
migrations.AlterField(
model_name='loginlog',
name='reason',
field=models.SmallIntegerField(choices=[(0, '-'), (1, 'Username/password check failed'), (2, 'MFA authentication failed'), (3, 'Username does not exist')], default=0, verbose_name='Reason'),
),
migrations.AlterField(
model_name='loginlog',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
]
# Generated by Django 2.1.1 on 2018-11-09 07:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0015_auto_20181105_1112'),
]
operations = [
migrations.AlterField(
model_name='user',
name='source',
field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID')], default='local', max_length=30, verbose_name='Source'),
),
]
# Generated by Django 2.1.1 on 2018-11-23 03:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0016_auto_20181109_1505'),
]
operations = [
migrations.AddField(
model_name='user',
name='date_password_last_updated',
field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date password last updated'),
),
migrations.AlterField(
model_name='accesskey',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_keys', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.AlterField(
model_name='loginlog',
name='reason',
field=models.SmallIntegerField(choices=[(0, '-'), (1, 'Username/password check failed'), (2, 'MFA authentication failed'), (3, 'Username does not exist'), (4, 'Password expired')], default=0, verbose_name='Reason'),
),
]
...@@ -17,7 +17,7 @@ class AccessKey(models.Model): ...@@ -17,7 +17,7 @@ class AccessKey(models.Model):
secret = models.UUIDField(verbose_name='AccessKeySecret', secret = models.UUIDField(verbose_name='AccessKeySecret',
default=uuid.uuid4, editable=False) default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, verbose_name='User', user = models.ForeignKey(User, verbose_name='User',
on_delete=models.CASCADE, related_name='access_key') on_delete=models.CASCADE, related_name='access_keys')
def get_id(self): def get_id(self):
return str(self.id) return str(self.id)
...@@ -25,6 +25,9 @@ class AccessKey(models.Model): ...@@ -25,6 +25,9 @@ class AccessKey(models.Model):
def get_secret(self): def get_secret(self):
return str(self.secret) return str(self.secret)
def get_full_value(self):
return '{}:{}'.format(self.id, self.secret)
def __str__(self): def __str__(self):
return str(self.id) return str(self.id)
...@@ -55,11 +58,15 @@ class LoginLog(models.Model): ...@@ -55,11 +58,15 @@ class LoginLog(models.Model):
REASON_NOTHING = 0 REASON_NOTHING = 0
REASON_PASSWORD = 1 REASON_PASSWORD = 1
REASON_MFA = 2 REASON_MFA = 2
REASON_NOT_EXIST = 3
REASON_PASSWORD_EXPIRED = 4
REASON_CHOICE = ( REASON_CHOICE = (
(REASON_NOTHING, _('-')), (REASON_NOTHING, _('-')),
(REASON_PASSWORD, _('Username/password check failed')), (REASON_PASSWORD, _('Username/password check failed')),
(REASON_MFA, _('MFA authentication failed')), (REASON_MFA, _('MFA authentication failed')),
(REASON_NOT_EXIST, _("Username does not exist")),
(REASON_PASSWORD_EXPIRED, _("Password expired")),
) )
STATUS_CHOICE = ( STATUS_CHOICE = (
...@@ -67,7 +74,7 @@ class LoginLog(models.Model): ...@@ -67,7 +74,7 @@ class LoginLog(models.Model):
(False, _('Failed')) (False, _('Failed'))
) )
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
username = models.CharField(max_length=20, verbose_name=_('Username')) username = models.CharField(max_length=128, verbose_name=_('Username'))
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type')) type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
ip = models.GenericIPAddressField(verbose_name=_('Login ip')) ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city')) city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
......
...@@ -2,20 +2,20 @@ ...@@ -2,20 +2,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import uuid import uuid
import base64
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractUser, UserManager from django.contrib.auth.models import AbstractUser
from django.core import signing from django.core import signing
from django.core.cache import cache
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.shortcuts import reverse from django.shortcuts import reverse
from common.utils import get_signer, date_expired_default from common.utils import get_signer, date_expired_default
from common.models import common_settings
from orgs.mixins import OrgManager
from orgs.utils import current_org from orgs.utils import current_org
...@@ -40,9 +40,11 @@ class User(AbstractUser): ...@@ -40,9 +40,11 @@ class User(AbstractUser):
) )
SOURCE_LOCAL = 'local' SOURCE_LOCAL = 'local'
SOURCE_LDAP = 'ldap' SOURCE_LDAP = 'ldap'
SOURCE_OPENID = 'openid'
SOURCE_CHOICES = ( SOURCE_CHOICES = (
(SOURCE_LOCAL, 'Local'), (SOURCE_LOCAL, 'Local'),
(SOURCE_LDAP, 'LDAP/AD'), (SOURCE_LDAP, 'LDAP/AD'),
(SOURCE_OPENID, 'OpenID'),
) )
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
username = models.CharField( username = models.CharField(
...@@ -95,6 +97,10 @@ class User(AbstractUser): ...@@ -95,6 +97,10 @@ class User(AbstractUser):
max_length=30, default=SOURCE_LOCAL, choices=SOURCE_CHOICES, max_length=30, default=SOURCE_LOCAL, choices=SOURCE_CHOICES,
verbose_name=_('Source') verbose_name=_('Source')
) )
date_password_last_updated = models.DateTimeField(
auto_now_add=True, blank=True, null=True,
verbose_name=_('Date password last updated')
)
def __str__(self): def __str__(self):
return '{0.name}({0.username})'.format(self) return '{0.name}({0.username})'.format(self)
...@@ -219,6 +225,34 @@ class User(AbstractUser): ...@@ -219,6 +225,34 @@ class User(AbstractUser):
def is_staff(self, value): def is_staff(self, value):
pass pass
@property
def is_local(self):
return self.source == self.SOURCE_LOCAL
@property
def date_password_expired(self):
interval = settings.SECURITY_PASSWORD_EXPIRATION_TIME
date_expired = self.date_password_last_updated + timezone.timedelta(
days=int(interval))
return date_expired
@property
def password_expired_remain_days(self):
date_remain = self.date_password_expired - timezone.now()
return date_remain.days
@property
def password_has_expired(self):
if self.is_local and self.password_expired_remain_days < 0:
return True
return False
@property
def password_will_expired(self):
if self.is_local and self.password_expired_remain_days < 5:
return True
return False
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.name: if not self.name:
self.name = self.username self.name = self.username
...@@ -241,15 +275,38 @@ class User(AbstractUser): ...@@ -241,15 +275,38 @@ class User(AbstractUser):
token = PrivateToken.objects.create(user=self) token = PrivateToken.objects.create(user=self)
return token.key return token.key
def refresh_private_token(self):
from .authentication import PrivateToken
PrivateToken.objects.filter(user=self).delete()
return PrivateToken.objects.create(user=self)
def create_bearer_token(self, request=None):
expiration = settings.TOKEN_EXPIRATION or 3600
if request:
remote_addr = request.META.get('REMOTE_ADDR', '')
else:
remote_addr = '0.0.0.0'
if not isinstance(remote_addr, bytes):
remote_addr = remote_addr.encode("utf-8")
remote_addr = base64.b16encode(remote_addr) # .replace(b'=', '')
token = cache.get('%s_%s' % (self.id, remote_addr))
if not token:
token = uuid.uuid4().hex
cache.set(token, self.id, expiration)
cache.set('%s_%s' % (self.id, remote_addr), token, expiration)
return token
def refresh_bearer_token(self, token):
pass
def create_access_key(self): def create_access_key(self):
from . import AccessKey from . import AccessKey
access_key = AccessKey.objects.create(user=self) access_key = AccessKey.objects.create(user=self)
return access_key return access_key
def refresh_private_token(self): @property
from .authentication import PrivateToken def access_key(self):
PrivateToken.objects.filter(user=self).delete() return self.access_keys.first()
return PrivateToken.objects.create(user=self)
def is_member_of(self, user_group): def is_member_of(self, user_group):
if user_group in self.groups.all(): if user_group in self.groups.all():
...@@ -257,7 +314,7 @@ class User(AbstractUser): ...@@ -257,7 +314,7 @@ class User(AbstractUser):
return False return False
def check_public_key(self, public_key): def check_public_key(self, public_key):
if self.ssH_public_key == public_key: if self.ssh_public_key == public_key:
return True return True
return False return False
...@@ -282,7 +339,7 @@ class User(AbstractUser): ...@@ -282,7 +339,7 @@ class User(AbstractUser):
@property @property
def otp_force_enabled(self): def otp_force_enabled(self):
if common_settings.SECURITY_MFA_AUTH: if settings.SECURITY_MFA_AUTH:
return True return True
return self.otp_level == 2 return self.otp_level == 2
...@@ -312,7 +369,8 @@ class User(AbstractUser): ...@@ -312,7 +369,8 @@ class User(AbstractUser):
'phone': self.phone, 'phone': self.phone,
'otp_level': self.otp_level, 'otp_level': self.otp_level,
'comment': self.comment, 'comment': self.comment,
'date_expired': self.date_expired.strftime('%Y-%m-%d %H:%M:%S') if self.date_expired is not None else None 'date_expired': self.date_expired.strftime('%Y-%m-%d %H:%M:%S') \
if self.date_expired is not None else None
}) })
@classmethod @classmethod
...@@ -339,6 +397,7 @@ class User(AbstractUser): ...@@ -339,6 +397,7 @@ class User(AbstractUser):
def reset_password(self, new_password): def reset_password(self, new_password):
self.set_password(new_password) self.set_password(new_password)
self.date_password_last_updated = timezone.now()
self.save() self.save()
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
......
# -*- coding: utf-8 -*-
#
from .v1 import *
\ No newline at end of file
...@@ -7,14 +7,16 @@ from rest_framework_bulk import BulkListSerializer ...@@ -7,14 +7,16 @@ from rest_framework_bulk import BulkListSerializer
from common.utils import get_signer, validate_ssh_public_key from common.utils import get_signer, validate_ssh_public_key
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin
from .models import User, UserGroup from ..models import User, UserGroup
signer = get_signer() signer = get_signer()
class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
groups_display = serializers.SerializerMethodField() groups_display = serializers.SerializerMethodField()
groups = serializers.PrimaryKeyRelatedField(many=True, queryset = UserGroup.objects.all(), required=False) groups = serializers.PrimaryKeyRelatedField(
many=True, queryset=UserGroup.objects.all(), required=False
)
class Meta: class Meta:
model = User model = User
......
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from ..models import User, AccessKey
class AccessKeySerializer(serializers.ModelSerializer):
class Meta:
model = AccessKey
fields = ['id', 'secret']
read_only_fields = ['id', 'secret']
class ServiceAccountRegistrationSerializer(serializers.ModelSerializer):
access_key = AccessKeySerializer(read_only=True)
class Meta:
model = User
fields = ['id', 'name', 'access_key']
read_only_fields = ['id', 'access_key']
def get_username(self):
return self.initial_data.get('name')
def get_email(self):
name = self.initial_data.get('name')
return '{}@serviceaccount.local'.format(name)
def validate_name(self, name):
email = self.get_email()
username = self.get_username()
if User.objects.filter(email=email) or \
User.objects.filter(username=username):
raise serializers.ValidationError('name not unique', code='unique')
return name
def create(self, validated_data):
validated_data['email'] = self.get_email()
validated_data['username'] = self.get_username()
validated_data['role'] = User.ROLE_APP
instance = super().create(validated_data)
instance.create_access_key()
return instance
...@@ -36,4 +36,3 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs): ...@@ -36,4 +36,3 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs):
if user: if user:
user.source = user.SOURCE_LDAP user.source = user.SOURCE_LDAP
user.save() user.save()
...@@ -2,10 +2,46 @@ ...@@ -2,10 +2,46 @@
# #
from celery import shared_task from celery import shared_task
from .utils import write_login_log
from ops.celery.utils import (
create_or_update_celery_periodic_tasks,
after_app_ready_start
)
from .models import User
from common.utils import get_logger
from .utils import write_login_log, send_password_expiration_reminder_mail
logger = get_logger(__file__)
@shared_task @shared_task
def write_login_log_async(*args, **kwargs): def write_login_log_async(*args, **kwargs):
write_login_log(*args, **kwargs) write_login_log(*args, **kwargs)
@shared_task
def check_password_expired():
users = User.objects.exclude(role=User.ROLE_APP)
for user in users:
if not user.password_will_expired:
continue
send_password_expiration_reminder_mail(user)
logger.info("The user {} password expires in {} days".format(
user, user.password_expired_remain_days)
)
@shared_task
@after_app_ready_start
def check_password_expired_periodic():
tasks = {
'check_password_expired_periodic': {
'task': check_password_expired.name,
'interval': None,
'crontab': '0 10 * * *',
'enabled': True,
}
}
create_or_update_celery_periodic_tasks(tasks)
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group date"> <div class="input-group date">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span> <span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input id="{{ form.date_expired.id_for_label }}" name="{{ form.date_expired.html_name }}" type="text" class="form-control" value="{{ form.date_expired.value|date:'Y-m-d' }}"> <input id="{{ form.date_expired.id_for_label }}" name="{{ form.date_expired.html_name }}" type="text" class="form-control" value="{{ form.date_expired.value|date:'Y-m-d H:i' }}">
</div> </div>
<span class="help-block ">{{ form.date_expired.errors }}</span> <span class="help-block ">{{ form.date_expired.errors }}</span>
</div> </div>
...@@ -52,18 +52,24 @@ ...@@ -52,18 +52,24 @@
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script> <script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.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" %} />
<script> <script>
var dateOptions = {
singleDatePicker: true,
showDropdowns: true,
timePicker: true,
timePicker24Hour: true,
autoApply: true,
locale: {
format: 'YYYY-MM-DD HH:mm'
}
};
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2(); $('.select2').select2();
$('#id_date_expired').daterangepicker(dateOptions);
$('.input-group.date').datepicker({
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
});
}) })
</script> </script>
{% endblock %} {% endblock %}
...@@ -50,6 +50,8 @@ ...@@ -50,6 +50,8 @@
{% if block_login %} {% if block_login %}
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p> <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 %} {% elif form.errors %}
{% if 'captcha' in form.errors %} {% if 'captcha' in form.errors %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p> <p class="red-fonts">{% trans 'Captcha invalid' %}</p>
...@@ -75,9 +77,23 @@ ...@@ -75,9 +77,23 @@
</p> </p>
{% endif %} {% endif %}
<a href="{% url 'users:forgot-password' %}"> <div class="text-muted text-center">
<small>{% trans 'Forgot password' %}?</small> <div>
</a> <a href="{% url 'users:forgot-password' %}">
<small>{% trans 'Forgot password' %}?</small>
</a>
</div>
{% if AUTH_OPENID %}
<div class="hr-line-dashed"></div>
<p class="text-muted text-center">{% trans "More login options" %}</p>
<div>
<button type="button" class="btn btn-default btn-sm btn-block" onclick="location.href='{% url 'authentication:openid-login' %}'">
<i class="fa fa-openid"></i>
{% trans 'Keycloak' %}
</button>
</div>
{% endif %}
</form> </form>
<p class="m-t"> <p class="m-t">
......
...@@ -99,7 +99,7 @@ ...@@ -99,7 +99,7 @@
container = $('#container'), container = $('#container'),
progress = $('#id_progress'), progress = $('#id_progress'),
password_check_rules = {{ password_check_rules|safe }}, password_check_rules = {{ password_check_rules|safe }},
minLength = {{ min_length }}, minLength = 6,
top = 146, left = 170, top = 146, left = 170,
i18n_fallback = { i18n_fallback = {
"veryWeak": "{% trans 'Very weak' %}", "veryWeak": "{% trans 'Very weak' %}",
...@@ -110,6 +110,12 @@ ...@@ -110,6 +110,12 @@
"veryStrong": "{% trans 'Very strong' %}" "veryStrong": "{% trans 'Very strong' %}"
}; };
jQuery.each(password_check_rules, function (idx, rules) {
if(rules.key === 'id_security_password_min_length'){
minLength = rules.value
}
});
// 初始化popover // 初始化popover
initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback); initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback);
......
...@@ -119,6 +119,10 @@ ...@@ -119,6 +119,10 @@
<td>{% trans 'Last login' %}:</td> <td>{% trans 'Last login' %}:</td>
<td><b>{{ user_object.last_login|date:"Y-m-j H:i:s" }}</b></td> <td><b>{{ user_object.last_login|date:"Y-m-j H:i:s" }}</b></td>
</tr> </tr>
<tr>
<td>{% trans 'Last password updated' %}:</td>
<td><b>{{ user_object.date_password_last_updated|date:"Y-m-j H:i:s" }}</b></td>
</tr>
<tr> <tr>
<td>{% trans 'Comment' %}:</td> <td>{% trans 'Comment' %}:</td>
<td><b>{{ user_object.comment }}</b></td> <td><b>{{ user_object.comment }}</b></td>
......
...@@ -103,7 +103,7 @@ function initTable() { ...@@ -103,7 +103,7 @@ function initTable() {
{data: "system_users_granted", orderable: false} {data: "system_users_granted", orderable: false}
] ]
}; };
asset_table = jumpserver.initDataTable(options); asset_table = jumpserver.initServerSideDataTable(options)
} }
function onSelected(event, treeNode) { function onSelected(event, treeNode) {
......
...@@ -58,7 +58,8 @@ $(document).ready(function() { ...@@ -58,7 +58,8 @@ $(document).ready(function() {
order: [], order: [],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
jumpserver.initDataTable(options); jumpserver.initServerSideDataTable(options);
}).on('click', '.btn_delete_user_group', function(){ }).on('click', '.btn_delete_user_group', function(){
var $this = $(this); var $this = $(this);
var group_id = $this.data('gid'); var group_id = $this.data('gid');
......
...@@ -90,12 +90,16 @@ function initTable() { ...@@ -90,12 +90,16 @@ function initTable() {
}}], }}],
ajax_url: '{% url "api-users:user-list" %}', ajax_url: '{% url "api-users:user-list" %}',
columns: [ columns: [
{data: "id"}, {data: "name" }, {data: "username" }, {data: "get_role_display" }, {data: "id"}, {data: "name" }, {data: "username" },
{data: "groups_display" }, {data: "get_source_display" }, {data: "is_valid" }, {data: "id" } {data: "get_role_display", orderable: false},
{data: "groups_display", orderable: false},
{data: "get_source_display", orderable: false},
{data: "is_valid", orderable: false},
{data: "id", orderable: false}
], ],
op_html: $('#actions').html() op_html: $('#actions').html()
}; };
table = jumpserver.initDataTable(options); var table = jumpserver.initServerSideDataTable(options);
return table return table
} }
......
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
container = $('#container'), container = $('#container'),
progress = $('#id_progress'), progress = $('#id_progress'),
password_check_rules = {{ password_check_rules|safe }}, password_check_rules = {{ password_check_rules|safe }},
minLength = {{ min_length }}, minLength = 6,
top = idPassword.offset().top - $('.navbar').outerHeight(true) - $('.page-heading').outerHeight(true) - 10 + 34, top = idPassword.offset().top - $('.navbar').outerHeight(true) - $('.page-heading').outerHeight(true) - 10 + 34,
left = 377, left = 377,
i18n_fallback = { i18n_fallback = {
...@@ -104,6 +104,12 @@ ...@@ -104,6 +104,12 @@
"veryStrong": "{% trans 'Very strong' %}" "veryStrong": "{% trans 'Very strong' %}"
}; };
jQuery.each(password_check_rules, function (idx, rules) {
if(rules.key === 'id_security_password_min_length'){
minLength = rules.value
}
});
// 初始化popover // 初始化popover
initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback); initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback);
......
...@@ -108,6 +108,10 @@ ...@@ -108,6 +108,10 @@
<td class="text-navy">{% trans 'Last login' %}</td> <td class="text-navy">{% trans 'Last login' %}</td>
<td>{{ user.last_login|date:"Y-m-d H:i:s" }}</td> <td>{{ user.last_login|date:"Y-m-d H:i:s" }}</td>
</tr> </tr>
<tr>
<td class="text-navy">{% trans 'Last password updated' %}</td>
<td>{{ user.date_password_last_updated|date:"Y-m-d H:i:s" }}</td>
</tr>
<tr> <tr>
<td class="text-navy">{% trans 'Date expired' %}</td> <td class="text-navy">{% trans 'Date expired' %}</td>
<td>{{ user.date_expired|date:"Y-m-d H:i:s" }}</td> <td>{{ user.date_expired|date:"Y-m-d H:i:s" }}</td>
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
container = $('#container'), container = $('#container'),
progress = $('#id_progress'), progress = $('#id_progress'),
password_check_rules = {{ password_check_rules|safe }}, password_check_rules = {{ password_check_rules|safe }},
minLength = {{ min_length }}, minLength = 6,
top = idPassword.offset().top - $('.navbar').outerHeight(true) - $('.page-heading').outerHeight(true) - 10 + 34, top = idPassword.offset().top - $('.navbar').outerHeight(true) - $('.page-heading').outerHeight(true) - 10 + 34,
left = 377, left = 377,
i18n_fallback = { i18n_fallback = {
...@@ -39,6 +39,12 @@ ...@@ -39,6 +39,12 @@
"veryStrong": "{% trans 'Very strong' %}" "veryStrong": "{% trans 'Very strong' %}"
}; };
jQuery.each(password_check_rules, function (idx, rules) {
if(rules.key === 'id_security_password_min_length'){
minLength = rules.value
}
});
// 初始化popover // 初始化popover
initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback); initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback);
......
...@@ -15,8 +15,7 @@ router.register(r'groups', api.UserGroupViewSet, 'user-group') ...@@ -15,8 +15,7 @@ router.register(r'groups', api.UserGroupViewSet, 'user-group')
urlpatterns = [ urlpatterns = [
# path(r'', api.UserListView.as_view()), # path('token/', api.UserToken.as_view(), name='user-token'),
path('token/', api.UserToken.as_view(), name='user-token'),
path('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'), path('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('profile/', api.UserProfileApi.as_view(), name='user-profile'), path('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'), path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
...@@ -31,5 +30,6 @@ urlpatterns = [ ...@@ -31,5 +30,6 @@ urlpatterns = [
path('users/<uuid:pk>/groups/', api.UserUpdateGroupApi.as_view(), name='user-update-group'), path('users/<uuid:pk>/groups/', api.UserUpdateGroupApi.as_view(), name='user-update-group'),
path('groups/<uuid:pk>/users/', api.UserGroupUpdateUserApi.as_view(), name='user-group-update-user'), path('groups/<uuid:pk>/users/', api.UserGroupUpdateUserApi.as_view(), name='user-group-update-user'),
] ]
urlpatterns += router.urls urlpatterns += router.urls
#!/usr/bin/env python
# ~*~ coding: utf-8 ~*~
#
from __future__ import absolute_import
from django.urls import path, include
from rest_framework_bulk.routes import BulkRouter
from ..api import v2 as api
app_name = 'users'
router = BulkRouter()
router.register(r'service-account-registrations',
api.ServiceAccountRegistrationViewSet,
'service-account-registration')
urlpatterns = [
# path('token/', api.UserToken.as_view(), name='user-token'),
]
urlpatterns += router.urls
...@@ -16,11 +16,12 @@ from django.contrib.auth.mixins import UserPassesTestMixin ...@@ -16,11 +16,12 @@ from django.contrib.auth.mixins import UserPassesTestMixin
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.cache import cache from django.core.cache import cache
from datetime import datetime
from common.tasks import send_mail_async from common.tasks import send_mail_async
from common.utils import reverse, get_object_or_none from common.utils import reverse, get_object_or_none
from common.models import common_settings, Setting
from common.forms import SecuritySettingForm from common.forms import SecuritySettingForm
from common.models import Setting
from .models import User, LoginLog from .models import User, LoginLog
...@@ -109,6 +110,44 @@ def send_reset_password_mail(user): ...@@ -109,6 +110,44 @@ def send_reset_password_mail(user):
send_mail_async.delay(subject, message, recipient_list, html_message=message) send_mail_async.delay(subject, message, recipient_list, html_message=message)
def send_password_expiration_reminder_mail(user):
subject = _('Security notice')
recipient_list = [user.email]
message = _("""
Hello %(name)s:
</br>
Your password will expire in %(date_password_expired)s,
</br>
For your account security, please click on the link below to update your password in time
</br>
<a href="%(update_password_url)s">Click here update password</a>
</br>
If your password has expired, please click
<a href="%(forget_password_url)s?email=%(email)s">Password expired</a>
to apply for a password reset email.
</br>
---
</br>
<a href="%(login_url)s">Login direct</a>
</br>
""") % {
'name': user.name,
'date_password_expired': datetime.fromtimestamp(datetime.timestamp(
user.date_password_expired)).strftime('%Y-%m-%d %H:%M'),
'update_password_url': reverse('users:user-password-update', external=True),
'forget_password_url': reverse('users:forgot-password', external=True),
'email': user.email,
'login_url': reverse('users:login', external=True),
}
if settings.DEBUG:
logger.debug(message)
send_mail_async.delay(subject, message, recipient_list, html_message=message)
def send_reset_ssh_key_mail(user): def send_reset_ssh_key_mail(user):
subject = _('SSH Key Reset') subject = _('SSH Key Reset')
recipient_list = [user.email] recipient_list = [user.email]
...@@ -163,24 +202,6 @@ def check_user_valid(**kwargs): ...@@ -163,24 +202,6 @@ def check_user_valid(**kwargs):
return None, _('Password or SSH public key invalid') return None, _('Password or SSH public key invalid')
def refresh_token(token, user, expiration=settings.TOKEN_EXPIRATION or 3600):
cache.set(token, user.id, expiration)
def generate_token(request, user):
expiration = settings.TOKEN_EXPIRATION or 3600
remote_addr = request.META.get('REMOTE_ADDR', '')
if not isinstance(remote_addr, bytes):
remote_addr = remote_addr.encode("utf-8")
remote_addr = base64.b16encode(remote_addr) # .replace(b'=', '')
token = cache.get('%s_%s' % (user.id, remote_addr))
if not token:
token = uuid.uuid4().hex
cache.set(token, user.id, expiration)
cache.set('%s_%s' % (user.id, remote_addr), token, expiration)
return token
def validate_ip(ip): def validate_ip(ip):
try: try:
ipaddress.ip_address(ip) ipaddress.ip_address(ip)
...@@ -275,57 +296,27 @@ def check_otp_code(otp_secret_key, otp_code): ...@@ -275,57 +296,27 @@ def check_otp_code(otp_secret_key, otp_code):
def get_password_check_rules(): def get_password_check_rules():
check_rules = [] check_rules = []
min_length = settings.DEFAULT_PASSWORD_MIN_LENGTH for rule in settings.SECURITY_PASSWORD_RULES:
min_name = 'SECURITY_PASSWORD_MIN_LENGTH' key = "id_{}".format(rule.lower())
base_filed = SecuritySettingForm.base_fields value = getattr(settings, rule)
password_setting = Setting.objects.filter(name__startswith='SECURITY_PASSWORD') if not value:
continue
if not password_setting: check_rules.append({'key': key, 'value': int(value)})
# 用户还没有设置过密码校验规则 return check_rules
label = base_filed.get(min_name).label
label += ' ' + str(min_length) + _('Bit')
id = 'rule_' + min_name
rules = {'id': id, 'label': label}
check_rules.append(rules)
for setting in password_setting:
if setting.cleaned_value:
id = 'rule_' + setting.name
label = base_filed.get(setting.name).label
if setting.name == min_name:
label += str(setting.cleaned_value) + _('Bit')
min_length = setting.cleaned_value
rules = {'id': id, 'label': label}
check_rules.append(rules)
return check_rules, min_length
def check_password_rules(password): def check_password_rules(password):
min_field_name = 'SECURITY_PASSWORD_MIN_LENGTH' pattern = r"^"
upper_field_name = 'SECURITY_PASSWORD_UPPER_CASE' if settings.SECURITY_PASSWORD_UPPER_CASE:
lower_field_name = 'SECURITY_PASSWORD_LOWER_CASE' pattern += '(?=.*[A-Z])'
number_field_name = 'SECURITY_PASSWORD_NUMBER' if settings.SECURITY_PASSWORD_LOWER_CASE:
special_field_name = 'SECURITY_PASSWORD_SPECIAL_CHAR' pattern += '(?=.*[a-z])'
min_length = getattr(common_settings, min_field_name) or \ if settings.SECURITY_PASSWORD_NUMBER:
settings.DEFAULT_PASSWORD_MIN_LENGTH pattern += '(?=.*\d)'
if settings.SECURITY_PASSWORD_SPECIAL_CHAR:
password_setting = Setting.objects.filter(name__startswith='SECURITY_PASSWORD') pattern += '(?=.*[`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?])'
if not password_setting: pattern += '[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?]'
pattern = r"^.{" + str(min_length) + ",}$" pattern += '.{' + str(settings.SECURITY_PASSWORD_MIN_LENGTH-1) + ',}$'
else:
pattern = r"^"
for setting in password_setting:
if setting.cleaned_value and setting.name == upper_field_name:
pattern += '(?=.*[A-Z])'
elif setting.cleaned_value and setting.name == lower_field_name:
pattern += '(?=.*[a-z])'
elif setting.cleaned_value and setting.name == number_field_name:
pattern += '(?=.*\d)'
elif setting.cleaned_value and setting.name == special_field_name:
pattern += '(?=.*[`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'",\.<>\/\?])'
pattern += '[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'",\.<>\/\?]'
match_obj = re.match(pattern, password) match_obj = re.match(pattern, password)
return bool(match_obj) return bool(match_obj)
...@@ -340,8 +331,7 @@ def increase_login_failed_count(username, ip): ...@@ -340,8 +331,7 @@ def increase_login_failed_count(username, ip):
count = cache.get(key_limit) count = cache.get(key_limit)
count = count + 1 if count else 1 count = count + 1 if count else 1
limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME or \ limit_time = settings.SECURITY_LOGIN_LIMIT_TIME
settings.DEFAULT_LOGIN_LIMIT_TIME
cache.set(key_limit, count, int(limit_time)*60) cache.set(key_limit, count, int(limit_time)*60)
...@@ -357,10 +347,8 @@ def is_block_login(username, ip): ...@@ -357,10 +347,8 @@ def is_block_login(username, ip):
key_block = key_prefix_block.format(username) key_block = key_prefix_block.format(username)
count = cache.get(key_limit, 0) count = cache.get(key_limit, 0)
limit_count = common_settings.SECURITY_LOGIN_LIMIT_COUNT or \ limit_count = settings.SECURITY_LOGIN_LIMIT_COUNT
settings.DEFAULT_LOGIN_LIMIT_COUNT limit_time = settings.SECURITY_LOGIN_LIMIT_TIME
limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME or \
settings.DEFAULT_LOGIN_LIMIT_TIME
if count >= limit_count: if count >= limit_count:
cache.set(key_block, 1, int(limit_time)*60) cache.set(key_block, 1, int(limit_time)*60)
......
...@@ -17,8 +17,8 @@ from django.views.decorators.csrf import csrf_protect ...@@ -17,8 +17,8 @@ from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from formtools.wizard.views import SessionWizardView
from django.conf import settings from django.conf import settings
from formtools.wizard.views import SessionWizardView
from common.utils import get_object_or_none, get_request_ip from common.utils import get_object_or_none, get_request_ip
from ..models import User, LoginLog from ..models import User, LoginLog
...@@ -68,7 +68,20 @@ class UserLoginView(FormView): ...@@ -68,7 +68,20 @@ class UserLoginView(FormView):
if not self.request.session.test_cookie_worked(): if not self.request.session.test_cookie_worked():
return HttpResponse(_("Please enable cookies and try again.")) return HttpResponse(_("Please enable cookies and try again."))
set_tmp_user_to_cache(self.request, form.get_user()) user = form.get_user()
# user password expired
if user.password_has_expired:
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_PASSWORD_EXPIRED,
'status': False
}
self.write_login_log(data)
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') username = form.cleaned_data.get('username')
ip = get_request_ip(self.request) ip = get_request_ip(self.request)
# 登陆成功,清除缓存计数 # 登陆成功,清除缓存计数
...@@ -78,10 +91,12 @@ class UserLoginView(FormView): ...@@ -78,10 +91,12 @@ class UserLoginView(FormView):
def form_invalid(self, form): def form_invalid(self, form):
# write login failed log # write login failed log
username = form.cleaned_data.get('username') username = form.cleaned_data.get('username')
exist = User.objects.filter(username=username).first()
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
data = { data = {
'username': username, 'username': username,
'mfa': LoginLog.MFA_UNKNOWN, 'mfa': LoginLog.MFA_UNKNOWN,
'reason': LoginLog.REASON_PASSWORD, 'reason': reason,
'status': False 'status': False
} }
self.write_login_log(data) self.write_login_log(data)
...@@ -128,6 +143,7 @@ class UserLoginView(FormView): ...@@ -128,6 +143,7 @@ class UserLoginView(FormView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = { context = {
'demo_mode': os.environ.get("DEMO_MODE"), 'demo_mode': os.environ.get("DEMO_MODE"),
'AUTH_OPENID': settings.AUTH_OPENID,
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
...@@ -196,6 +212,9 @@ class UserLogoutView(TemplateView): ...@@ -196,6 +212,9 @@ class UserLogoutView(TemplateView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
auth_logout(request) auth_logout(request)
next_uri = request.COOKIES.get("next")
if next_uri:
return redirect(next_uri)
response = super().get(request, *args, **kwargs) response = super().get(request, *args, **kwargs)
return response return response
...@@ -218,8 +237,11 @@ class UserForgotPasswordView(TemplateView): ...@@ -218,8 +237,11 @@ class UserForgotPasswordView(TemplateView):
email = request.POST.get('email') email = request.POST.get('email')
user = get_object_or_none(User, email=email) user = get_object_or_none(User, email=email)
if not user: if not user:
return self.get(request, errors=_('Email address invalid, ' error = _('Email address invalid, please input again')
'please input again')) return self.get(request, errors=error)
elif not user.is_local:
error = _('User auth from {}, go there change password'.format(user.source))
return self.get(request, errors=error)
else: else:
send_reset_password_mail(user) send_reset_password_mail(user)
return HttpResponseRedirect( return HttpResponseRedirect(
...@@ -251,8 +273,7 @@ class UserResetPasswordSuccessView(TemplateView): ...@@ -251,8 +273,7 @@ class UserResetPasswordSuccessView(TemplateView):
'auto_redirect': True, 'auto_redirect': True,
} }
kwargs.update(context) kwargs.update(context)
return super()\ return super().get_context_data(**kwargs)
.get_context_data(**kwargs)
class UserResetPasswordView(TemplateView): class UserResetPasswordView(TemplateView):
...@@ -261,13 +282,11 @@ class UserResetPasswordView(TemplateView): ...@@ -261,13 +282,11 @@ class UserResetPasswordView(TemplateView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
token = request.GET.get('token') token = request.GET.get('token')
user = User.validate_reset_token(token) user = User.validate_reset_token(token)
check_rules, min_length = get_password_check_rules()
password_rules = {'password_check_rules': check_rules, 'min_length': min_length}
kwargs.update(password_rules)
if not user: if not user:
kwargs.update({'errors': _('Token invalid or expired')}) kwargs.update({'errors': _('Token invalid or expired')})
else:
check_rules = get_password_check_rules()
kwargs.update({'password_check_rules': check_rules})
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
......
...@@ -14,6 +14,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin ...@@ -14,6 +14,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth import authenticate, login as auth_login from django.contrib.auth import authenticate, login as auth_login
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache from django.core.cache import cache
from django.conf import settings
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
...@@ -33,7 +34,6 @@ from django.contrib.auth import logout as auth_logout ...@@ -33,7 +34,6 @@ from django.contrib.auth import logout as auth_logout
from common.const import create_success_msg, update_success_msg from common.const import create_success_msg, update_success_msg
from common.mixins import JSONResponseMixin from common.mixins import JSONResponseMixin
from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen
from common.models import Setting, common_settings
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org from orgs.utils import current_org
from .. import forms from .. import forms
...@@ -106,12 +106,11 @@ class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -106,12 +106,11 @@ class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
success_message = update_success_msg success_message = update_success_msg
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
check_rules, min_length = get_password_check_rules() check_rules = get_password_check_rules()
context = { context = {
'app': _('Users'), 'app': _('Users'),
'action': _('Update user'), 'action': _('Update user'),
'password_check_rules': check_rules, 'password_check_rules': check_rules,
'min_length': min_length
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
...@@ -362,7 +361,7 @@ class UserProfileView(LoginRequiredMixin, TemplateView): ...@@ -362,7 +361,7 @@ class UserProfileView(LoginRequiredMixin, TemplateView):
template_name = 'users/user_profile.html' template_name = 'users/user_profile.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
mfa_setting = common_settings.SECURITY_MFA_AUTH mfa_setting = settings.SECURITY_MFA_AUTH
context = { context = {
'action': _('Profile'), 'action': _('Profile'),
'mfa_setting': mfa_setting if mfa_setting is not None else False, 'mfa_setting': mfa_setting if mfa_setting is not None else False,
...@@ -399,12 +398,11 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView): ...@@ -399,12 +398,11 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView):
return self.request.user return self.request.user
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
check_rules, min_length = get_password_check_rules() check_rules = get_password_check_rules()
context = { context = {
'app': _('Users'), 'app': _('Users'),
'action': _('Password update'), 'action': _('Password update'),
'password_check_rules': check_rules, 'password_check_rules': check_rules,
'min_length': min_length,
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
......
#!/bin/bash
#
version=$1
if [ -z "$version" ];then
echo "Usage: sh build version"
exit
fi
docker build -t jumpserver/jumpserver:$version .
"""
jumpserver.config
~~~~~~~~~~~~~~~~~
Jumpserver project setting file
:copyright: (c) 2014-2017 by Jumpserver Team
:license: GPL v2, see LICENSE for more details.
"""
import os
import json
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
class Config:
# Use it to encrypt or decrypt data
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('SECRET_KEY') or '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x'
# How many line display every page if using django pager, default 25
DISPLAY_PER_PAGE = 25
# It's used to identify your site, When we send a create mail to user, we only know login url is /login/
# But we should know the absolute url like: http://jms.jumpserver.org/login/, so SITE_URL is
# HTTP_PROTOCOL://HOST[:PORT]
SITE_URL = 'http://localhost'
# Django security setting, if your disable debug model, you should setting that
ALLOWED_HOSTS = ['*']
# Development env open this, when error occur display the full process track, Production disable it
DEBUG = True
# DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/
LOG_LEVEL = 'DEBUG'
LOG_DIR = os.path.join(BASE_DIR, 'logs')
# Database setting, Support sqlite3, mysql, postgres ....
# See https://docs.djangoproject.com/en/1.10/ref/settings/#databases
# SQLite setting:
DB_ENGINE = 'sqlite3'
DB_NAME = os.path.join(BASE_DIR, 'data', 'db.sqlite3')
# MySQL or postgres setting like:
# DB_ENGINE = 'mysql'
# DB_HOST = '127.0.0.1'
# DB_PORT = 3306
# DB_USER = 'root'
# DB_PASSWORD = ''
# DB_NAME = 'jumpserver'
# When Django start it will bind this host and port
# ./manage.py runserver 127.0.0.1:8080
HTTP_BIND_HOST = '0.0.0.0'
HTTP_LISTEN_PORT = 8080
# Use Redis as broker for celery and web socket
REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379
REDIS_PASSWORD = ''
BROKER_URL = 'redis://%(password)s%(host)s:%(port)s/3' % {
'password': REDIS_PASSWORD,
'host': REDIS_HOST,
'port': REDIS_PORT,
}
# Api token expiration when create, Jumpserver refresh time when request arrive
TOKEN_EXPIRATION = 3600
# Session and csrf domain settings
SESSION_COOKIE_AGE = 3600*24
# Email SMTP setting, we only support smtp send mail
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 25
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = '' # Caution: Some SMTP server using `Authorization Code` except password
EMAIL_USE_SSL = True if EMAIL_PORT == 465 else False
EMAIL_USE_TLS = True if EMAIL_PORT == 587 else False
EMAIL_SUBJECT_PREFIX = '[Jumpserver] '
CAPTCHA_TEST_MODE = False
# You can set jumpserver usage url here, that when user submit wizard redirect to
USER_GUIDE_URL = ''
# LDAP Auth settings
AUTH_LDAP = False
AUTH_LDAP_SERVER_URI = 'ldap://localhost:389'
AUTH_LDAP_BIND_DN = 'cn=admin,dc=jumpserver,dc=org'
AUTH_LDAP_BIND_PASSWORD = ''
AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org'
AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)'
AUTH_LDAP_USER_ATTR_MAP = {
"username": "cn",
"name": "sn",
"email": "mail"
}
AUTH_LDAP_START_TLS = False
def __init__(self):
pass
def __getattr__(self, item):
return None
class DockerConfig(Config):
"""
配置文件默认从环境变量里读取,如果没有会使用后面的默认值
"""
# 用来加密数据的key, 可以修改,但务必保存好这个字符串,丢失它后加密会无法解开
# SECRET_KEY = "SOME_KEY_NO_ONE_GUESS"
SECRET_KEY = os.environ.get("SECRET_KEY") or "MD923lkSDi8213kl),3()&^%aM2q1mz;223lkM0o1"
# 访问的域名, 格式 http[s]://域名[:端口号]
# SITE_URL = "http://jumpserver.fit2cloud.com"
SITE_URL = os.environ.get("SITE_URL") or 'http://localhost'
# 是否开启DEBUG模式
# DEBUG = True, or DEBUG = False,
DEBUG = bool(os.environ.get("DEBUG")) if os.environ.get("DEBUG") else False
# 日志级别, 默认 INFO
# LOG_LEVEL = WARN
LOG_LEVEL = os.environ.get("LOG_LEVEL") or "INFO"
# 使用的数据库类型,支持 SQLite, MySQL, PostgreSQL, Oracle
# 数据库设置, 如果使用外部的mysql请设置,否则不要改动
# DB_ENGINE = "oracle" | "postgre" | "mysql" | "sqlite3"
DB_ENGINE = os.environ.get("DB_ENGINE") or 'mysql'
# DB_HOST = "192.168.1.1"
DB_HOST = os.environ.get("DB_HOST") or 'mysql'
# 端口号
# DB_PORT = 3306
DB_PORT = os.environ.get("DB_PORT") or 3306
# 数据库账号
# DB_USER = "jumpserver"
DB_USER = os.environ.get("DB_USER") or 'root'
# 数据库密码
# DB_PASSWORD = "db_jumpserver_password"
DB_PASSWORD = os.environ.get("DB_PASSWORD") or ''
# 数据库名称
# DB_NAME = "jumpserver"
DB_NAME = os.environ.get("DB_NAME") or 'jumpserver'
# Redis配置,如果不使用外部redis不要改动
# Redis地址
# REDIS_HOST = "192.168.1.1"
REDIS_HOST = os.environ.get("REDIS_HOST") or 'redis'
# Redis端口号
# REDIS_PORT = 6380
REDIS_PORT = os.environ.get("REDIS_PORT") or 6379
# Redis密码
# REDIS_PASSWORD = "redis_password"
REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD") or ''
# 邮箱SMTP设置, 可以参考各运营商配置文档
# SMTP服务器地址
# EMAIL_HOST = 'smtp.qq.com'
EMAIL_HOST = 'smtp.163.com'
# SMTP端口号
# EMAIL_PORT = 465
EMAIL_PORT = 25
# SMTP连接邮箱地址
# EMAIL_HOST_USER = "noreply@jumpserver.org"
EMAIL_HOST_USER = ''
# SMTP邮箱的密码, 注意 一些运营商通常要求使用授权码来发SMTP邮件
EMAIL_HOST_PASSWORD = ''
# 是否启用SSL, 如果端口号是 465通常设置为True
# EMAIL_USE_SSL = True
EMAIL_USE_SSL = True if EMAIL_PORT == 465 else False
# 是否启用TLS, 如果端口号是 587通常设置为True
# EMAIL_USE_TLS = True
EMAIL_USE_TLS = True if EMAIL_PORT == 587 else False
# 邮件的主题前缀
EMAIL_SUBJECT_PREFIX = '[Jumpserver] '
# 认证启用LDAP的设置
# 是否启用LDAP,默认不启用
# AUTH_LDAP = True
AUTH_LDAP = False
# LDAP的地址
AUTH_LDAP_SERVER_URI = 'ldap://localhost:389'
# LDAP绑定的查询账户
AUTH_LDAP_BIND_DN = 'cn=admin,dc=jumpserver,dc=org'
# 密码
AUTH_LDAP_BIND_PASSWORD = ''
# 用户所在的ou
AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org'
# 查询时使用的过滤器, 仅可以修改前面的表示符,可能是cn或uid, 也就是登录用户名所在字段
# AUTH_LDAP_SEARCH_FILTER = '(uid=%(user)s)'
AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)'
# LDAP用户信息映射到Jumpserver
AUTH_LDAP_USER_ATTR_MAP = {
"username": "cn", # 将LDAP信息中的 `cn` 字段映射为 `username(用户名)`
"name": "sn", # 将 LDAP信息中的 `sn` 映射为 `name(姓名)`
"email": "mail" # 将 LDAP信息中的 `mail` 映射为 `email(邮箱地址)`
}
# 是否启用TLS加密
AUTH_LDAP_START_TLS = False
# Default using Config settings, you can write if/else for different env
config = DockerConfig()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" """
jumpserver.config jumpserver.config
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
...@@ -13,46 +15,73 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ...@@ -13,46 +15,73 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
class Config: class Config:
# Use it to encrypt or decrypt data """
Jumpserver Config File
Jumpserver 配置文件
Jumpserver use this config for drive django framework running,
You can set is value or set the same envirment value,
Jumpserver look for config order: file => env => default
Jumpserver使用配置来驱动Django框架的运行,
你可以在该文件中设置,或者设置同样名称的环境变量,
Jumpserver使用配置的顺序: 文件 => 环境变量 => 默认值
"""
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('SECRET_KEY') or '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x' # 加密秘钥 生产环境中请修改为随机字符串,请勿外泄
SECRET_KEY = '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x'
# Django security setting, if your disable debug model, you should setting that # SECURITY WARNING: keep the bootstrap token used in production secret!
ALLOWED_HOSTS = ['*'] # 预共享Token coco和guacamole用来注册服务账号,不在使用原来的注册接受机制
BOOTSTRAP_TOKEN = 'PleaseChangeMe'
# Development env open this, when error occur display the full process track, Production disable it # Development env open this, when error occur display the full process track, Production disable it
DEBUG = os.environ.get("DEBUG") or True # DEBUG 模式 开启DEBUG后遇到错误时可以看到更多日志
# DEBUG = True
# DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/ # DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/
LOG_LEVEL = os.environ.get("LOG_LEVEL") or 'DEBUG' # 日志级别
LOG_DIR = os.path.join(BASE_DIR, 'logs') # LOG_LEVEL = 'DEBUG'
# LOG_DIR = os.path.join(BASE_DIR, 'logs')
# Database setting, Support sqlite3, mysql, postgres .... # Database setting, Support sqlite3, mysql, postgres ....
# 数据库设置
# See https://docs.djangoproject.com/en/1.10/ref/settings/#databases # See https://docs.djangoproject.com/en/1.10/ref/settings/#databases
# SQLite setting: # SQLite setting:
DB_ENGINE = 'sqlite3' # 使用单文件sqlite数据库
DB_NAME = os.path.join(BASE_DIR, 'data', 'db.sqlite3') # DB_ENGINE = 'sqlite3'
# DB_NAME = os.path.join(BASE_DIR, 'data', 'db.sqlite3')
# MySQL or postgres setting like: # MySQL or postgres setting like:
# DB_ENGINE = os.environ.get("DB_ENGINE") or 'mysql' # 使用Mysql作为数据库
# DB_HOST = os.environ.get("DB_HOST") or '127.0.0.1' DB_ENGINE = 'mysql'
# DB_PORT = os.environ.get("DB_PORT") or 3306 DB_HOST = '127.0.0.1'
# DB_USER = os.environ.get("DB_USER") or 'jumpserver' DB_PORT = 3306
# DB_PASSWORD = os.environ.get("DB_PASSWORD") or 'weakPassword' DB_USER = 'jumpserver'
# DB_NAME = os.environ.get("DB_NAME") or 'jumpserver' DB_PASSWORD = ''
DB_NAME = 'jumpserver'
# When Django start it will bind this host and port # When Django start it will bind this host and port
# ./manage.py runserver 127.0.0.1:8080 # ./manage.py runserver 127.0.0.1:8080
# 运行时绑定端口
HTTP_BIND_HOST = '0.0.0.0' HTTP_BIND_HOST = '0.0.0.0'
HTTP_LISTEN_PORT = 8080 HTTP_LISTEN_PORT = 8080
# Use Redis as broker for celery and web socket # Use Redis as broker for celery and web socket
REDIS_HOST = os.environ.get("REDIS_HOST") or '127.0.0.1' # Redis配置
REDIS_PORT = os.environ.get("REDIS_PORT") or 6379 REDIS_HOST = '127.0.0.1'
REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD") or '' REDIS_PORT = 6379
REDIS_DB_CELERY = os.environ.get('REDIS_DB') or 3 REDIS_PASSWORD = ''
REDIS_DB_CACHE = os.environ.get('REDIS_DB') or 4
# Use OpenID authorization
# 使用OpenID 来进行认证设置
# BASE_SITE_URL = 'http://localhost:8080'
# AUTH_OPENID = False # True or False
# AUTH_OPENID_SERVER_URL = 'https://openid-auth-server.com/'
# AUTH_OPENID_REALM_NAME = 'realm-name'
# AUTH_OPENID_CLIENT_ID = 'client-id'
# AUTH_OPENID_CLIENT_SECRET = 'client-secret'
def __init__(self): def __init__(self):
pass pass
......
#!/bin/bash
function cleanup()
{
local pids=`jobs -p`
if [[ "$pids" != "" ]]; then
kill $pids >/dev/null 2>/dev/null
fi
}
trap cleanup EXIT
python jms start all
...@@ -178,7 +178,7 @@ def start_celery(): ...@@ -178,7 +178,7 @@ def start_celery():
'-A', 'ops', '-A', 'ops',
'-l', LOG_LEVEL.lower(), '-l', LOG_LEVEL.lower(),
'--pidfile', pid_file, '--pidfile', pid_file,
'-c', str(WORKERS), '--autoscale', '20,4',
] ]
if DAEMON: if DAEMON:
cmd.extend([ cmd.extend([
......
libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk python-dev openssl libssl-dev libldap2-dev libsasl2-dev sqlite gcc automake libkrb5-dev libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk python-dev openssl libssl-dev libldap2-dev libsasl2-dev sqlite libkrb5-dev sshpass
...@@ -35,7 +35,7 @@ eventlet==0.24.1 ...@@ -35,7 +35,7 @@ eventlet==0.24.1
ForgeryPy==0.1 ForgeryPy==0.1
greenlet==0.4.14 greenlet==0.4.14
gunicorn==19.9.0 gunicorn==19.9.0
idna==2.7 idna==2.6
itsdangerous==0.24 itsdangerous==0.24
itypes==1.1.0 itypes==1.1.0
Jinja2==2.10 Jinja2==2.10
...@@ -61,7 +61,7 @@ pytz==2018.3 ...@@ -61,7 +61,7 @@ pytz==2018.3
PyYAML==3.12 PyYAML==3.12
redis==2.10.6 redis==2.10.6
requests==2.18.4 requests==2.18.4
jms-storage==0.0.19 jms-storage==0.0.20
s3transfer==0.1.13 s3transfer==0.1.13
simplejson==3.13.2 simplejson==3.13.2
six==1.11.0 six==1.11.0
...@@ -74,3 +74,6 @@ Werkzeug==0.14.1 ...@@ -74,3 +74,6 @@ Werkzeug==0.14.1
drf-nested-routers==0.90.2 drf-nested-routers==0.90.2
aliyun-python-sdk-core-v3==2.9.1 aliyun-python-sdk-core-v3==2.9.1
aliyun-python-sdk-ecs==4.10.1 aliyun-python-sdk-ecs==4.10.1
python-keycloak==0.13.3
python-keycloak-client==0.1.3
rest_condition==1.0.3
libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel sshpass openldap-devel mysql-devel libffi-devel openssh-clients libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel sshpass openldap-devel mariadb-devel mysql-devel libffi-devel openssh-clients
#!/bin/bash
#
host=127.0.0.1
port=3306
username=root
db=jumpserver
echo "备份原来的 migrations"
mysqldump -u${username} -h${host} -P${port} -p ${db} django_migrations > django_migrations.sql.bak
ret=$?
if [ ${ret} == "0" ];then
echo "开始使用新的migrations文件"
mysql -u${username} -h${host} -P${port} -p ${db} < django_migrations.sql
else
echo "Not valid"
fi
# 名称 用户名 密码
test123 testq12 test123123123
#!/usr/bin/env python
import requests
import sys
admin_username = 'admin'
admin_password = 'admin'
domain_url = 'http://localhost:8080'
class UserCreation:
headers = {}
def __init__(self, username, password, domain):
self.username = username
self.password = password
self.domain = domain
def auth(self):
url = "{}/api/users/v1/token/".format(self.domain)
data = {"username": self.username, "password": self.password}
resp = requests.post(url, data=data)
if resp.status_code == 200:
data = resp.json()
self.headers.update({
'Authorization': '{} {}'.format(data['Keyword'], data['Token'])
})
else:
print("用户名 或 密码 或 地址 不对")
sys.exit(2)
def get_user_detail(self, name, url):
resp = requests.get(url, headers=self.headers)
if resp.status_code == 200:
data = resp.json()
if len(data) < 1:
return None
for d in data:
if d['name'] == name:
return d
return None
return None
def get_system_user_detail(self, name):
url = '{}/api/assets/v1/system-user/?name={}'.format(self.domain, name)
return self.get_user_detail(name, url)
def create_system_user(self, info):
system_user = self.get_system_user_detail(info.get('name'))
if system_user:
return system_user
url = '{}/api/assets/v1/system-user/'.format(self.domain)
resp = requests.post(url, data=info, headers=self.headers, json=False)
if resp.status_code == 201:
return resp.json()
else:
print("创建系统用户失败: {} {}".format(info['name'], resp.content))
return None
def set_system_user_auth(self, system_user, info):
url = '{}/api/assets/v1/system-user/{}/auth-info/'.format(
self.domain, system_user['id']
)
data = {'password': info.get('password')}
resp = requests.patch(url, data=data, headers=self.headers)
if resp.status_code > 300:
print("设置系统用户密码失败: {} {}".format(
system_user.get('name'), resp.content.decode()
))
else:
return True
def get_admin_user_detail(self, name):
url = '{}/api/assets/v1/admin-user/?name={}'.format(self.domain, name)
return self.get_user_detail(name, url)
def create_admin_user(self, info):
admin_user = self.get_admin_user_detail(info.get('name'))
if admin_user:
return admin_user
url = '{}/api/assets/v1/admin-user/'.format(self.domain)
resp = requests.post(url, data=info, headers=self.headers, json=False)
if resp.status_code == 201:
return resp.json()
else:
print("创建管理用户失败: {} {}".format(info['name'], resp.content.decode()))
return None
def set_admin_user_auth(self, admin_user, info):
url = '{}/api/assets/v1/admin-user/{}/auth/'.format(
self.domain, admin_user['id']
)
data = {'password': info.get('password')}
resp = requests.patch(url, data=data, headers=self.headers)
if resp.status_code > 300:
print("设置管理用户密码失败: {} {}".format(
admin_user.get('name'), resp.content.decode()
))
else:
return True
def create_system_users(self):
print("#"*10, " 开始创建系统用户 ", "#"*10)
users = []
f = open('system_users.txt')
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
name, username, password, protocol, auto_push = line.split()[:5]
info = {
"name": name,
"username": username,
"password": password,
"protocol": protocol,
"auto_push": bool(int(auto_push)),
"login_mode": "auto"
}
users.append(info)
for i, info in enumerate(users, start=1):
system_user = self.create_system_user(info)
if system_user and self.set_system_user_auth(system_user, info):
print("[{}] 创建系统用户成功: {}".format(i, system_user['name']))
def create_admin_users(self):
print("\n", "#"*10, " 开始创建管理用户 ", "#"*10)
users = []
f = open('admin_users.txt')
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
name, username, password = line.split()[:3]
info = {
"name": name,
"username": username,
"password": password,
}
users.append(info)
for i, info in enumerate(users, start=1):
admin_user = self.create_admin_user(info)
if admin_user and self.set_admin_user_auth(admin_user, info):
print("[{}] 创建管理用户成功: {}".format(i, admin_user['name']))
def main():
api = UserCreation(username=admin_username,
password=admin_password,
domain=domain_url)
api.auth()
api.create_system_users()
api.create_admin_users()
if __name__ == '__main__':
main()
# 名称 用户名 密码 协议[ssh,rdp] 自动推送[0不推送,1自动推送]
test123 test123 test123123123 ssh 0
test1323 test123 test123123123 ssh 0
1. 安装依赖包
$ pip install requests
2. 设置账号密码和地址
$ vim bulk_create_user.py # 设置为正确的值
admin_username = 'admin'
admin_password = 'admin'
domain_url = 'http://localhost:8081'
3. 配置需要添加的系统用户
$ vim system_users.txt
# 名称 用户名 密码
test123 testq12 test123123123
3. 配置需要添加的系统用户
$ vim system_users.txt
# 名称 用户名 密码 协议[ssh,rdp] 自动推送[0不推送,1自动推送]
test123 test123 test123123123 ssh 0
4. 运行
$ python bulk_create_user.py
\ No newline at end of file
-- MySQL dump 10.13 Distrib 5.7.17, for osx10.12 (x86_64)
--
-- Host: 127.0.0.1 Database: jumpserver
-- ------------------------------------------------------
-- Server version 5.7.19
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `django_migrations`
--
DROP TABLE IF EXISTS `django_migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `django_migrations` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`applied` datetime(6) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=99 DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `django_migrations`
--
LOCK TABLES `django_migrations` WRITE;
/*!40000 ALTER TABLE `django_migrations` DISABLE KEYS */;
INSERT INTO `django_migrations` VALUES (1,'contenttypes','0001_initial','2018-10-16 03:54:01.083767'),(2,'contenttypes','0002_remove_content_type_name','2018-10-16 03:54:01.134230'),(3,'auth','0001_initial','2018-10-16 03:54:01.303742'),(4,'auth','0002_alter_permission_name_max_length','2018-10-16 03:54:01.336313'),(5,'auth','0003_alter_user_email_max_length','2018-10-16 03:54:01.348483'),(6,'auth','0004_alter_user_username_opts','2018-10-16 03:54:01.360740'),(7,'auth','0005_alter_user_last_login_null','2018-10-16 03:54:01.371895'),(8,'auth','0006_require_contenttypes_0002','2018-10-16 03:54:01.378513'),(9,'auth','0007_alter_validators_add_error_messages','2018-10-16 03:54:01.388758'),(10,'auth','0008_alter_user_username_max_length','2018-10-16 03:54:01.400890'),(11,'users','0001_initial','2018-10-16 03:54:01.944343'),(12,'admin','0001_initial','2018-10-16 03:54:02.034975'),(13,'admin','0002_logentry_remove_auto_add','2018-10-16 03:54:02.049349'),(14,'admin','0003_logentry_add_action_flag_choices','2018-10-16 03:54:02.064696'),(15,'assets','0001_initial','2018-10-16 03:54:02.667713'),(16,'assets','0002_auto_20180105_1807','2018-10-16 03:54:02.705499'),(17,'assets','0003_auto_20180109_2331','2018-10-16 03:54:02.813949'),(18,'assets','0004_auto_20180125_1218','2018-10-16 03:54:02.877967'),(19,'assets','0005_auto_20180126_1637','2018-10-16 03:54:03.011673'),(20,'assets','0006_auto_20180130_1502','2018-10-16 03:54:03.265462'),(21,'assets','0007_auto_20180225_1815','2018-10-16 03:54:03.613870'),(22,'assets','0008_auto_20180306_1804','2018-10-16 03:54:03.821335'),(23,'assets','0009_auto_20180307_1212','2018-10-16 03:54:03.843992'),(24,'assets','0010_auto_20180307_1749','2018-10-16 03:54:03.871740'),(25,'assets','0011_auto_20180326_0957','2018-10-16 03:54:04.041219'),(26,'assets','0012_auto_20180404_1302','2018-10-16 03:54:04.100811'),(27,'assets','0013_auto_20180411_1135','2018-10-16 03:54:04.218430'),(28,'assets','0014_auto_20180427_1245','2018-10-16 03:54:04.328114'),(29,'assets','0015_auto_20180510_1235','2018-10-16 03:54:04.355353'),(30,'assets','0016_auto_20180511_1203','2018-10-16 03:54:04.382377'),(31,'assets','0017_auto_20180702_1415','2018-10-16 03:54:04.542299'),(32,'assets','0018_auto_20180807_1116','2018-10-16 03:54:04.928882'),(33,'assets','0019_auto_20180816_1320','2018-10-16 03:54:04.996638'),(34,'assets','0020_auto_20180816_1652','2018-10-16 03:54:05.343654'),(35,'assets','0021_auto_20180903_1132','2018-10-16 03:54:05.370216'),(36,'assets','0022_auto_20181012_1717','2018-10-16 03:54:05.538709'),(37,'users','0002_auto_20171225_1157','2018-10-16 03:54:05.698481'),(38,'users','0003_auto_20180101_0046','2018-10-16 03:54:05.711697'),(39,'users','0004_auto_20180125_1218','2018-10-16 03:54:05.772696'),(40,'users','0005_auto_20180306_1804','2018-10-16 03:54:05.797780'),(41,'users','0006_auto_20180411_1135','2018-10-16 03:54:05.948125'),(42,'users','0007_auto_20180419_1036','2018-10-16 03:54:06.091956'),(43,'users','0008_auto_20180425_1516','2018-10-16 03:54:06.105389'),(44,'users','0009_auto_20180517_1537','2018-10-16 03:54:06.131188'),(45,'users','0010_auto_20180606_1505','2018-10-16 03:54:06.321848'),(46,'users','0011_user_source','2018-10-16 03:54:06.428777'),(47,'users','0012_auto_20180710_1641','2018-10-16 03:54:06.514067'),(48,'users','0013_auto_20180807_1116','2018-10-16 03:54:06.650543'),(49,'users','0014_auto_20180816_1652','2018-10-16 03:54:06.746302'),(50,'audits','0001_initial','2018-10-16 03:54:06.779067'),(51,'audits','0002_ftplog_org_id','2018-10-16 03:54:06.816801'),(52,'audits','0003_auto_20180816_1652','2018-10-16 03:54:06.855191'),(53,'audits','0004_operatelog_passwordchangelog_userloginlog','2018-10-16 03:54:06.907298'),(54,'auth','0009_alter_user_last_name_max_length','2018-10-16 03:54:06.922438'),(55,'captcha','0001_initial','2018-10-16 03:54:06.952390'),(56,'common','0001_initial','2018-10-16 03:54:06.979536'),(57,'common','0002_auto_20180111_1407','2018-10-16 03:54:07.040816'),(58,'common','0003_setting_category','2018-10-16 03:54:07.072335'),(59,'common','0004_setting_encrypted','2018-10-16 03:54:07.108007'),(60,'django_celery_beat','0001_initial','2018-10-16 03:54:07.246440'),(61,'django_celery_beat','0002_auto_20161118_0346','2018-10-16 03:54:07.330192'),(62,'django_celery_beat','0003_auto_20161209_0049','2018-10-16 03:54:07.356330'),(63,'django_celery_beat','0004_auto_20170221_0000','2018-10-16 03:54:07.365220'),(64,'django_celery_beat','0005_add_solarschedule_events_choices','2018-10-16 03:54:07.377127'),(65,'django_celery_beat','0006_auto_20180210_1226','2018-10-16 03:54:07.414056'),(66,'ops','0001_initial','2018-10-16 03:54:07.627395'),(67,'ops','0002_celerytask','2018-10-16 03:54:07.653158'),(68,'orgs','0001_initial','2018-10-16 03:54:07.849837'),(69,'orgs','0002_auto_20180903_1132','2018-10-16 03:54:07.865926'),(70,'perms','0001_initial','2018-10-16 03:54:08.126412'),(71,'perms','0002_auto_20171228_0025','2018-10-16 03:54:08.328918'),(72,'perms','0003_auto_20180225_1815','2018-10-16 03:54:08.501763'),(73,'perms','0004_auto_20180411_1135','2018-10-16 03:54:08.770485'),(74,'perms','0005_migrate_data_20180411_1144','2018-10-16 03:54:08.871984'),(75,'perms','0006_auto_20180606_1505','2018-10-16 03:54:08.947327'),(76,'perms','0007_auto_20180807_1116','2018-10-16 03:54:09.158080'),(77,'perms','0008_auto_20180816_1652','2018-10-16 03:54:09.277244'),(78,'perms','0009_auto_20180903_1132','2018-10-16 03:54:09.312652'),(79,'sessions','0001_initial','2018-10-16 03:54:09.347562'),(80,'terminal','0001_initial','2018-10-16 03:54:09.588109'),(81,'terminal','0002_auto_20171228_0025','2018-10-16 03:54:09.864943'),(82,'terminal','0003_auto_20171230_0308','2018-10-16 03:54:09.916736'),(83,'terminal','0004_session_remote_addr','2018-10-16 03:54:09.955561'),(84,'terminal','0005_auto_20180122_1154','2018-10-16 03:54:10.032369'),(85,'terminal','0006_auto_20180123_1037','2018-10-16 03:54:10.053268'),(86,'terminal','0007_session_date_last_active','2018-10-16 03:54:10.093694'),(87,'terminal','0008_auto_20180307_1603','2018-10-16 03:54:10.124400'),(88,'terminal','0009_auto_20180326_0957','2018-10-16 03:54:10.233769'),(89,'terminal','0010_auto_20180423_1140','2018-10-16 03:54:10.380928'),(90,'terminal','0011_auto_20180807_1116','2018-10-16 03:54:10.467649'),(91,'terminal','0012_auto_20180816_1652','2018-10-16 03:54:10.557186'),(92,'xpack','0001_initial','2018-10-16 03:54:11.066130'),(93,'assets','0023_auto_20181016_1603','2018-10-16 08:40:26.566475'),(94,'assets','0024_auto_20181016_1640','2018-10-16 08:40:26.654385'),(95,'assets','0025_auto_20181016_1640','2018-10-16 08:40:45.837227'),(96,'assets','0023_auto_20181016_1650','2018-10-16 08:50:48.274294'),(97,'users','0015_auto_20181105_1112','2018-11-09 03:58:37.965880'),(98,'users','0016_auto_20181109_1505','2018-11-13 03:42:19.711320');
/*!40000 ALTER TABLE `django_migrations` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2018-11-13 16:02:01
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