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
.idea/
db.sqlite3
config.py
migrations/
*.log
host_rsa_key
*.bat
......@@ -33,3 +32,4 @@ celerybeat-schedule.db
data/static
docs/_build/
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采纳分布式架构,支持多机房跨区域部署,中心节点
### 功能
![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 @@
# -*- coding: utf-8 -*-
#
__version__ = "1.4.3"
__version__ = "1.4.4"
......@@ -17,6 +17,7 @@ from django.db import transaction
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from common.mixins import IDInFilterMixin
from common.utils import get_logger
......@@ -37,9 +38,17 @@ class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet):
"""
Admin user api set, for add,delete,update,list,retrieve resource
"""
filter_fields = ("name", "username")
search_fields = filter_fields
queryset = AdminUser.objects.all()
serializer_class = serializers.AdminUserSerializer
permission_classes = (IsOrgAdmin,)
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
return queryset
class AdminUserAuthApi(generics.UpdateAPIView):
......
......@@ -53,14 +53,14 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
if show_current_asset:
self.queryset = self.queryset.filter(
Q(nodes=node_id) | Q(nodes__isnull=True)
).distinct()
)
return
if show_current_asset:
self.queryset = self.queryset.filter(nodes=node).distinct()
self.queryset = self.queryset.filter(nodes=node)
else:
self.queryset = self.queryset.filter(
nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key),
).distinct()
)
def filter_admin_user_id(self):
admin_user_id = self.request.query_params.get('admin_user_id')
......
......@@ -2,6 +2,7 @@
#
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from django.shortcuts import get_object_or_404
from ..hands import IsOrgAdmin
......@@ -13,14 +14,20 @@ __all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
class CommandFilterViewSet(BulkModelViewSet):
filter_fields = ("name",)
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
queryset = CommandFilter.objects.all()
serializer_class = serializers.CommandFilterSerializer
pagination_class = LimitOffsetPagination
class CommandFilterRuleViewSet(BulkModelViewSet):
filter_fields = ("content",)
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterRuleSerializer
pagination_class = LimitOffsetPagination
def get_queryset(self):
fpk = self.kwargs.get('filter_pk')
......
......@@ -2,6 +2,7 @@
from rest_framework_bulk import BulkModelViewSet
from rest_framework.views import APIView, Response
from rest_framework.pagination import LimitOffsetPagination
from django.views.generic.detail import SingleObjectMixin
......@@ -20,6 +21,11 @@ class DomainViewSet(BulkModelViewSet):
queryset = Domain.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.DomainSerializer
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
return queryset
def get_serializer_class(self):
if self.request.query_params.get('gateway'):
......@@ -33,11 +39,12 @@ class DomainViewSet(BulkModelViewSet):
class GatewayViewSet(BulkModelViewSet):
filter_fields = ("domain",)
filter_fields = ("domain__name", "name", "username", "ip", "domain")
search_fields = filter_fields
queryset = Gateway.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.GatewaySerializer
pagination_class = LimitOffsetPagination
class GatewayTestConnectionApi(SingleObjectMixin, APIView):
......
......@@ -14,6 +14,7 @@
# limitations under the License.
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from django.db.models import Count
from common.utils import get_logger
......@@ -27,8 +28,11 @@ __all__ = ['LabelViewSet']
class LabelViewSet(BulkModelViewSet):
filter_fields = ("name", "value")
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.LabelSerializer
pagination_class = LimitOffsetPagination
def list(self, request, *args, **kwargs):
if request.query_params.get("distinct"):
......
......@@ -42,9 +42,16 @@ class SystemUserViewSet(BulkModelViewSet):
"""
System user api set, for add,delete,update,list,retrieve resource
"""
filter_fields = ("name", "username")
search_fields = filter_fields
queryset = SystemUser.objects.all()
serializer_class = serializers.SystemUserSerializer
permission_classes = (IsOrgAdminOrAppUser,)
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
return queryset
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):
'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):
"""
Ansible use it create inventory, First using asset user,
......
......@@ -92,7 +92,7 @@ class Node(OrgModelMixin):
return child
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(
key__regex=pattern.format(self.key)
)
......@@ -121,10 +121,10 @@ class Node(OrgModelMixin):
def get_assets(self):
from .asset import Asset
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:
assets = Asset.objects.filter(nodes__id=self.id)
return assets
return assets.distinct()
def get_valid_assets(self):
return self.get_assets().valid()
......
......@@ -93,7 +93,7 @@ $(document).ready(function(){
columns: [{data: function(){return ""}}, {data: "name" }, {data: "username" }, {data: "assets_amount" },
{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 () {
......
......@@ -66,7 +66,7 @@ function initTable() {
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
......
......@@ -95,7 +95,7 @@ function initTable() {
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
......
......@@ -98,7 +98,7 @@ function initTable() {
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
......
......@@ -62,7 +62,7 @@ function initTable() {
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
......
......@@ -47,7 +47,7 @@ function initTable() {
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
......
......@@ -100,7 +100,7 @@ function initTable() {
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
......
......@@ -62,7 +62,7 @@
{% block custom_foot_js %}
<script>
var zTree, asset_table;
var zTree, asset_table, show=0;
var inited = false;
var url;
function initTable() {
......@@ -102,7 +102,7 @@ function initTable() {
{data: "system_users_granted", orderable: false}
]
};
asset_table = jumpserver.initDataTable(options);
asset_table = jumpserver.initServerSideDataTable(options);
return asset_table
}
......@@ -183,6 +183,21 @@ $(document).ready(function () {
$('#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>
{% endblock %}
\ No newline at end of file
......@@ -277,7 +277,8 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
v = ''
elif k == 'domain':
v = get_object_or_none(Domain, name=v)
elif k == 'platform':
v = v.lower().capitalize()
if v != '':
asset_dict[k] = v
......
......@@ -36,6 +36,7 @@ class LabelCreateView(AdminUserRequiredMixin, CreateView):
form_class = LabelForm
success_url = reverse_lazy('assets:label-list')
success_message = create_success_msg
disable_name = ['draw', 'search', 'limit', 'offset', '_']
def get_context_data(self, **kwargs):
context = {
......@@ -45,6 +46,16 @@ class LabelCreateView(AdminUserRequiredMixin, CreateView):
kwargs.update(context)
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):
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):
return users
def get_queryset(self):
users = self.get_org_users()
queryset = super().get_queryset().filter(username__in=users)
if current_org.is_default():
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.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 -*-
#
import os
import json
import jms_storage
from rest_framework.views import Response, APIView
from ldap3 import Server, Connection
......@@ -8,8 +11,9 @@ from django.core.mail import get_connection, send_mail
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from .permissions import IsOrgAdmin
from .permissions import IsOrgAdmin, IsSuperUser
from .serializers import MailTestSerializer, LDAPTestSerializer
from .models import Setting
class MailTestingAPI(APIView):
......@@ -85,19 +89,95 @@ class LDAPTestingAPI(APIView):
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):
def get(self, request):
if not settings.DEBUG:
return Response("Not in debug mode")
data = {}
for k, v in settings.__dict__.items():
if k and k.isupper():
try:
json.dumps(v)
data[k] = v
except (json.JSONDecodeError, TypeError):
data[k] = str(v)
for i in [settings, getattr(settings, '_wrapped')]:
if not i:
continue
for k, v in i.__dict__.items():
if k and k.isupper():
try:
json.dumps(v)
data[k] = v
except (json.JSONDecodeError, TypeError):
data[k] = str(v)
return Response(data)
......
......@@ -4,29 +4,28 @@ import json
from django import forms
from django.utils.translation import ugettext_lazy as _
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, \
FormEncryptMixin, FormEncryptDictField
FormEncryptMixin
class BaseForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, field in self.fields.items():
db_value = getattr(common_settings, name)
django_value = getattr(settings, name) if hasattr(settings, name) else None
value = getattr(settings, name, 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
if db_value is False or db_value:
if isinstance(db_value, dict):
db_value = json.dumps(db_value)
initial_value = db_value
elif django_value is False or django_value:
initial_value = django_value
if value is not None:
if isinstance(value, dict):
value = json.dumps(value)
initial_value = value
# elif django_value is False or django_value:
# initial_value = django_value
else:
initial_value = ''
field.initial = initial_value
......@@ -44,7 +43,7 @@ class BaseForm(forms.Form):
field = self.fields[name]
if isinstance(field.widget, forms.PasswordInput) and not value:
continue
if value == getattr(common_settings, name):
if value == getattr(settings, name):
continue
encrypted = True if isinstance(field, FormEncryptMixin) else False
......@@ -70,7 +69,7 @@ class BasicSettingForm(BaseForm):
)
EMAIL_SUBJECT_PREFIX = forms.CharField(
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):
class LDAPSettingForm(BaseForm):
AUTH_LDAP_SERVER_URI = forms.CharField(
label=_("LDAP server"), initial='ldap://localhost:389'
label=_("LDAP server"),
)
AUTH_LDAP_BIND_DN = forms.CharField(
label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org'
label=_("Bind DN"),
)
AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField(
label=_("Password"), initial='',
label=_("Password"),
widget=forms.PasswordInput, required=False
)
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")
)
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)")
)
AUTH_LDAP_USER_ATTR_MAP = FormDictField(
......@@ -120,14 +119,14 @@ class LDAPSettingForm(BaseForm):
help_text=_(
"User attr map present how to map LDAP user attr to jumpserver, "
"username,name,email is jumpserver attr"
)
),
)
# AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
# AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
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):
......@@ -135,32 +134,24 @@ class TerminalSettingForm(BaseForm):
('hostname', _('Hostname')),
('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(
initial=True, required=False, label=_("Password auth")
)
TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField(
initial=True, required=False, label=_("Public key auth")
)
TERMINAL_COMMAND_STORAGE = FormEncryptDictField(
label=_("Command storage"), help_text=_(
"Set terminal storage setting, `default` is the using as default,"
"You can set other storage and some terminal using"
)
TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField(
initial=5, label=_("Heartbeat interval"), help_text=_("Units: seconds")
)
TERMINAL_REPLAY_STORAGE = FormEncryptDictField(
label=_("Replay storage"), help_text=_(
"Set replay storage setting, `default` is the using as default,"
"You can set other storage and some terminal using"
)
TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField(
choices=SORT_BY_CHOICES, initial='hostname', label=_("List sort by")
)
class TerminalCommandStorage(BaseForm):
pass
class SecuritySettingForm(BaseForm):
# MFA global setting
SECURITY_MFA_AUTH = forms.BooleanField(
......@@ -181,10 +172,11 @@ class SecuritySettingForm(BaseForm):
initial=30, min_value=5,
label=_("No logon interval"),
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."
)
)
# ssh max idle time
SECURITY_MAX_IDLE_TIME = forms.IntegerField(
initial=30, required=False,
label=_("Connection max idle time"),
......@@ -193,6 +185,18 @@ class SecuritySettingForm(BaseForm):
'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
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
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:
def get(self, request, *args, **kwargs):
self.get_date_range()
return super().get(request, *args, **kwargs)
import json
import ldap
from django.db import models
from django.core.cache import cache
from django.db.utils import ProgrammingError, OperationalError
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from .utils import get_signer
......@@ -40,11 +39,7 @@ class Setting(models.Model):
return self.name
def __getattr__(self, item):
instances = self.__class__.objects.filter(name=item)
if len(instances) == 1:
return instances[0].cleaned_value
else:
return None
return cache.get(item)
@property
def cleaned_value(self):
......@@ -67,6 +62,32 @@ class Setting(models.Model):
except json.JSONDecodeError as 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
def refresh_all_settings(cls):
try:
......@@ -78,22 +99,15 @@ class Setting(models.Model):
def refresh_setting(self):
setattr(settings, self.name, self.cleaned_value)
if self.name == "AUTH_LDAP":
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:
settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND)
if self.name == "AUTH_LDAP_SEARCH_FILTER":
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)
old_setting = settings.AUTHENTICATION_BACKENDS
old_setting.remove(settings.AUTH_LDAP_BACKEND)
settings.AUTHENTICATION_BACKENDS = old_setting
class Meta:
db_table = "settings"
common_settings = Setting()
......@@ -5,6 +5,7 @@ from rest_framework import permissions
from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import redirect
from django.http.response import HttpResponseForbidden
from django.conf import settings
from orgs.utils import current_org
......@@ -96,3 +97,12 @@ class SuperUserRequiredMixin(UserPassesTestMixin):
def test_func(self):
if self.request.user.is_authenticated and self.request.user.is_superuser:
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 @@
from django.dispatch import Signal
django_ready = Signal()
ldap_auth_enable = Signal(providing_args=["enabled"])
......@@ -2,13 +2,14 @@
#
from django.dispatch import receiver
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.core.cache import cache
from jumpserver.utils import current_request
from .models import Setting
from .utils import get_logger
from .signals import django_ready, ldap_auth_enable
from .signals import django_ready
logger = get_logger(__file__)
......@@ -25,25 +26,43 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs):
def refresh_all_settings_on_django_ready(sender, **kwargs):
logger.debug("Receive django ready signal")
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:
LazySettings.__getattr__ = monkey_patch_getattr
LazySettings.__setattr__ = monkey_patch_setattr
LazySettings.__delattr__ = monkey_patch_delattr
Setting.refresh_all_settings()
except (ProgrammingError, OperationalError):
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")
def on_create_set_created_by(sender, instance=None, **kwargs):
if hasattr(instance, 'created_by') and not instance.created_by:
......
......@@ -22,10 +22,6 @@ def send_mail_async(*args, **kwargs):
Example:
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:
args = list(args)
args[0] = settings.EMAIL_SUBJECT_PREFIX + args[0]
......
......@@ -75,32 +75,6 @@
{% block custom_foot_js %}
<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: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>
{% 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 @@
{% endif %}
{% 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>
<h3>{% trans "Command storage" %}</h3>
......@@ -71,6 +79,7 @@
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Type' %}</th>
<th>{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
......@@ -78,10 +87,13 @@
<tr>
<td>{{ name }}</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>
{% endfor %}
</tbody>
</table>
<a href="{% url 'common:command-storage-create' %}" class="btn btn-primary btn-xs">{% trans 'Add' %}</a>
<div class="hr-line-dashed"></div>
<h3>{% trans "Replay storage" %}</h3>
<table class="table table-hover " id="task-history-list-table">
......@@ -89,6 +101,7 @@
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Type' %}</th>
<th>{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
......@@ -96,18 +109,14 @@
<tr>
<td>{{ name }}</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>
{% endfor %}
</tbody>
</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="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>
</div>
</div>
......@@ -116,40 +125,63 @@
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<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;
});
<script>
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) {
toastr.error(message)
}
function deleteStorage($this, the_url){
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) {
toastr.success(message.msg)
}
$(document).ready(function () {
APIUpdateAttr({
url: the_url,
body: JSON.stringify(data),
method: "POST",
flash_message: false,
success: success,
error: error
});
})
.on('click', '', function () {
})
.on('click', '.btn-del-replay', function(){
var $this = $(this);
var the_url = "{% url 'api-common:replay-storage-delete' %}";
deleteStorage($this, the_url);
})
.on('click', '.btn-del-command', function() {
var $this = $(this);
var the_url = "{% url 'api-common:command-storage-delete' %}";
deleteStorage($this, the_url)
});
})
</script>
</script>
{% endblock %}
......@@ -9,5 +9,9 @@ app_name = 'common'
urlpatterns = [
path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'),
path('ldap/testing/', api.LDAPTestingAPI.as_view(), name='ldap-testing'),
# path('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 = [
url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'),
url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-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'),
]
......@@ -37,7 +37,8 @@ def reverse(view_name, urlconf=None, args=None, kwargs=None,
kwargs=kwargs, current_app=current_app)
if external:
url = settings.SITE_URL.strip('/') + url
site_url = settings.SITE_URL
url = site_url.strip('/') + url
return url
......@@ -387,6 +388,20 @@ def get_request_ip(request):
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:
origin_stdout = sys.stdout
......
......@@ -2,12 +2,11 @@ from django.views.generic import TemplateView
from django.shortcuts import render, redirect
from django.contrib import messages
from django.utils.translation import ugettext as _
from django.conf import settings
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
TerminalSettingForm, SecuritySettingForm
from common.permissions import SuperUserRequiredMixin
from .signals import ldap_auth_enable
from . import utils
class BasicSettingView(SuperUserRequiredMixin, TemplateView):
......@@ -27,7 +26,7 @@ class BasicSettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST)
if form.is_valid():
form.save()
msg = _("Update setting successfully, please restart program")
msg = _("Update setting successfully")
messages.success(request, msg)
return redirect('settings:basic-setting')
else:
......@@ -79,9 +78,7 @@ class LDAPSettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST)
if form.is_valid():
form.save()
if "AUTH_LDAP" in form.cleaned_data:
ldap_auth_enable.send(sender=self.__class__, enabled=form.cleaned_data["AUTH_LDAP"])
msg = _("Update setting successfully, please restart program")
msg = _("Update setting successfully")
messages.success(request, msg)
return redirect('settings:ldap-setting')
else:
......@@ -95,14 +92,15 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
template_name = "common/terminal_setting.html"
def get_context_data(self, **kwargs):
command_storage = settings.TERMINAL_COMMAND_STORAGE
replay_storage = settings.TERMINAL_REPLAY_STORAGE
command_storage = utils.get_command_storage_setting()
replay_storage = utils.get_replay_storage_setting()
context = {
'app': _('Settings'),
'action': _('Terminal setting'),
'form': self.form_class(),
'replay_storage': replay_storage,
'command_storage': command_storage,
'command_storage': command_storage
}
kwargs.update(context)
return super().get_context_data(**kwargs)
......@@ -111,7 +109,7 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST)
if form.is_valid():
form.save()
msg = _("Update setting successfully, please restart program")
msg = _("Update setting successfully")
messages.success(request, msg)
return redirect('settings:terminal-setting')
else:
......@@ -120,6 +118,30 @@ class TerminalSettingView(SuperUserRequiredMixin, TemplateView):
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):
form_class = SecuritySettingForm
template_name = "common/security_setting.html"
......@@ -137,7 +159,7 @@ class SecuritySettingView(SuperUserRequiredMixin, TemplateView):
form = self.form_class(request.POST)
if form.is_valid():
form.save()
msg = _("Update setting successfully, please restart program")
msg = _("Update setting successfully")
messages.success(request, msg)
return redirect('settings:security-setting')
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
from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion
from django.urls import reverse_lazy
from .conf import load_user_config
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR)
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})()
CONFIG = load_user_config()
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
......@@ -42,16 +30,19 @@ except ImportError:
# SECURITY WARNING: keep the secret key used in production secret!
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!
DEBUG = CONFIG.DEBUG or False
DEBUG = CONFIG.DEBUG
# 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 = 'DEBUG' if DEBUG else CONFIG.LOG_LEVEL or 'WARNING'
LOG_LEVEL = CONFIG.LOG_LEVEL
ALLOWED_HOSTS = CONFIG.ALLOWED_HOSTS or []
ALLOWED_HOSTS = ['*']
# Application definition
......@@ -64,6 +55,7 @@ INSTALLED_APPS = [
'common.apps.CommonConfig',
'terminal.apps.TerminalConfig',
'audits.apps.AuditsConfig',
'authentication.apps.AuthenticationConfig', # authentication
'rest_framework',
'rest_framework_swagger',
'drf_yasg',
......@@ -94,6 +86,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'authentication.openid.middleware.OpenIDAuthenticationMiddleware', # openid
'jumpserver.middleware.TimezoneMiddleware',
'jumpserver.middleware.DemoMiddleware',
'jumpserver.middleware.RequestMiddleware',
......@@ -150,9 +143,9 @@ TEMPLATES = [
LOGIN_REDIRECT_URL = reverse_lazy('index')
LOGIN_URL = reverse_lazy('users:login')
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN or None
CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN or None
SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE or 3600 * 24
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN
SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
# Database
......@@ -315,13 +308,13 @@ MEDIA_ROOT = os.path.join(PROJECT_DIR, 'data', 'media').replace('\\', '/') + '/'
FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ]
# Email config
EMAIL_HOST = CONFIG.EMAIL_HOST
EMAIL_PORT = CONFIG.EMAIL_PORT
EMAIL_HOST_USER = CONFIG.EMAIL_HOST_USER
EMAIL_HOST_PASSWORD = CONFIG.EMAIL_HOST_PASSWORD
EMAIL_USE_SSL = CONFIG.EMAIL_USE_SSL
EMAIL_USE_TLS = CONFIG.EMAIL_USE_TLS
EMAIL_SUBJECT_PREFIX = CONFIG.EMAIL_SUBJECT_PREFIX or ''
EMAIL_HOST = 'smtp.jumpserver.org'
EMAIL_PORT = 25
EMAIL_HOST_USER = 'noreply@jumpserver.org'
EMAIL_HOST_PASSWORD = ''
EMAIL_USE_SSL = False
EMAIL_USE_TLS = False
EMAIL_SUBJECT_PREFIX = '[JMS] '
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
......@@ -361,23 +354,23 @@ FILE_UPLOAD_PERMISSIONS = 0o644
FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
# Auth LDAP settings
AUTH_LDAP = CONFIG.AUTH_LDAP
AUTH_LDAP_SERVER_URI = CONFIG.AUTH_LDAP_SERVER_URI
AUTH_LDAP_BIND_DN = CONFIG.AUTH_LDAP_BIND_DN
AUTH_LDAP_BIND_PASSWORD = CONFIG.AUTH_LDAP_BIND_PASSWORD
AUTH_LDAP_SEARCH_OU = CONFIG.AUTH_LDAP_SEARCH_OU
AUTH_LDAP_SEARCH_FILTER = CONFIG.AUTH_LDAP_SEARCH_FILTER
AUTH_LDAP_START_TLS = CONFIG.AUTH_LDAP_START_TLS
AUTH_LDAP_USER_ATTR_MAP = CONFIG.AUTH_LDAP_USER_ATTR_MAP
AUTH_LDAP_USER_SEARCH_UNION = [
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_START_TLS = False
AUTH_LDAP_USER_ATTR_MAP = {"username": "cn", "name": "sn", "email": "mail"}
AUTH_LDAP_USER_SEARCH_UNION = lambda: [
LDAPSearch(USER_SEARCH, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER)
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_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
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 = {
ldap.OPT_TIMEOUT: 5
......@@ -389,12 +382,30 @@ AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend'
if AUTH_LDAP:
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_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '',
'host': CONFIG.REDIS_HOST or '127.0.0.1',
'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_RESULT_SERIALIZER = 'pickle'
......@@ -416,10 +427,10 @@ CACHES = {
'default': {
'BACKEND': 'redis_cache.RedisCache',
'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
'password': CONFIG.REDIS_PASSWORD if CONFIG.REDIS_PASSWORD else '',
'host': CONFIG.REDIS_HOST or '127.0.0.1',
'port': CONFIG.REDIS_PORT or 6379,
'db': CONFIG.REDIS_DB_CACHE or 4,
'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT,
'db': CONFIG.REDIS_DB_CACHE,
}
}
}
......@@ -434,27 +445,45 @@ COMMAND_STORAGE = {
'ENGINE': 'terminal.backends.command.db',
}
TERMINAL_COMMAND_STORAGE = {
DEFAULT_TERMINAL_COMMAND_STORAGE = {
"default": {
"TYPE": "server",
},
}
TERMINAL_COMMAND_STORAGE = {
# 'ali-es': {
# 'TYPE': 'elasticsearch',
# 'HOSTS': ['http://elastic:changeme@localhost:9200'],
# },
}
TERMINAL_REPLAY_STORAGE = {
DEFAULT_TERMINAL_REPLAY_STORAGE = {
"default": {
"TYPE": "server",
},
}
TERMINAL_REPLAY_STORAGE = {
}
DEFAULT_PASSWORD_MIN_LENGTH = 6
DEFAULT_LOGIN_LIMIT_COUNT = 7
DEFAULT_LOGIN_LIMIT_TIME = 30 # Unit: minute
DEFAULT_SECURITY_MAX_IDLE_TIME = 30 # Unit: minute
SECURITY_MFA_AUTH = False
SECURITY_LOGIN_LIMIT_COUNT = 7
SECURITY_LOGIN_LIMIT_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
BOOTSTRAP3 = {
......@@ -466,16 +495,18 @@ BOOTSTRAP3 = {
'success_css_class': '',
}
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION or 3600
DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE or 25
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE
DEFAULT_EXPIRED_YEARS = 70
USER_GUIDE_URL = ""
SWAGGER_SETTINGS = {
'DEFAULT_AUTO_SCHEMA_CLASS': 'jumpserver.swagger.CustomSwaggerAutoSchema',
'SECURITY_DEFINITIONS': {
'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 ~*~
from __future__ import unicode_literals
import re
import os
from django.urls import path, include, re_path
from django.conf import settings
from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns
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 .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(
openapi.Info(
title="Jumpserver API Docs",
default_version='v1',
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')),
api_v2_patterns = [
path('api/', include([
path('terminal/v2/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')),
path('users/v2/', include('users.urls.api_urls_v2', namespace='api-users-v2')),
]))
]
app_view_patterns = [
......@@ -75,8 +39,10 @@ app_view_patterns = [
path('ops/', include('ops.urls.view_urls', namespace='ops')),
path('audits/', include('audits.urls.view_urls', namespace='audits')),
path('orgs/', include('orgs.urls.views_urls', namespace='orgs')),
path('auth/', include('authentication.urls.view_urls'), name='auth'),
]
if settings.XPACK_ENABLED:
app_view_patterns.append(path('xpack/', include('xpack.urls', namespace='xpack')))
......@@ -86,12 +52,13 @@ js_i18n_patterns = i18n_patterns(
urlpatterns = [
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('i18n/<str:lang>/', I18NView.as_view(), name='i18n-switch'),
path('settings/', include('common.urls.view_urls', namespace='settings')),
path('common/', include('common.urls.view_urls', namespace='common')),
path('api/v1/', redirect_format_api),
path('api/', include(v1_api_patterns)),
# path('api/v2/', include(api_v2_patterns)),
# External apps url
path('captcha/', include('captcha.urls')),
......@@ -103,7 +70,13 @@ urlpatterns += js_i18n_patterns
if settings.DEBUG:
urlpatterns += [
re_path('swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'),
path('docs/', schema_view.with_ui('swagger', cache_timeout=None), name="docs"),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=None), name='redoc'),
re_path('^swagger(?P<format>\.json|\.yaml)$',
get_swagger_view().without_ui(cache_timeout=1), name='schema-json'),
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 re
from django.http import HttpResponse, HttpResponseRedirect
from django.conf import settings
......@@ -8,6 +9,10 @@ from django.utils.translation import ugettext_lazy as _
from django.db.models import Count
from django.shortcuts import redirect
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 assets.models import Asset
......@@ -188,3 +193,29 @@ class I18NView(View):
response = HttpResponseRedirect(referer_url)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang)
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 ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\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"
"Last-Translator: ibuler <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: {}"
msgstr "测试节点下资产是否可连接: {}"
#: 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:191
#: assets/templates/assets/asset_detail.html:187
#: assets/templates/assets/asset_detail.html:195
#: assets/templates/assets/system_user_asset.html:95 perms/models.py:32
msgid "Nodes"
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:116 assets/models/asset.py:88
#: 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/templates/cloud/sync_instance_task_detail.html:67
#: xpack/plugins/orgs/templates/orgs/org_list.html:18
......@@ -110,11 +110,12 @@ msgstr "选择资产"
#: assets/templates/assets/domain_gateway_list.html:58
#: assets/templates/assets/system_user_asset.html:52
#: assets/templates/assets/user_asset_list.html:163
#: common/templates/common/replay_storage_create.html:60
msgid "Port"
msgstr "端口"
#: 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_list.html:26
#: assets/templates/assets/label_list.html:16
......@@ -125,7 +126,7 @@ msgstr "端口"
#: perms/templates/perms/asset_permission_create_update.html:40
#: perms/templates/perms/asset_permission_list.html:56
#: 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:73
#: terminal/templates/terminal/session_list.html:41
......@@ -153,17 +154,19 @@ msgstr "不能包含特殊字符"
#: assets/templates/assets/domain_list.html:25
#: assets/templates/assets/label_list.html:14
#: assets/templates/assets/system_user_detail.html:58
#: assets/templates/assets/system_user_list.html:29 common/models.py:30
#: common/templates/common/terminal_setting.html:72
#: common/templates/common/terminal_setting.html:90 ops/models/adhoc.py:37
#: assets/templates/assets/system_user_list.html:29 common/models.py:29
#: common/templates/common/command_storage_create.html:41
#: 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
#: orgs/models.py:12 perms/models.py:28
#: perms/templates/perms/asset_permission_detail.html:62
#: perms/templates/perms/asset_permission_list.html:53
#: 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
#: 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_group_detail.html:55
#: users/templates/users/user_group_list.html:12
......@@ -188,9 +191,9 @@ msgstr "名称"
#: assets/templates/assets/system_user_list.html:30
#: audits/templates/audits/login_log_list.html:49
#: 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/login.html:62
#: users/templates/users/login.html:64
#: users/templates/users/user_detail.html:67
#: users/templates/users/user_list.html:24
#: users/templates/users/user_profile.html:47
......@@ -201,9 +204,9 @@ msgstr "用户名"
msgid "Password or private key passphrase"
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/templates/users/login.html:65
#: users/templates/users/login.html:67
#: users/templates/users/reset_password.html:53
#: users/templates/users/user_create.html:10
#: users/templates/users/user_password_authentication.html:18
......@@ -213,7 +216,7 @@ msgstr "密码或密钥密码"
msgid "Password"
msgstr "密码"
#: assets/forms/user.py:29 users/models/user.py:78
#: assets/forms/user.py:29 users/models/user.py:80
msgid "Private key"
msgstr "ssh私钥"
......@@ -243,7 +246,9 @@ msgstr "自动推送系统用户到资产"
msgid ""
"1-100, High level will be using login asset as default, if user was granted "
"more than 2 system user"
msgstr "1-100, 1最低优先级,100最高优先级。授权多个用户时,高优先级的系统用户将会作为默认登录用户"
msgstr ""
"1-100, 1最低优先级,100最高优先级。授权多个用户时,高优先级的系统用户将会作为"
"默认登录用户"
#: assets/forms/user.py:155
msgid ""
......@@ -260,7 +265,7 @@ msgstr "如果选择手动登录模式,用户名和密码可以不填写"
#: assets/templates/assets/system_user_asset.html:51
#: assets/templates/assets/user_asset_list.html:46
#: 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
#: users/templates/users/user_granted_asset.html:45
#: users/templates/users/user_group_granted_asset.html:45
......@@ -273,7 +278,7 @@ msgstr "IP"
#: assets/templates/assets/asset_list.html:92
#: assets/templates/assets/system_user_asset.html:50
#: 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
#: users/templates/users/user_granted_asset.html:44
#: users/templates/users/user_group_granted_asset.html:44
......@@ -281,7 +286,7 @@ msgid "Hostname"
msgstr "主机名"
#: 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/system_user_detail.html:70
#: assets/templates/assets/system_user_list.html:31
......@@ -290,14 +295,14 @@ msgstr "主机名"
msgid "Protocol"
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
msgid "Platform"
msgstr "系统平台"
#: assets/models/asset.py:84 assets/models/cmd_filter.py:20
#: 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
msgid "Is active"
msgstr "激活"
......@@ -306,19 +311,19 @@ msgstr "激活"
msgid "Public 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"
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"
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"
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"
msgstr "序列号"
......@@ -338,7 +343,7 @@ msgstr "CPU核数"
msgid "CPU vcpus"
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"
msgstr "内存"
......@@ -350,7 +355,7 @@ msgstr "硬盘大小"
msgid "Disk info"
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
msgid "OS"
msgstr "操作系统"
......@@ -368,7 +373,7 @@ msgid "Hostname raw"
msgstr "主机名原始"
#: 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
msgid "Labels"
msgstr "标签管理"
......@@ -377,13 +382,13 @@ msgstr "标签管理"
#: assets/models/cluster.py:28 assets/models/cmd_filter.py:24
#: assets/models/cmd_filter.py:54 assets/models/group.py:21
#: 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/domain_detail.html:72
#: assets/templates/assets/system_user_detail.html:100
#: 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
#: 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
msgid "Created by"
msgstr "创建者"
......@@ -412,7 +417,7 @@ msgstr "创建日期"
#: assets/models/domain.py:51 assets/models/group.py:23
#: assets/models/label.py:22 assets/templates/assets/admin_user_detail.html:72
#: 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_list.html:27
#: assets/templates/assets/cmd_filter_rule_list.html:62
......@@ -421,15 +426,15 @@ msgstr "创建日期"
#: assets/templates/assets/domain_list.html:28
#: assets/templates/assets/system_user_detail.html:104
#: 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
#: perms/models.py:86 perms/templates/perms/asset_permission_detail.html:102
#: terminal/models.py:28 terminal/templates/terminal/terminal_detail.html:63
#: users/models/group.py:15 users/models/user.py:84
#: users/templates/users/user_detail.html:123
#: users/models/group.py:15 users/models/user.py:86
#: users/templates/users/user_detail.html:127
#: users/templates/users/user_group_detail.html:67
#: 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/templates/cloud/account_detail.html:72
#: xpack/plugins/cloud/templates/cloud/account_list.html:15
......@@ -456,7 +461,7 @@ msgstr "带宽"
msgid "Contact"
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
msgid "Phone"
msgstr "手机"
......@@ -482,7 +487,7 @@ msgid "Default"
msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14
#: users/models/user.py:363
#: users/models/user.py:422
msgid "System"
msgstr "系统"
......@@ -510,7 +515,7 @@ msgstr "BGP全网通"
msgid "Regex"
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:71
#: terminal/templates/terminal/session_detail.html:48
......@@ -533,8 +538,10 @@ msgstr "过滤器"
#: assets/models/cmd_filter.py:46
#: assets/templates/assets/cmd_filter_rule_list.html:58
#: audits/templates/audits/login_log_list.html:50
#: common/templates/common/terminal_setting.html:73
#: common/templates/common/terminal_setting.html:91
#: common/templates/common/command_storage_create.html:31
#: common/templates/common/replay_storage_create.html:31
#: common/templates/common/terminal_setting.html:81
#: common/templates/common/terminal_setting.html:103
msgid "Type"
msgstr "类型"
......@@ -568,6 +575,8 @@ msgstr "每行一个命令"
#: 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: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/task_history.html:65 ops/templates/ops/task_list.html:42
#: perms/templates/perms/asset_permission_list.html:60
......@@ -607,14 +616,14 @@ msgstr "默认资产组"
#: perms/templates/perms/asset_permission_create_update.html:36
#: perms/templates/perms/asset_permission_list.html:54
#: 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:72
#: terminal/templates/terminal/session_list.html:33
#: terminal/templates/terminal/session_list.html:71 users/forms.py:312
#: users/models/user.py:33 users/models/user.py:351
#: terminal/templates/terminal/session_list.html:71 users/forms.py:310
#: users/models/user.py:33 users/models/user.py:410
#: 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/templates/orgs/org_detail.html:113
#: xpack/plugins/orgs/templates/orgs/org_list.html:14
......@@ -622,7 +631,7 @@ msgid "User"
msgstr "用户"
#: 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"
msgstr "值"
......@@ -657,8 +666,8 @@ msgstr "手动登录"
#: assets/views/domain.py:29 assets/views/domain.py:45
#: assets/views/domain.py:61 assets/views/domain.py:74
#: 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/label.py:58 assets/views/system_user.py:28
#: assets/views/domain.py:145 assets/views/label.py:26 assets/views/label.py:43
#: 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:74 templates/_nav.html:19
msgid "Assets"
......@@ -690,7 +699,7 @@ msgstr "登录模式"
#: perms/templates/perms/asset_permission_detail.html:140
#: perms/templates/perms/asset_permission_list.html:58
#: 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:74
#: terminal/templates/terminal/session_list.html:49
......@@ -843,17 +852,19 @@ msgstr "其它"
#: assets/templates/assets/gateway_create_update.html:58
#: assets/templates/assets/label_create_update.html:18
#: 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/ldap_setting.html:62
#: common/templates/common/replay_storage_create.html:139
#: 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
#: terminal/templates/terminal/terminal_update.html:47
#: users/templates/users/_user.html:46
#: 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_profile.html:198
#: users/templates/users/user_profile.html:202
#: users/templates/users/user_profile_update.html:63
#: users/templates/users/user_pubkey_update.html:70
#: users/templates/users/user_pubkey_update.html:76
......@@ -874,10 +885,12 @@ msgstr "重置"
#: assets/templates/assets/gateway_create_update.html:59
#: assets/templates/assets/label_create_update.html:19
#: 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/ldap_setting.html:63
#: common/templates/common/replay_storage_create.html:140
#: 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
#: terminal/templates/terminal/command_list.html:103
#: terminal/templates/terminal/session_list.html:127
......@@ -945,12 +958,12 @@ msgid "Quick update"
msgstr "快速更新"
#: 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"
msgstr "测试可连接性"
#: 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:161
#: assets/templates/assets/system_user_detail.html:151
......@@ -979,9 +992,9 @@ msgstr "测试"
#: users/templates/users/user_group_detail.html:28
#: users/templates/users/user_group_list.html:43
#: users/templates/users/user_list.html:77
#: users/templates/users/user_profile.html:151
#: users/templates/users/user_profile.html:181
#: users/templates/users/user_profile.html:190
#: users/templates/users/user_profile.html:155
#: users/templates/users/user_profile.html:185
#: users/templates/users/user_profile.html:194
#: xpack/plugins/cloud/templates/cloud/account_detail.html:25
#: xpack/plugins/cloud/templates/cloud/account_list.html:38
#: xpack/plugins/orgs/templates/orgs/org_detail.html:25
......@@ -1003,6 +1016,8 @@ msgstr "更新"
#: assets/templates/assets/label_list.html:39
#: assets/templates/assets/system_user_detail.html:30
#: 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
#: perms/templates/perms/asset_permission_detail.html:34
#: perms/templates/perms/asset_permission_list.html:201
......@@ -1031,21 +1046,22 @@ msgid "Select nodes"
msgstr "选择节点"
#: 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/cmd_filter_detail.html:106
#: assets/templates/assets/system_user_asset.html:112
#: 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
#: users/templates/users/user_detail.html:382
#: users/templates/users/user_detail.html:408
#: users/templates/users/user_detail.html:431
#: users/templates/users/user_detail.html:476
#: users/templates/users/user_detail.html:386
#: users/templates/users/user_detail.html:412
#: users/templates/users/user_detail.html:435
#: users/templates/users/user_detail.html:480
#: users/templates/users/user_group_create_update.html:32
#: users/templates/users/user_group_list.html:87
#: users/templates/users/user_list.html:201
#: users/templates/users/user_profile.html:232
#: users/templates/users/user_group_list.html:88
#: users/templates/users/user_list.html:205
#: users/templates/users/user_profile.html:236
#: xpack/plugins/cloud/templates/cloud/account_create_update.html:34
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create.html:36
#: xpack/plugins/orgs/templates/orgs/org_create_update.html:33
......@@ -1096,28 +1112,28 @@ msgstr "选择需要修改属性"
msgid "Select all"
msgstr "全选"
#: assets/templates/assets/asset_detail.html:85
#: assets/templates/assets/asset_detail.html:89
msgid "CPU"
msgstr "CPU"
#: assets/templates/assets/asset_detail.html:93
#: assets/templates/assets/asset_detail.html:97
msgid "Disk"
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_profile.html:104
msgid "Date joined"
msgstr "创建日期"
#: assets/templates/assets/asset_detail.html:137
#: assets/templates/assets/asset_detail.html:141
#: terminal/templates/terminal/session_detail.html:81
#: users/templates/users/user_detail.html:134
#: users/templates/users/user_profile.html:142
#: users/templates/users/user_detail.html:138
#: users/templates/users/user_profile.html:146
msgid "Quick modify"
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/user_asset_list.html:47 perms/models.py:34
#: perms/models.py:82
......@@ -1126,7 +1142,7 @@ msgstr "快速修改"
#: perms/templates/perms/asset_permission_list.html:59
#: terminal/templates/terminal/terminal_list.html:34
#: 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_group_granted_asset.html:46
#: users/templates/users/user_list.html:28
......@@ -1134,17 +1150,17 @@ msgstr "快速修改"
msgid "Active"
msgstr "激活中"
#: assets/templates/assets/asset_detail.html:160
#: assets/templates/assets/asset_detail.html:164
msgid "Refresh hardware"
msgstr "更新硬件信息"
#: assets/templates/assets/asset_detail.html:163
#: assets/templates/assets/asset_detail.html:167
msgid "Refresh"
msgstr "刷新"
#: assets/templates/assets/asset_detail.html:300
#: users/templates/users/user_detail.html:301
#: users/templates/users/user_detail.html:328
#: assets/templates/assets/asset_detail.html:304
#: users/templates/users/user_detail.html:305
#: users/templates/users/user_detail.html:332
msgid "Update successfully!"
msgstr "更新成功"
......@@ -1257,11 +1273,11 @@ msgstr "重命名失败,不能更改root节点的名称"
#: assets/templates/assets/asset_list.html:627
#: assets/templates/assets/system_user_list.html:137
#: users/templates/users/user_detail.html:376
#: users/templates/users/user_detail.html:402
#: users/templates/users/user_detail.html:470
#: users/templates/users/user_group_list.html:81
#: users/templates/users/user_list.html:195
#: users/templates/users/user_detail.html:380
#: users/templates/users/user_detail.html:406
#: users/templates/users/user_detail.html:474
#: users/templates/users/user_group_list.html:82
#: users/templates/users/user_list.html:199
msgid "Are you sure?"
msgstr "你确认吗?"
......@@ -1271,12 +1287,13 @@ msgstr "删除选择资产"
#: assets/templates/assets/asset_list.html:631
#: assets/templates/assets/system_user_list.html:141
#: users/templates/users/user_detail.html:380
#: users/templates/users/user_detail.html:406
#: users/templates/users/user_detail.html:474
#: common/templates/common/terminal_setting.html:163
#: users/templates/users/user_detail.html:384
#: 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_list.html:85
#: users/templates/users/user_list.html:199
#: users/templates/users/user_group_list.html:86
#: users/templates/users/user_list.html:203
#: xpack/plugins/orgs/templates/orgs/org_create_update.html:32
msgid "Cancel"
msgstr "取消"
......@@ -1346,7 +1363,7 @@ msgstr "创建命令过滤器"
#: assets/templates/assets/cmd_filter_rule_list.html:33
#: assets/views/cmd_filter.py:98
msgid "Command filter rule list"
msgstr "命令过滤器列表"
msgstr "命令过滤器规则列表"
#: assets/templates/assets/cmd_filter_rule_list.html:50
msgid "Create rule"
......@@ -1407,7 +1424,7 @@ msgstr "JMS => 网域网关 => 目标资产"
msgid "Create domain"
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"
msgstr "创建标签"
......@@ -1527,7 +1544,7 @@ msgstr "批量更新资产"
msgid "Update asset"
msgstr "更新资产"
#: assets/views/asset.py:291
#: assets/views/asset.py:292
msgid "already exists"
msgstr "已经存在"
......@@ -1575,7 +1592,11 @@ msgstr "创建网关"
msgid "Label list"
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"
msgstr "更新标签"
......@@ -1603,7 +1624,7 @@ msgstr "系统用户集群资产"
#: audits/templates/audits/ftp_log_list.html:73
#: audits/templates/audits/operate_log_list.html:70
#: 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
msgid "Remote addr"
msgstr "远端地址"
......@@ -1618,8 +1639,8 @@ msgid "Filename"
msgstr "文件名"
#: audits/models.py:22 audits/templates/audits/ftp_log_list.html:76
#: ops/templates/ops/task_list.html:39 users/models/authentication.py:66
#: users/templates/users/user_detail.html:452 xpack/plugins/cloud/api.py:61
#: ops/templates/ops/task_list.html:39 users/models/authentication.py:73
#: users/templates/users/user_detail.html:456 xpack/plugins/cloud/api.py:61
msgid "Success"
msgstr "成功"
......@@ -1644,7 +1665,7 @@ msgstr "修改者"
#: ops/templates/ops/adhoc_history.html:52
#: ops/templates/ops/adhoc_history_detail.html:61
#: 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
msgid "Date start"
msgstr "开始日期"
......@@ -1685,20 +1706,20 @@ msgstr "Agent"
msgid "City"
msgstr "城市"
#: audits/templates/audits/login_log_list.html:54 users/forms.py:169
#: users/models/authentication.py:75 users/models/user.py:73
#: audits/templates/audits/login_log_list.html:54 users/forms.py:168
#: users/models/authentication.py:82 users/models/user.py:75
#: users/templates/users/first_login.html:45
msgid "MFA"
msgstr "MFA"
#: 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
msgid "Reason"
msgstr "原因"
#: 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/templates/cloud/sync_instance_task_history.html:70
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:67
......@@ -1735,34 +1756,47 @@ msgstr "操作日志"
msgid "Password change log"
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:92 users/views/login.py:327 users/views/user.py:68
#: users/views/user.py:83 users/views/user.py:111 users/views/user.py:193
#: users/views/user.py:354 users/views/user.py:404 users/views/user.py:439
#: 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:192
#: users/views/user.py:353 users/views/user.py:403 users/views/user.py:437
msgid "Users"
msgstr "用户管理"
#: audits/views.py:184 templates/_nav.html:76
#: audits/views.py:188 templates/_nav.html:76
msgid "Login log"
msgstr "登录日志"
#: common/api.py:18
#: common/api.py:22
msgid "Test mail sent to {}, please check"
msgstr "邮件已经发送{}, 请检查"
#: common/api.py:42
#: common/api.py:46
msgid "Test ldap success"
msgstr "连接LDAP成功"
#: common/api.py:72
#: common/api.py:76
msgid "Search no entry matched in ou {}"
msgstr "在ou:{}中没有匹配条目"
#: common/api.py:81
#: common/api.py:85
msgid "Match {} s users"
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
#, python-format
msgid "<b>%(name)s</b> was created successfully"
......@@ -1785,88 +1819,92 @@ msgstr "不是字符类型"
msgid "Encrypt field using Secret Key"
msgstr ""
#: common/forms.py:64
#: common/forms.py:63
msgid "Current SITE URL"
msgstr "当前站点URL"
#: common/forms.py:68
#: common/forms.py:67
msgid "User Guide URL"
msgstr "用户向导URL"
#: common/forms.py:69
#: common/forms.py:68
msgid "User first login update profile done redirect to it"
msgstr "用户第一次登录,修改profile后重定向到地址"
#: common/forms.py:72
#: common/forms.py:71
msgid "Email Subject Prefix"
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"
msgstr "SMTP主机"
#: common/forms.py:81
#: common/forms.py:80
msgid "SMTP port"
msgstr "SMTP端口"
#: common/forms.py:83
#: common/forms.py:82
msgid "SMTP user"
msgstr "SMTP账号"
#: common/forms.py:86
#: common/forms.py:85
msgid "SMTP password"
msgstr "SMTP密码"
#: common/forms.py:87
#: common/forms.py:86
msgid "Some provider use token except password"
msgstr "一些邮件提供商需要输入的是Token"
#: common/forms.py:90 common/forms.py:128
#: common/forms.py:89 common/forms.py:127
msgid "Use SSL"
msgstr "使用SSL"
#: common/forms.py:91
#: common/forms.py:90
msgid "If SMTP port is 465, may be select"
msgstr "如果SMTP端口是465,通常需要启用SSL"
#: common/forms.py:94
#: common/forms.py:93
msgid "Use TLS"
msgstr "使用TLS"
#: common/forms.py:95
#: common/forms.py:94
msgid "If SMTP port is 587, may be select"
msgstr "如果SMTP端口是587,通常需要启用TLS"
#: common/forms.py:101
#: common/forms.py:100
msgid "LDAP server"
msgstr "LDAP地址"
#: common/forms.py:104
#: common/forms.py:103
msgid "Bind DN"
msgstr "绑定DN"
#: common/forms.py:111
#: common/forms.py:110
msgid "User OU"
msgstr "用户OU"
#: common/forms.py:112
#: common/forms.py:111
msgid "Use | split User OUs"
msgstr "使用|分隔各OU"
#: common/forms.py:115
#: common/forms.py:114
msgid "User search filter"
msgstr "用户过滤器"
#: common/forms.py:116
#: common/forms.py:115
#, python-format
msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)"
msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)"
#: common/forms.py:119
#: common/forms.py:118
msgid "User attr map"
msgstr "LDAP属性映射"
#: common/forms.py:121
#: common/forms.py:120
msgid ""
"User attr map present how to map LDAP user attr to jumpserver, username,name,"
"email is jumpserver attr"
......@@ -1874,125 +1912,115 @@ msgstr ""
"用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name,"
"email 是jumpserver的属性"
#: common/forms.py:130
#: common/forms.py:129
msgid "Enable LDAP auth"
msgstr "启用LDAP认证"
#: common/forms.py:139
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
#: common/forms.py:138
msgid "Password auth"
msgstr "密码认证"
#: common/forms.py:148
#: common/forms.py:141
msgid "Public key auth"
msgstr "密钥认证"
#: common/forms.py:151 common/templates/common/terminal_setting.html:68
#: terminal/forms.py:30 terminal/models.py:22
msgid "Command storage"
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:144
msgid "Heartbeat interval"
msgstr "心跳间隔"
#: common/forms.py:157 common/templates/common/terminal_setting.html:86
#: terminal/forms.py:35 terminal/models.py:23
msgid "Replay storage"
msgstr "录像存储"
#: common/forms.py:144 ops/models/adhoc.py:38
msgid "Units: seconds"
msgstr "单位: 秒"
#: common/forms.py:158
msgid ""
"Set replay storage setting, `default` is the using as default,You can set "
"other storage and some terminal using"
msgstr "设置终端录像存储,default是默认用的存储方式"
#: common/forms.py:147
msgid "List sort by"
msgstr "资产列表排序"
#: common/forms.py:168
#: common/forms.py:159
msgid "MFA Secondary certification"
msgstr "MFA 二次认证"
#: common/forms.py:170
#: common/forms.py:161
msgid ""
"After opening, the user login must use MFA secondary authentication (valid "
"for all users, including administrators)"
msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)"
#: common/forms.py:177
#: common/forms.py:168
msgid "Limit the number of login failures"
msgstr "限制登录失败次数"
#: common/forms.py:182
#: common/forms.py:173
msgid "No logon interval"
msgstr "禁止登录时间间隔"
#: common/forms.py:184
#: common/forms.py:175
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."
msgstr ""
"提示: (单位: 分钟) 当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录."
#: common/forms.py:190
#: common/forms.py:182
msgid "Connection max idle time"
msgstr "SSH最大空闲时间"
#: common/forms.py:192
#: common/forms.py:184
msgid ""
"If idle time more than it, disconnect connection(only ssh now) Unit: minute"
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"
msgstr "密码最小长度 "
#: common/forms.py:204
#: common/forms.py:208
msgid "Must contain capital letters"
msgstr "必须包含大写字母"
#: common/forms.py:206
#: common/forms.py:210
msgid ""
"After opening, the user password changes and resets must contain uppercase "
"letters"
msgstr "开启后,用户密码修改、重置必须包含大写字母"
#: common/forms.py:212
#: common/forms.py:216
msgid "Must contain lowercase letters"
msgstr "必须包含小写字母"
#: common/forms.py:213
#: common/forms.py:217
msgid ""
"After opening, the user password changes and resets must contain lowercase "
"letters"
msgstr "开启后,用户密码修改、重置必须包含小写字母"
#: common/forms.py:219
#: common/forms.py:223
msgid "Must contain numeric characters"
msgstr "必须包含数字字符"
#: common/forms.py:220
#: common/forms.py:224
msgid ""
"After opening, the user password changes and resets must contain numeric "
"characters"
msgstr "开启后,用户密码修改、重置必须包含数字字符"
#: common/forms.py:226
#: common/forms.py:230
msgid "Must contain special characters"
msgstr "必须包含特殊字符"
#: common/forms.py:227
#: common/forms.py:231
msgid ""
"After opening, the user password changes and resets must contain special "
"characters"
......@@ -2006,7 +2034,7 @@ msgstr ""
msgid "discard time"
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
msgid "Enabled"
msgstr "启用"
......@@ -2016,7 +2044,7 @@ msgstr "启用"
#: common/templates/common/ldap_setting.html:15
#: common/templates/common/security_setting.html:15
#: 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"
msgstr "基本设置"
......@@ -2024,7 +2052,7 @@ msgstr "基本设置"
#: common/templates/common/email_setting.html:18
#: common/templates/common/ldap_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"
msgstr "邮件设置"
......@@ -2032,7 +2060,7 @@ msgstr "邮件设置"
#: common/templates/common/email_setting.html:21
#: common/templates/common/ldap_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"
msgstr "LDAP设置"
......@@ -2040,7 +2068,7 @@ msgstr "LDAP设置"
#: common/templates/common/email_setting.html:24
#: common/templates/common/ldap_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"
msgstr "终端设置"
......@@ -2048,10 +2076,72 @@ msgstr "终端设置"
#: common/templates/common/email_setting.html:27
#: common/templates/common/ldap_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"
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
msgid "User login settings"
msgstr "用户登录设置"
......@@ -2060,21 +2150,66 @@ msgstr "用户登录设置"
msgid "Password check rule"
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
msgid "Special char not allowed"
msgstr "不能包含特殊字符"
#: common/views.py:19 common/views.py:45 common/views.py:71 common/views.py:101
#: common/views.py:129 templates/_nav.html:116
#: common/views.py:18 common/views.py:44 common/views.py:70 common/views.py:99
#: common/views.py:126 common/views.py:138 common/views.py:151
#: templates/_nav.html:116
msgid "Settings"
msgstr "系统设置"
#: common/views.py:30 common/views.py:56 common/views.py:84 common/views.py:114
#: common/views.py:140
#: common/views.py:29 common/views.py:81 common/views.py:112
#: 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"
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 ""
"<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, "
......@@ -2117,11 +2252,6 @@ msgstr "模式"
msgid "Options"
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
msgid "Run as admin"
msgstr "再次执行"
......@@ -2330,16 +2460,16 @@ msgstr "任务列表"
msgid "Task run history"
msgstr "执行历史"
#: orgs/mixins.py:78 orgs/models.py:24
#: orgs/mixins.py:77 orgs/models.py:24
msgid "Organization"
msgstr "组织管理"
#: 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: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/user_detail.html:207
#: users/templates/users/user_detail.html:211
#: users/templates/users/user_list.html:26
#: xpack/plugins/orgs/templates/orgs/org_list.html:15
msgid "User group"
......@@ -2355,8 +2485,8 @@ msgstr "资产和节点至少选一个"
#: perms/models.py:36 perms/models.py:83
#: perms/templates/perms/asset_permission_detail.html:90
#: users/models/user.py:89 users/templates/users/user_detail.html:107
#: users/templates/users/user_profile.html:112
#: users/models/user.py:91 users/templates/users/user_detail.html:107
#: users/templates/users/user_profile.html:116
msgid "Date expired"
msgstr "失效日期"
......@@ -2373,29 +2503,19 @@ msgstr "用户或用户组"
#: perms/templates/perms/asset_permission_asset.html:27
#: perms/templates/perms/asset_permission_detail.html:27
#: perms/templates/perms/asset_permission_user.html:27
msgid "Assets and asset groups"
msgstr "资产或资产组"
msgid "Assets and node"
msgstr "资产或节点"
#: perms/templates/perms/asset_permission_asset.html:80
msgid "Add asset to this permission"
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
msgid "Add node to this permission"
msgstr "添加节点"
#: perms/templates/perms/asset_permission_asset.html:125
#: users/templates/users/user_detail.html:224
#: users/templates/users/user_detail.html:228
msgid "Join"
msgstr "加入"
......@@ -2489,14 +2609,14 @@ msgstr "文档"
msgid "Commercial support"
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/first_login.html:39
#: users/templates/users/user_password_update.html:40
#: users/templates/users/user_profile.html:17
#: users/templates/users/user_profile_update.html:37
#: 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"
msgstr "个人信息"
......@@ -2513,7 +2633,7 @@ msgid "Logout"
msgstr "注销登录"
#: 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"
msgstr "登录"
......@@ -2521,7 +2641,41 @@ msgstr "登录"
msgid "Dashboard"
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
msgid ""
"\n"
......@@ -2534,7 +2688,7 @@ msgstr ""
"</a> 补充完整\n"
" "
#: templates/_message.html:20
#: templates/_message.html:40
#, python-format
msgid ""
"\n"
......@@ -2665,10 +2819,6 @@ msgid ""
"assets per user host per month, respectively."
msgstr "以下图形分别描述一个月活跃用户和资产占所有用户主机的百分比"
#: templates/index.html:91
msgid "Host"
msgstr "主机"
#: templates/index.html:106 templates/index.html:121
msgid "Top 10 assets in a week"
msgstr "一周Top10资产"
......@@ -2787,12 +2937,12 @@ msgstr "输入"
msgid "Session"
msgstr "会话"
#: terminal/forms.py:31
#: terminal/forms.py:28
msgid "Command can store in server db or ES, default to server, more see docs"
msgstr ""
"命令支持存储到服务器端数据库、ES中,默认存储的服务器端数据库,更多查看文档"
#: terminal/forms.py:36
#: terminal/forms.py:33
msgid ""
"Replay file can store in server disk, AWS S3, Aliyun OSS, default to server, "
"more see docs"
......@@ -2812,43 +2962,43 @@ msgstr "SSH端口"
msgid "HTTP Port"
msgstr "HTTP端口"
#: terminal/models.py:104
#: terminal/models.py:109
msgid "Session Online"
msgstr "在线会话"
#: terminal/models.py:105
#: terminal/models.py:110
msgid "CPU Usage"
msgstr "CPU使用"
#: terminal/models.py:106
#: terminal/models.py:111
msgid "Memory Used"
msgstr "内存使用"
#: terminal/models.py:107
#: terminal/models.py:112
msgid "Connections"
msgstr "连接数"
#: terminal/models.py:108
#: terminal/models.py:113
msgid "Threads"
msgstr "线程数"
#: terminal/models.py:109
#: terminal/models.py:114
msgid "Boot Time"
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"
msgstr "回放"
#: terminal/models.py:142
#: terminal/models.py:147
msgid "Date last active"
msgstr "最后活跃日期"
#: terminal/models.py:144
#: terminal/models.py:149
msgid "Date end"
msgstr "结束日期"
#: terminal/models.py:161
#: terminal/models.py:166
msgid "Args"
msgstr "参数"
......@@ -2994,70 +3144,74 @@ msgstr "你可以使用ssh客户端工具连接终端"
msgid "Log in frequently and try again later"
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"
msgstr "请携带seed值, 进行MFA二次认证"
#: users/api/auth.py:192
#: users/api/auth.py:204
msgid "Please verify the user name and password first"
msgstr "请先进行用户名和密码验证"
#: users/api/auth.py:204
#: users/api/auth.py:216
msgid "MFA certification failed"
msgstr "MFA认证失败"
#: users/api/user.py:135
#: users/api/user.py:140
msgid "Could not reset self otp, use profile reset instead"
msgstr "不能再该页面重置MFA, 请去个人信息页面重置"
#: users/authentication.py:56
#: users/authentication.py:53
msgid "Invalid signature header. No credentials provided."
msgstr ""
#: users/authentication.py:59
#: users/authentication.py:56
msgid "Invalid signature header. Signature string should not contain spaces."
msgstr ""
#: users/authentication.py:66
#: users/authentication.py:63
msgid "Invalid signature header. Format like AccessKeyId:Signature"
msgstr ""
#: users/authentication.py:70
#: users/authentication.py:67
msgid ""
"Invalid signature header. Signature string should not contain invalid "
"characters."
msgstr ""
#: users/authentication.py:90 users/authentication.py:106
#: users/authentication.py:87 users/authentication.py:103
msgid "Invalid signature."
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"
msgstr ""
#: users/authentication.py:102
#: users/authentication.py:99
msgid "Expired, more than 15 minutes"
msgstr ""
#: users/authentication.py:109
#: users/authentication.py:106
msgid "User disabled."
msgstr "用户已禁用"
#: users/authentication.py:124
#: users/authentication.py:121
msgid "Invalid token header. No credentials provided."
msgstr ""
#: users/authentication.py:127
#: users/authentication.py:124
msgid "Invalid token header. Sign string should not contain spaces."
msgstr ""
#: users/authentication.py:134
#: users/authentication.py:131
msgid ""
"Invalid token header. Sign string should not contain invalid characters."
msgstr ""
#: users/authentication.py:145
#: users/authentication.py:142
msgid "Invalid token or cache refreshed."
msgstr ""
......@@ -3065,7 +3219,7 @@ msgstr ""
msgid "MFA code"
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/user_detail.html:87
#: users/templates/users/user_list.html:25
......@@ -3073,11 +3227,11 @@ msgstr "MFA 验证码"
msgid "Role"
msgstr "角色"
#: users/forms.py:55 users/forms.py:228
#: users/forms.py:55 users/forms.py:226
msgid "ssh public key"
msgstr "ssh公钥"
#: users/forms.py:56 users/forms.py:229
#: users/forms.py:56 users/forms.py:227
msgid "ssh-rsa AAAA..."
msgstr ""
......@@ -3085,19 +3239,19 @@ msgstr ""
msgid "Paste user id_rsa.pub here."
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"
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."
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"
msgstr "ssh密钥不合法"
#: users/forms.py:154
#: users/forms.py:153
msgid ""
"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 "
......@@ -3106,11 +3260,11 @@ msgstr ""
"提示:启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修"
"改->更改MFA设置)中直接绑定!"
#: users/forms.py:164
#: users/forms.py:163
msgid "* Enable MFA authentication to make the account more secure."
msgstr "* 启用MFA认证,使账号更加安全."
#: users/forms.py:174
#: users/forms.py:173
msgid ""
"In order to protect you and your company, please keep your account, password "
"and key sensitive information properly. (for example: setting complex "
......@@ -3119,41 +3273,41 @@ msgstr ""
"为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:"
"设置复杂密码,启用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:130
msgid "Finish"
msgstr "完成"
#: users/forms.py:187
#: users/forms.py:186
msgid "Old password"
msgstr "原来密码"
#: users/forms.py:192
#: users/forms.py:191
msgid "New password"
msgstr "新密码"
#: users/forms.py:197
#: users/forms.py:196
msgid "Confirm password"
msgstr "确认密码"
#: users/forms.py:207
#: users/forms.py:206
msgid "Old password error"
msgstr "原来密码错误"
#: users/forms.py:215
#: users/forms.py:214
msgid "Password does not match"
msgstr "密码不一致"
#: users/forms.py:226
#: users/forms.py:224
msgid "Automatically configure and download the SSH key"
msgstr "自动配置并下载SSH密钥"
#: users/forms.py:230
#: users/forms.py:228
msgid "Paste your id_rsa.pub here."
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/user_password_update.html:46
#: users/templates/users/user_profile.html:68
......@@ -3162,57 +3316,65 @@ msgstr "复制你的公钥到这里"
msgid "Public key"
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
msgid "Select users"
msgstr "选择用户"
#: users/models/authentication.py:36
#: users/models/authentication.py:39
msgid "Private Token"
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"
msgstr "禁用"
#: users/models/authentication.py:52 users/models/authentication.py:60
#: users/models/authentication.py:55 users/models/authentication.py:65
msgid "-"
msgstr ""
#: users/models/authentication.py:61
#: users/models/authentication.py:66
msgid "Username/password check failed"
msgstr "用户名/密码 校验失败"
#: users/models/authentication.py:62
#: users/models/authentication.py:67
msgid "MFA authentication failed"
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
msgid "Failed"
msgstr "失败"
#: users/models/authentication.py:71
#: users/models/authentication.py:78
msgid "Login type"
msgstr "登录方式"
#: users/models/authentication.py:72
#: users/models/authentication.py:79
msgid "Login ip"
msgstr "登录IP"
#: users/models/authentication.py:73
#: users/models/authentication.py:80
msgid "Login city"
msgstr "登录城市"
#: users/models/authentication.py:74
#: users/models/authentication.py:81
msgid "User agent"
msgstr "Agent"
#: users/models/authentication.py:78
#: users/models/authentication.py:85
msgid "Date login"
msgstr "登录日期"
#: users/models/user.py:32 users/models/user.py:359
#: users/models/user.py:32 users/models/user.py:418
msgid "Administrator"
msgstr "管理员"
......@@ -3221,13 +3383,13 @@ msgid "Application"
msgstr "应用程序"
#: 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:166
#: users/templates/users/user_profile.html:167
#: users/templates/users/user_profile.html:170
msgid "Disable"
msgstr "禁用"
#: 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"
msgstr "启用"
......@@ -3235,26 +3397,30 @@ msgstr "启用"
msgid "Force enable"
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
msgid "Email"
msgstr "邮件"
#: users/models/user.py:64
#: users/models/user.py:66
msgid "Avatar"
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"
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_profile.html:100
msgid "Source"
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"
msgstr "Administrator是初始的超级管理员"
......@@ -3350,7 +3516,7 @@ msgstr "获取更多信息"
#: users/templates/users/forgot_password.html:11
#: users/templates/users/forgot_password.html:27
#: users/templates/users/login.html:79
#: users/templates/users/login.html:83
msgid "Forgot password"
msgstr "忘记密码"
......@@ -3395,10 +3561,22 @@ msgstr ""
msgid "Changes the world, starting with a little bit."
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"
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/user_detail.html:91
#: users/templates/users/user_profile.html:85
......@@ -3459,7 +3637,7 @@ msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry"
msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry"
#: users/templates/users/reset_password.html:46
#: users/templates/users/user_detail.html:367 users/utils.py:80
#: users/templates/users/user_detail.html:371 users/utils.py:81
msgid "Reset password"
msgstr "重置密码"
......@@ -3530,7 +3708,7 @@ msgid "Reset link will be generated and sent to the user. "
msgstr "生成重置密码连接,通过邮件发送给用户"
#: 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"
msgstr "用户详情"
......@@ -3550,79 +3728,84 @@ msgstr "强制启用"
msgid "Last login"
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"
msgstr "强制启用MFA"
#: users/templates/users/user_detail.html:169
#: users/templates/users/user_detail.html:173
msgid "Reset MFA"
msgstr "重置MFA"
#: users/templates/users/user_detail.html:177
#: users/templates/users/user_detail.html:181
msgid "Send reset password mail"
msgstr "发送重置密码邮件"
#: users/templates/users/user_detail.html:180
#: users/templates/users/user_detail.html:188
#: users/templates/users/user_detail.html:184
#: users/templates/users/user_detail.html:192
msgid "Send"
msgstr "发送"
#: users/templates/users/user_detail.html:185
#: users/templates/users/user_detail.html:189
msgid "Send reset ssh key mail"
msgstr "发送重置密钥邮件"
#: users/templates/users/user_detail.html:193
#: users/templates/users/user_detail.html:455
#: users/templates/users/user_detail.html:197
#: users/templates/users/user_detail.html:459
msgid "Unblock user"
msgstr "解除登录限制"
#: users/templates/users/user_detail.html:196
#: users/templates/users/user_detail.html:200
msgid "Unblock"
msgstr "解除"
#: users/templates/users/user_detail.html:310
#: users/templates/users/user_detail.html:314
msgid "Goto profile page enable 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."
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"
msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱"
#: users/templates/users/user_detail.html:392
#: users/templates/users/user_detail.html:396
msgid ""
"The reset-ssh-public-key E-mail has been sent successfully. Please inform "
"the user to update his new ssh public key."
msgstr "重设密钥邮件将会发送到用户邮箱"
#: users/templates/users/user_detail.html:393
#: users/templates/users/user_detail.html:397
msgid "Reset SSH public key"
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"
msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱"
#: users/templates/users/user_detail.html:421
#: users/templates/users/user_profile.html:221
#: users/templates/users/user_detail.html:425
#: users/templates/users/user_profile.html:225
msgid "Successfully updated the SSH public key."
msgstr "更新ssh密钥成功"
#: users/templates/users/user_detail.html:422
#: users/templates/users/user_detail.html:426
#: users/templates/users/user_profile.html:222
#: users/templates/users/user_profile.html:227
#: users/templates/users/user_detail.html:430
#: users/templates/users/user_profile.html:226
#: users/templates/users/user_profile.html:231
msgid "User SSH public key update"
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."
msgstr "解除用户登录限制后,此用户即可正常登录"
#: users/templates/users/user_detail.html:485
#: users/templates/users/user_detail.html:489
msgid "Reset user MFA success"
msgstr "重置用户MFA成功"
......@@ -3641,37 +3824,37 @@ msgstr "添加用户"
msgid "Create user group"
msgstr "创建用户组"
#: users/templates/users/user_group_list.html:82
#: users/templates/users/user_group_list.html:83
msgid "This will delete the selected groups !!!"
msgstr "删除选择组"
#: users/templates/users/user_group_list.html:91
#: users/templates/users/user_group_list.html:92
msgid "UserGroups Deleted."
msgstr "用户组删除"
#: users/templates/users/user_group_list.html:92
#: users/templates/users/user_group_list.html:97
#: users/templates/users/user_group_list.html:93
#: users/templates/users/user_group_list.html:98
msgid "UserGroups Delete"
msgstr "用户组删除"
#: users/templates/users/user_group_list.html:96
#: users/templates/users/user_group_list.html:97
msgid "UserGroup Deleting failed."
msgstr "用户组删除失败"
#: users/templates/users/user_list.html:196
#: users/templates/users/user_list.html:200
msgid "This will delete the selected users !!!"
msgstr "删除选中用户 !!!"
#: users/templates/users/user_list.html:205
#: users/templates/users/user_list.html:209
msgid "User Deleted."
msgstr "已被删除"
#: users/templates/users/user_list.html:206
#: users/templates/users/user_list.html:211
#: users/templates/users/user_list.html:210
#: users/templates/users/user_list.html:215
msgid "User Delete"
msgstr "删除"
#: users/templates/users/user_list.html:210
#: users/templates/users/user_list.html:214
msgid "User Deleting failed."
msgstr "用户删除失败"
......@@ -3720,32 +3903,32 @@ msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接
msgid "Administrator Settings force MFA login"
msgstr "管理员设置强制使用MFA登录"
#: users/templates/users/user_profile.html:116 users/views/user.py:230
#: users/views/user.py:284
#: users/templates/users/user_profile.html:120 users/views/user.py:229
#: users/views/user.py:283
msgid "User groups"
msgstr "用户组"
#: users/templates/users/user_profile.html:148
#: users/templates/users/user_profile.html:152
msgid "Update password"
msgstr "更改密码"
#: users/templates/users/user_profile.html:156
#: users/templates/users/user_profile.html:160
msgid "Set MFA"
msgstr "设置MFA"
#: users/templates/users/user_profile.html:178
#: users/templates/users/user_profile.html:182
msgid "Update MFA"
msgstr "更改MFA"
#: users/templates/users/user_profile.html:187
#: users/templates/users/user_profile.html:191
msgid "Update SSH public key"
msgstr "更改SSH密钥"
#: users/templates/users/user_profile.html:195
#: users/templates/users/user_profile.html:199
msgid "Reset public key and download"
msgstr "重置并下载SSH密钥"
#: users/templates/users/user_profile.html:225
#: users/templates/users/user_profile.html:229
msgid "Failed to update SSH public key."
msgstr "更新密钥失败"
......@@ -3775,11 +3958,11 @@ msgstr "新的公钥已设置成功,请下载对应的私钥"
msgid "Update user"
msgstr "更新用户"
#: users/utils.py:41
#: users/utils.py:42
msgid "Create account successfully"
msgstr "创建账户成功"
#: users/utils.py:43
#: users/utils.py:44
#, python-format
msgid ""
"\n"
......@@ -3824,7 +4007,7 @@ msgstr ""
" </br>\n"
" "
#: users/utils.py:82
#: users/utils.py:83
#, python-format
msgid ""
"\n"
......@@ -3868,11 +4051,64 @@ msgstr ""
" </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"
msgstr "重置ssh密钥"
#: users/utils.py:115
#: users/utils.py:154
#, python-format
msgid ""
"\n"
......@@ -3897,22 +4133,18 @@ msgstr ""
" </br>\n"
" "
#: users/utils.py:148
#: users/utils.py:187
msgid "User not exist"
msgstr "用户不存在"
#: users/utils.py:150
#: users/utils.py:189
msgid "Disabled or expired"
msgstr "禁用或失效"
#: users/utils.py:163
#: users/utils.py:202
msgid "Password or SSH public key invalid"
msgstr "密码或密钥不合法"
#: users/utils.py:286 users/utils.py:296
msgid "Bit"
msgstr " 位"
#: users/views/group.py:29
msgid "User group list"
msgstr "用户组列表"
......@@ -3929,100 +4161,104 @@ msgstr "用户组授权资产"
msgid "Please enable cookies and try again."
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"
msgstr "MFA验证码不正确,或者服务器端时间不对"
#: users/views/login.py:204
#: users/views/login.py:223
msgid "Logout success"
msgstr "退出登录成功"
#: users/views/login.py:205
#: users/views/login.py:224
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
#: users/views/login.py:221
#: users/views/login.py:240
msgid "Email address invalid, please input again"
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"
msgstr "发送重置密码邮件"
#: users/views/login.py:235
#: users/views/login.py:257
msgid "Send reset password mail success, login your mail box and follow it "
msgstr ""
"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)"
#: users/views/login.py:248
#: users/views/login.py:270
msgid "Reset password success"
msgstr "重置密码成功"
#: users/views/login.py:249
#: users/views/login.py:271
msgid "Reset password success, return to login page"
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"
msgstr "Token错误或失效"
#: users/views/login.py:279
#: users/views/login.py:298
msgid "Password not same"
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"
msgstr "* 您的密码不符合要求"
#: users/views/login.py:327
#: users/views/login.py:346
msgid "First login"
msgstr "首次登陆"
#: users/views/user.py:144
#: users/views/user.py:143
msgid "Bulk update user success"
msgstr "批量更新用户成功"
#: users/views/user.py:174
#: users/views/user.py:173
msgid "Bulk update user"
msgstr "批量更新用户"
#: users/views/user.py:259
#: users/views/user.py:258
msgid "Invalid file."
msgstr "文件不合法"
#: users/views/user.py:355
#: users/views/user.py:354
msgid "User granted assets"
msgstr "用户授权资产"
#: users/views/user.py:386
#: users/views/user.py:385
msgid "Profile setting"
msgstr "个人信息设置"
#: users/views/user.py:405
#: users/views/user.py:404
msgid "Password update"
msgstr "密码更新"
#: users/views/user.py:440
#: users/views/user.py:438
msgid "Public key update"
msgstr "密钥更新"
#: users/views/user.py:481
#: users/views/user.py:479
msgid "Password invalid"
msgstr "用户名或密码无效"
#: users/views/user.py:581
#: users/views/user.py:579
msgid "MFA enable success"
msgstr "MFA 绑定成功"
#: users/views/user.py:582
#: users/views/user.py:580
msgid "MFA enable success, return login page"
msgstr "MFA 绑定成功,返回到登录页面"
#: users/views/user.py:584
#: users/views/user.py:582
msgid "MFA disable success"
msgstr "MFA 解绑成功"
#: users/views/user.py:585
#: users/views/user.py:583
msgid "MFA disable success, return login page"
msgstr "MFA 解绑成功,返回登录页面"
......@@ -4160,12 +4396,6 @@ msgstr "同步实例任务历史"
msgid "Instance"
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
msgid "任务执行开始: {}"
msgstr ""
......@@ -4339,6 +4569,24 @@ msgstr "创建组织"
msgid "Update org"
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"
#~ msgstr "同步实例任务详情"
......
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -17,58 +17,58 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: static/js/jumpserver.js:158
#: static/js/jumpserver.js:168
msgid "Update is successful!"
msgstr "更新成功"
#: static/js/jumpserver.js:160
#: static/js/jumpserver.js:170
msgid "An unknown error occurred while updating.."
msgstr "更新时发生未知错误"
#: static/js/jumpserver.js:205 static/js/jumpserver.js:247
#: static/js/jumpserver.js:252
#: static/js/jumpserver.js:236 static/js/jumpserver.js:273
#: static/js/jumpserver.js:276
msgid "Error"
msgstr "错误"
#: static/js/jumpserver.js:205
#: static/js/jumpserver.js:236
msgid "Being used by the asset, please unbind the asset first."
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"
msgstr "删除成功"
#: static/js/jumpserver.js:219
#: static/js/jumpserver.js:248
msgid "Are you sure about deleting it?"
msgstr "你确定删除吗 ?"
#: static/js/jumpserver.js:224 static/js/jumpserver.js:273
#: static/js/jumpserver.js:252 static/js/jumpserver.js:293
msgid "Cancel"
msgstr "取消"
#: static/js/jumpserver.js:227 static/js/jumpserver.js:276
#: static/js/jumpserver.js:254 static/js/jumpserver.js:295
msgid "Confirm"
msgstr "确认"
#: static/js/jumpserver.js:247
#: static/js/jumpserver.js:273
msgid ""
"The organization contains undeleted information. Please try again after "
"deleting"
msgstr "组织中包含未删除信息,请删除后重试"
#: static/js/jumpserver.js:252
#: static/js/jumpserver.js:276
msgid ""
"Do not perform this operation under this organization. Try again after "
"switching to another organization"
msgstr "请勿在此组织下执行此操作,切换到其他组织后重试"
#: static/js/jumpserver.js:267
#: static/js/jumpserver.js:289
msgid ""
"Please ensure that the following information in the organization has been "
"deleted"
msgstr "请确保组织内的以下信息已删除"
#: static/js/jumpserver.js:269
#: static/js/jumpserver.js:290
msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission"
......@@ -76,32 +76,52 @@ msgstr ""
"用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权"
"规则"
#: static/js/jumpserver.js:311
#: static/js/jumpserver.js:329
msgid "Loading ..."
msgstr "加载中 ..."
#: static/js/jumpserver.js:313
#: static/js/jumpserver.js:330
msgid "Search"
msgstr "搜索"
#: static/js/jumpserver.js:317
#: static/js/jumpserver.js:333
#, javascript-format
msgid "Selected item %d"
msgstr "选中 %d 项"
#: static/js/jumpserver.js:322
#: static/js/jumpserver.js:337
msgid "Per page _MENU_"
msgstr "每页 _MENU_"
#: static/js/jumpserver.js:324
#: static/js/jumpserver.js:338
msgid ""
"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项"
#: static/js/jumpserver.js:328
#: static/js/jumpserver.js:341
msgid "No match"
msgstr "没有匹配项"
#: static/js/jumpserver.js:330
#: static/js/jumpserver.js:342
msgid "No record"
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):
queryset = Task.objects.all()
serializer_class = TaskSerializer
permission_classes = (IsOrgAdmin,)
label = None
help_text = ''
# label = None
# help_text = ''
class TaskRun(generics.RetrieveAPIView):
queryset = Task.objects.all()
serializer_class = TaskViewSet
# serializer_class = TaskViewSet
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
......
......@@ -50,7 +50,7 @@ class JMSInventory(BaseInventory):
def convert_to_ansible(self, asset, run_as_admin=False):
info = {
'id': asset.id,
'hostname': asset.hostname,
'hostname': asset.fullname,
'ip': asset.ip,
'port': asset.port,
'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
from common.mixins import DatetimeSearchMixin
from .models import Task, AdHoc, AdHocRunHistory, CeleryTask
from common.permissions import SuperUserRequiredMixin
from common.permissions import SuperUserRequiredMixin, AdminUserRequiredMixin
class TaskListView(SuperUserRequiredMixin, DatetimeSearchMixin, ListView):
......@@ -121,6 +121,6 @@ class AdHocHistoryDetailView(SuperUserRequiredMixin, DetailView):
return super().get_context_data(**kwargs)
class CeleryTaskLogView(SuperUserRequiredMixin, DetailView):
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
template_name = 'ops/celery_task_log.html'
model = CeleryTask
# -*- 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 .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()
serializer_class = OrgSerializer
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
from django.http.response import HttpResponseForbidden
from django.core.exceptions import ValidationError
from common.utils import get_logger
from .utils import current_org, set_current_org, set_to_root_org
from .models import Organization
......@@ -19,7 +18,7 @@ tl = Local()
__all__ = [
'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm',
'RootOrgViewMixin',
'RootOrgViewMixin', 'OrgMembershipSerializerMixin', 'OrgMembershipModelViewSetMixin'
]
......@@ -176,3 +175,29 @@ class OrgModelForm(ModelForm):
continue
model = field.queryset.model
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 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 .mixins import OrgMembershipSerializerMixin
class OrgSerializer(ModelSerializer):
class Meta:
model = Organization
list_serializer_class = BulkListSerializer
fields = '__all__'
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 -*-
#
from django.urls import path
from rest_framework.routers import DefaultRouter
from .. import api
app_name = 'orgs'
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')
......
......@@ -5,6 +5,7 @@ from django.shortcuts import get_object_or_404
from rest_framework.views import APIView, Response
from rest_framework.generics import ListAPIView, get_object_or_404, RetrieveUpdateAPIView
from rest_framework import viewsets
from rest_framework.pagination import LimitOffsetPagination
from common.utils import set_or_append_attr_bulk
from common.permissions import IsValidUser, IsOrgAdmin, IsOrgAdminOrAppUser
......@@ -15,6 +16,16 @@ from .hands import AssetGrantedSerializer, User, UserGroup, Asset, Node, \
NodeGrantedSerializer, SystemUser, NodeSerializer
from orgs.utils import set_to_root_org
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):
......@@ -23,6 +34,7 @@ class AssetPermissionViewSet(viewsets.ModelViewSet):
"""
queryset = AssetPermission.objects.all()
serializer_class = serializers.AssetPermissionCreateUpdateSerializer
pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdmin,)
def get_serializer_class(self):
......@@ -31,10 +43,15 @@ class AssetPermissionViewSet(viewsets.ModelViewSet):
return self.serializer_class
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')
node_id = self.request.query_params.get('node')
inherit_nodes = set()
if search:
queryset = queryset.filter(name__icontains=search)
if not asset_id and not node_id:
return queryset
......@@ -53,15 +70,17 @@ class AssetPermissionViewSet(viewsets.ModelViewSet):
_permissions = queryset.filter(nodes=n)
set_or_append_attr_bulk(_permissions, "inherit", n.value)
permissions.update(_permissions)
return permissions
return list(permissions)
class UserGrantedAssetsApi(ListAPIView):
class UserGrantedAssetsApi(AssetsFilterMixin, ListAPIView):
"""
用户授权的所有资产
"""
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = AssetGrantedSerializer
pagination_class = LimitOffsetPagination
def change_org_if_need(self):
if self.request.user.is_superuser or \
......@@ -84,6 +103,7 @@ class UserGrantedAssetsApi(ListAPIView):
system_users_granted = [s for s in v if s.protocol == k.protocol]
k.system_users_granted = system_users_granted
queryset.append(k)
return queryset
def get_permissions(self):
......@@ -122,7 +142,7 @@ class UserGrantedNodesApi(ListAPIView):
return super().get_permissions()
class UserGrantedNodesWithAssetsApi(ListAPIView):
class UserGrantedNodesWithAssetsApi(AssetsFilterMixin, ListAPIView):
"""
用户授权的节点并带着节点下资产的api
"""
......@@ -155,19 +175,25 @@ class UserGrantedNodesWithAssetsApi(ListAPIView):
queryset.append(node)
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):
if self.kwargs.get('pk') is None:
self.permission_classes = (IsValidUser,)
return super().get_permissions()
class UserGrantedNodeAssetsApi(ListAPIView):
class UserGrantedNodeAssetsApi(AssetsFilterMixin, ListAPIView):
"""
查询用户授权的节点下的资产的api, 与上面api不同的是,只返回某个节点下的资产
"""
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = AssetGrantedSerializer
pagination_class = LimitOffsetPagination
def change_org_if_need(self):
if self.request.user.is_superuser or \
self.request.user.is_app or \
......@@ -189,6 +215,8 @@ class UserGrantedNodeAssetsApi(ListAPIView):
assets = nodes.get(node, [])
for asset, system_users in assets.items():
asset.system_users_granted = system_users
assets = list(assets.keys())
return assets
def get_permissions(self):
......@@ -274,7 +302,7 @@ class UserGroupGrantedNodeAssetsApi(ListAPIView):
return assets
class ValidateUserAssetPermissionView(RootOrgViewMixin, APIView):
class ValidateUserAssetPermissionApi(RootOrgViewMixin, APIView):
permission_classes = (IsOrgAdminOrAppUser,)
@staticmethod
......@@ -367,3 +395,88 @@ class AssetPermissionAddAssetApi(RetrieveUpdateAPIView):
return Response({"msg": "ok"})
else:
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 @@
#
from rest_framework import serializers
from .models import AssetPermission
from common.fields import StringManyToManyField
from .models import AssetPermission
from assets.models import Node
from assets.serializers import AssetGrantedSerializer
class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer):
......@@ -45,3 +48,29 @@ class AssetPermissionUpdateAssetSerializer(serializers.ModelSerializer):
model = AssetPermission
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 @@
</li>
<li class="active">
<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>
</ul>
</div>
......
......@@ -54,9 +54,9 @@
<div class="col-sm-9">
<div class="input-daterange input-group" id="datepicker">
<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>
<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>
<span class="help-block ">{{ form.date_expired.errors }}</span>
<span class="help-block ">{{ form.date_start.errors }}</span>
......@@ -70,6 +70,7 @@
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
</div>
</div>
......@@ -80,19 +81,27 @@
{% endblock %}
{% block custom_foot_js %}
<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>
var dateOptions = {
singleDatePicker: true,
showDropdowns: true,
timePicker: true,
timePicker24Hour: true,
autoApply: true,
locale: {
format: 'YYYY-MM-DD HH:mm'
}
};
$(document).ready(function () {
$('.select2').select2({
closeOnSelect: false
});
$('#datepicker').datepicker({
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
});
$('#date_start').daterangepicker(dateOptions);
$('#date_expired').daterangepicker(dateOptions);
$("#id_assets").parent().find(".select2-selection").on('click', function (e) {
if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){
e.preventDefault();
......@@ -110,6 +119,6 @@ $(document).ready(function () {
$('.select2').val(assets).trigger('change');
});
$("#asset_list_modal").modal('hide');
})
});
</script>
{% endblock %}
\ No newline at end of file
......@@ -24,7 +24,7 @@
</li>
<li>
<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 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>
......
......@@ -217,7 +217,7 @@ function initTable() {
select: {},
op_html: $('#actions').html()
};
table = jumpserver.initDataTable(options);
table = jumpserver.initServerSideDataTable(options);
return table
}
......
......@@ -24,7 +24,7 @@
</li>
<li>
<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>
</ul>
</div>
......
......@@ -19,6 +19,8 @@ urlpatterns = [
api.UserGrantedNodesApi.as_view(), name='user-nodes'),
path('user/nodes/', api.UserGrantedNodesApi.as_view(),
name='my-nodes'),
path('user/nodes/children/', api.UserGrantedNodeChildrenApi.as_view(),
name='my-node-children'),
path('user/<uuid:pk>/nodes/<uuid:node_id>/assets/',
api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'),
path('user/nodes/<uuid:node_id>/assets/',
......@@ -55,7 +57,7 @@ urlpatterns = [
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'),
]
......
......@@ -156,3 +156,22 @@ class AssetPermissionUtil:
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,
ListView):
template_name = 'perms/asset_permission_user.html'
context_object_name = 'asset_permission'
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
paginate_by = settings.DISPLAY_PER_PAGE
object = None
def get(self, request, *args, **kwargs):
......@@ -133,7 +133,7 @@ class AssetPermissionAssetView(AdminUserRequiredMixin,
ListView):
template_name = 'perms/asset_permission_asset.html'
context_object_name = 'asset_permission'
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
paginate_by = settings.DISPLAY_PER_PAGE
object = None
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() {
if (app === ''){
$('#index').addClass('active');
}
else if (app === 'xpack') {
else if (app === 'xpack' && resource === 'cloud') {
var item = url_array[3];
$("#" + app).addClass('active');
$('#' + app + ' #' + resource).addClass('active');
$('#' + app + ' #' + resource + ' #' + item + ' a').css('color', '#ffffff');
}
else if (app === 'settings'){
$("#" + app).addClass('active');
}
else {
$("#" + app).addClass('active');
$('#' + app + ' #' + resource).addClass('active');
......@@ -505,7 +508,7 @@ jumpserver.initServerSideDataTable = function (options) {
if (data.order !== null && data.order.length === 1) {
var col = data.order[0].column;
var order = options.columns[col].data;
if (data.order[0].dir = "desc") {
if (data.order[0].dir === "desc") {
order = "-" + order;
}
data.order = order;
......@@ -685,41 +688,69 @@ function setUrlParam(url, name, value) {
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) {
if (wordMinLength(password, minLength)) {
$('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', 'green')
$('#'+rules_short_map_id['min']).css('color', 'green')
}
else {
$('#rule_SECURITY_PASSWORD_MIN_LENGTH').css('color', '#908a8a')
$('#'+rules_short_map_id['min']).css('color', '#908a8a')
}
if (wordUpperCase(password)) {
$('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', 'green');
$('#'+rules_short_map_id['upper']).css('color', 'green')
}
else {
$('#rule_SECURITY_PASSWORD_UPPER_CASE').css('color', '#908a8a')
$('#'+rules_short_map_id['upper']).css('color', '#908a8a')
}
if (wordLowerCase(password)) {
$('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', 'green')
$('#'+rules_short_map_id['lower']).css('color', 'green')
}
else {
$('#rule_SECURITY_PASSWORD_LOWER_CASE').css('color', '#908a8a')
$('#'+rules_short_map_id['lower']).css('color', '#908a8a')
}
if (wordNumber(password)) {
$('#rule_SECURITY_PASSWORD_NUMBER').css('color', 'green')
$('#'+rules_short_map_id['number']).css('color', 'green')
}
else {
$('#rule_SECURITY_PASSWORD_NUMBER').css('color', '#908a8a')
$('#'+rules_short_map_id['number']).css('color', '#908a8a')
}
if (wordSpecialChar(password)) {
$('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', 'green')
$('#'+rules_short_map_id['special']).css('color', 'green')
}
else {
$('#rule_SECURITY_PASSWORD_SPECIAL_CHAR').css('color', '#908a8a')
$('#'+rules_short_map_id['special']).css('color', '#908a8a')
}
}
......@@ -746,11 +777,12 @@ function wordSpecialChar(word) {
return word.match(/[`,~,!,@,#,\$,%,\^,&,\*,\(,\),\-,_,=,\+,\{,\},\[,\],\|,\\,;,',:,",\,,\.,<,>,\/,\?]+/)
}
// 显示弹窗密码规则
function popoverPasswordRules(password_check_rules, $el) {
var message = "";
jQuery.each(password_check_rules, function (idx, rules) {
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>";
jQuery.each(password_check_rules, function (idx, rule) {
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);
$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 @@
<div class="ibox-content">
{% if form.errors.all %}
<div class="alert alert-danger" style="margin: 20px auto 0px">
{{ form.errors.all }}
</div>
{{ form.errors.all }}
</div>
{% endif %}
{% block form %}
{% endblock %}
......
{% load i18n %}
<div class="footer fixed">
<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">-->
</div>
<div>
......
{% 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 %}
{% if request.user.is_authenticated and request.user.is_first_login %}
<div class="alert alert-danger help-message alert-dismissable">
......@@ -6,7 +27,6 @@
{% blocktrans %}
Your information was incomplete. Please click <a href="{{ first_login_url }}"> this link </a>to complete your information.
{% endblocktrans %}
<button aria-hidden="true" data-dismiss="alert" class="close" type="button" style="outline: none;">×</button>
</div>
{% endif %}
......
# -*- coding: utf-8 -*-
#
# -*- coding: utf-8 -*-
#
from .terminal import *
from .session import *
from .task import *
# -*- coding: utf-8 -*-
#
from collections import OrderedDict
import logging
import os
import uuid
from django.core.cache import cache
from django.shortcuts import get_object_or_404, redirect
from django.utils import timezone
from django.shortcuts import get_object_or_404
from django.core.files.storage import default_storage
from django.http.response import HttpResponseRedirectBase
from django.http import HttpResponseNotFound
from django.conf import settings
import jms_storage
from rest_framework.pagination import LimitOffsetPagination
from rest_framework import viewsets
from rest_framework.views import APIView, Response
from rest_framework.permissions import AllowAny
from rest_framework.views import Response
from rest_framework_bulk import BulkModelViewSet
import jms_storage
from common.utils import get_object_or_none, is_uuid
from .hands import SystemUser
from .models import Terminal, Status, Session, Task
from .serializers import TerminalSerializer, StatusSerializer, \
SessionSerializer, TaskSerializer, ReplaySerializer
from common.permissions import IsAppUser, IsOrgAdminOrAppUser
from .backends import get_command_storage, get_multi_command_storage, \
from common.utils import is_uuid
from common.permissions import IsOrgAdminOrAppUser
from ...hands import SystemUser
from ...models import Terminal, Session
from ...serializers import v1 as serializers
from ...backends import get_command_storage, get_multi_command_storage, \
SessionCommandSerializer
__all__ = ['SessionViewSet', 'SessionReplayViewSet', 'CommandViewSet']
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):
queryset = Session.objects.all()
serializer_class = SessionSerializer
serializer_class = serializers.SessionSerializer
pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdminOrAppUser,)
......@@ -198,29 +50,6 @@ class SessionViewSet(BulkModelViewSet):
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):
"""接受app发送来的command log, 格式如下
{
......@@ -262,7 +91,7 @@ class CommandViewSet(viewsets.ViewSet):
class SessionReplayViewSet(viewsets.ViewSet):
serializer_class = ReplaySerializer
serializer_class = serializers.ReplaySerializer
permission_classes = (IsOrgAdminOrAppUser,)
session = None
upload_to = 'replay' # 仅添加到本地存储中
......@@ -318,6 +147,11 @@ class SessionReplayViewSet(viewsets.ViewSet):
session_id = kwargs.get('pk')
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() # 存在外部存储上的路径
local_path = self.get_local_path()
......@@ -327,7 +161,8 @@ class SessionReplayViewSet(viewsets.ViewSet):
for _local_path in (local_path, local_path_v1, session_path):
if default_storage.exists(_local_path):
url = default_storage.url(_local_path)
return redirect(url)
data['src'] = url
return Response(data)
# 去定义的外部storage查找
configs = settings.TERMINAL_REPLAY_STORAGE
......@@ -344,31 +179,6 @@ class SessionReplayViewSet(viewsets.ViewSet):
if not ok:
logger.error("Failed download replay file: {}".format(err))
return HttpResponseNotFound()
return redirect(default_storage.url(local_path))
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,)
data['src'] = default_storage.url(local_path)
return Response(data)
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
from django.conf import settings
from .command.serializers import SessionCommandSerializer
from common import utils
TYPE_ENGINE_MAPPING = {
'elasticsearch': 'terminal.backends.command.es',
}
......@@ -16,7 +18,9 @@ def get_command_storage():
def get_terminal_command_storages():
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']
if tp == 'server':
storage = get_command_storage()
......
......@@ -2,36 +2,33 @@
#
from django import forms
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from .models import Terminal
def get_all_command_storage():
# storage_choices = []
from common.models import Setting
Setting.refresh_all_settings()
for k, v in settings.TERMINAL_COMMAND_STORAGE.items():
from common import utils
command_storage = utils.get_command_storage_setting()
for k, v in command_storage.items():
yield (k, k)
def get_all_replay_storage():
# storage_choices = []
from common.models import Setting
Setting.refresh_all_settings()
for k, v in settings.TERMINAL_REPLAY_STORAGE.items():
from common import utils
replay_storage = utils.get_replay_storage_setting()
for k, v in replay_storage.items():
yield (k, k)
class TerminalForm(forms.ModelForm):
command_storage = forms.ChoiceField(
choices=get_all_command_storage(),
choices=get_all_command_storage,
label=_("Command storage"),
help_text=_("Command can store in server db or ES, default to server, more see docs"),
)
replay_storage = forms.ChoiceField(
choices=get_all_replay_storage(),
choices=get_all_replay_storage,
label=_("Replay storage"),
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
from users.models import User
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
class Terminal(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
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)
http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000)
command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default')
......@@ -40,7 +40,7 @@ class Terminal(models.Model):
self.user.save()
def get_common_storage(self):
storage_all = settings.TERMINAL_COMMAND_STORAGE
storage_all = get_command_storage_setting()
if self.command_storage in storage_all:
storage = storage_all.get(self.command_storage)
else:
......@@ -48,7 +48,7 @@ class Terminal(models.Model):
return {"TERMINAL_COMMAND_STORAGE": storage}
def get_replay_storage(self):
storage_all = settings.TERMINAL_REPLAY_STORAGE
storage_all = get_replay_storage_setting()
if self.replay_storage in storage_all:
storage = storage_all.get(self.replay_storage)
else:
......@@ -64,14 +64,19 @@ class Terminal(models.Model):
configs.update(self.get_common_storage())
configs.update(self.get_replay_storage())
configs.update({
'SECURITY_MAX_IDLE_TIME': common_settings.SECURITY_MAX_IDLE_TIME or
settings.DEFAULT_SECURITY_MAX_IDLE_TIME,
'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME
})
return configs
@property
def service_account(self):
return self.user
def create_app_user(self):
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.save()
return user, access_key
......
# -*- coding: utf-8 -*-
#
from django.core.cache import cache
from django.utils import timezone
from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin
from .models import Terminal, Status, Session, Task
from .backends import get_multi_command_storage
from ..models import Terminal, Status, Session, Task
from ..backends import get_multi_command_storage
class TerminalSerializer(serializers.ModelSerializer):
......@@ -33,6 +31,8 @@ class TerminalSerializer(serializers.ModelSerializer):
return cache.get(key)
class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer):
command_amount = serializers.SerializerMethodField()
command_store = get_multi_command_storage()
......@@ -71,3 +71,4 @@ class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class ReplaySerializer(serializers.Serializer):
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 @@
# -*- coding: utf-8 -*-
#
from django.urls import path
from django.urls import path, include
from rest_framework_bulk.routes import BulkRouter
from .. import api
from ..api import v1 as api
app_name = 'terminal'
......@@ -20,7 +20,7 @@ router.register(r'status', api.StatusViewSet, 'status')
urlpatterns = [
path('sessions/<uuid:pk>/replay/',
api.SessionReplayV2ViewSet.as_view({'get': 'retrieve', 'post': 'create'}),
api.SessionReplayViewSet.as_view({'get': 'retrieve', 'post': 'create'}),
name='session-replay'),
path('tasks/kill-session/', api.KillSessionAPI.as_view(), name='kill-session'),
path('terminal/<uuid:terminal>/access-key/', api.TerminalTokenApi.as_view(),
......@@ -33,3 +33,6 @@ urlpatterns = [
]
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
from ..serializers import UserSerializer
from ..tasks import write_login_log_async
from ..models import User, LoginLog
from ..utils import check_user_valid, generate_token, \
check_otp_code, increase_login_failed_count, is_block_login, \
from ..utils import check_user_valid, check_otp_code, \
increase_login_failed_count, is_block_login, \
clean_failed_count
from ..hands import Asset, SystemUser
......@@ -43,16 +43,32 @@ class UserAuthApi(RootOrgViewMixin, APIView):
user, msg = self.check_user_valid(request)
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 = {
'username': request.data.get('username', ''),
'username': username,
'mfa': LoginLog.MFA_UNKNOWN,
'reason': LoginLog.REASON_PASSWORD,
'reason': reason,
'status': False
}
self.write_login_log(request, data)
increase_login_failed_count(username, ip)
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:
data = {
'username': user.username,
......@@ -63,12 +79,9 @@ class UserAuthApi(RootOrgViewMixin, APIView):
self.write_login_log(request, data)
# 登陆成功,清除原来的缓存计数
clean_failed_count(username, ip)
token = generate_token(request, user)
token = user.create_bearer_token(request)
return Response(
{
'token': token,
'user': self.serializer_class(user).data
}
{'token': token, 'user': self.serializer_class(user).data}
)
seed = uuid.uuid4().hex
......@@ -110,7 +123,6 @@ class UserAuthApi(RootOrgViewMixin, APIView):
'user_agent': user_agent,
}
data.update(tmp_data)
write_login_log_async.delay(**data)
......@@ -172,7 +184,7 @@ class UserToken(APIView):
user = request.user
msg = None
if user:
token = generate_token(request, user)
token = user.create_bearer_token(request)
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
else:
return Response({'error': msg}, status=406)
......@@ -210,7 +222,7 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView):
'status': True
}
self.write_login_log(request, data)
token = generate_token(request, user)
token = user.create_bearer_token(request)
return Response(
{
'token': token,
......
......@@ -3,6 +3,7 @@
from rest_framework import generics
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from ..serializers import UserGroupSerializer, \
UserGroupUpdateMemeberSerializer
......@@ -15,9 +16,12 @@ __all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi']
class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet):
filter_fields = ("name",)
search_fields = filter_fields
queryset = UserGroup.objects.all()
serializer_class = UserGroupSerializer
permission_classes = (IsOrgAdmin,)
pagination_class = LimitOffsetPagination
class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
......
......@@ -9,14 +9,16 @@ from rest_framework import generics
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
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, \
UserUpdateGroupSerializer, ChangeUserPasswordSerializer
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__)
......@@ -28,10 +30,12 @@ __all__ = [
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
permission_classes = (IsOrgAdmin,)
filter_fields = ('username', 'email', 'name', 'id')
pagination_class = LimitOffsetPagination
def get_queryset(self):
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 -*-
#
import base64
import uuid
import hashlib
import time
from django.core.cache import cache
......@@ -12,11 +10,10 @@ from django.utils.translation import ugettext as _
from django.utils.six import text_type
from django.utils.translation import ugettext_lazy as _
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 common.utils import get_object_or_none, make_signature, http_to_unixtime
from .utils import refresh_token
from .models import User, AccessKey, PrivateToken
......@@ -144,7 +141,6 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
if not user:
msg = _('Invalid token or cache refreshed.')
raise exceptions.AuthenticationFailed(msg)
refresh_token(token, user)
return user, None
......
......@@ -120,8 +120,7 @@ class UserCreateUpdateForm(OrgModelForm):
public_key = self.cleaned_data.get('public_key')
user = super().save(commit=commit)
if password:
user.set_password(password)
user.save()
user.reset_password(password)
if otp_level:
user.otp_level = otp_level
user.save()
......@@ -217,8 +216,7 @@ class UserPasswordForm(forms.Form):
def save(self):
password = self.cleaned_data['new_password']
self.instance.set_password(password)
self.instance.save()
self.instance.reset_password(new_password=password)
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):
secret = models.UUIDField(verbose_name='AccessKeySecret',
default=uuid.uuid4, editable=False)
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):
return str(self.id)
......@@ -25,6 +25,9 @@ class AccessKey(models.Model):
def get_secret(self):
return str(self.secret)
def get_full_value(self):
return '{}:{}'.format(self.id, self.secret)
def __str__(self):
return str(self.id)
......@@ -55,11 +58,15 @@ class LoginLog(models.Model):
REASON_NOTHING = 0
REASON_PASSWORD = 1
REASON_MFA = 2
REASON_NOT_EXIST = 3
REASON_PASSWORD_EXPIRED = 4
REASON_CHOICE = (
(REASON_NOTHING, _('-')),
(REASON_PASSWORD, _('Username/password check failed')),
(REASON_MFA, _('MFA authentication failed')),
(REASON_NOT_EXIST, _("Username does not exist")),
(REASON_PASSWORD_EXPIRED, _("Password expired")),
)
STATUS_CHOICE = (
......@@ -67,7 +74,7 @@ class LoginLog(models.Model):
(False, _('Failed'))
)
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'))
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
......
......@@ -2,20 +2,20 @@
# -*- coding: utf-8 -*-
#
import uuid
import base64
from collections import OrderedDict
from django.conf import settings
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.cache import cache
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.shortcuts import reverse
from 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
......@@ -40,9 +40,11 @@ class User(AbstractUser):
)
SOURCE_LOCAL = 'local'
SOURCE_LDAP = 'ldap'
SOURCE_OPENID = 'openid'
SOURCE_CHOICES = (
(SOURCE_LOCAL, 'Local'),
(SOURCE_LDAP, 'LDAP/AD'),
(SOURCE_OPENID, 'OpenID'),
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
username = models.CharField(
......@@ -95,6 +97,10 @@ class User(AbstractUser):
max_length=30, default=SOURCE_LOCAL, choices=SOURCE_CHOICES,
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):
return '{0.name}({0.username})'.format(self)
......@@ -219,6 +225,34 @@ class User(AbstractUser):
def is_staff(self, value):
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):
if not self.name:
self.name = self.username
......@@ -241,15 +275,38 @@ class User(AbstractUser):
token = PrivateToken.objects.create(user=self)
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):
from . import AccessKey
access_key = AccessKey.objects.create(user=self)
return access_key
def refresh_private_token(self):
from .authentication import PrivateToken
PrivateToken.objects.filter(user=self).delete()
return PrivateToken.objects.create(user=self)
@property
def access_key(self):
return self.access_keys.first()
def is_member_of(self, user_group):
if user_group in self.groups.all():
......@@ -257,7 +314,7 @@ class User(AbstractUser):
return False
def check_public_key(self, public_key):
if self.ssH_public_key == public_key:
if self.ssh_public_key == public_key:
return True
return False
......@@ -282,7 +339,7 @@ class User(AbstractUser):
@property
def otp_force_enabled(self):
if common_settings.SECURITY_MFA_AUTH:
if settings.SECURITY_MFA_AUTH:
return True
return self.otp_level == 2
......@@ -312,7 +369,8 @@ class User(AbstractUser):
'phone': self.phone,
'otp_level': self.otp_level,
'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
......@@ -339,6 +397,7 @@ class User(AbstractUser):
def reset_password(self, new_password):
self.set_password(new_password)
self.date_password_last_updated = timezone.now()
self.save()
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
from common.utils import get_signer, validate_ssh_public_key
from common.mixins import BulkSerializerMixin
from .models import User, UserGroup
from ..models import User, UserGroup
signer = get_signer()
class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
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:
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):
if user:
user.source = user.SOURCE_LDAP
user.save()
......@@ -2,10 +2,46 @@
#
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
def write_login_log_async(*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 @@
<div class="col-sm-9">
<div class="input-group date">
<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>
<span class="help-block ">{{ form.date_expired.errors }}</span>
</div>
......@@ -52,18 +52,24 @@
{% endblock %}
{% block custom_foot_js %}
<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>
var dateOptions = {
singleDatePicker: true,
showDropdowns: true,
timePicker: true,
timePicker24Hour: true,
autoApply: true,
locale: {
format: 'YYYY-MM-DD HH:mm'
}
};
$(document).ready(function () {
$('.select2').select2();
$('.input-group.date').datepicker({
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
});
$('#id_date_expired').daterangepicker(dateOptions);
})
</script>
{% endblock %}
......@@ -50,6 +50,8 @@
{% if block_login %}
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
{% elif password_expired %}
<p class="red-fonts">{% trans 'The user password has expired' %}</p>
{% elif form.errors %}
{% if 'captcha' in form.errors %}
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
......@@ -75,9 +77,23 @@
</p>
{% endif %}
<a href="{% url 'users:forgot-password' %}">
<small>{% trans 'Forgot password' %}?</small>
</a>
<div class="text-muted text-center">
<div>
<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>
<p class="m-t">
......
......@@ -99,7 +99,7 @@
container = $('#container'),
progress = $('#id_progress'),
password_check_rules = {{ password_check_rules|safe }},
minLength = {{ min_length }},
minLength = 6,
top = 146, left = 170,
i18n_fallback = {
"veryWeak": "{% trans 'Very weak' %}",
......@@ -110,6 +110,12 @@
"veryStrong": "{% trans 'Very strong' %}"
};
jQuery.each(password_check_rules, function (idx, rules) {
if(rules.key === 'id_security_password_min_length'){
minLength = rules.value
}
});
// 初始化popover
initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback);
......
......@@ -119,6 +119,10 @@
<td>{% trans 'Last login' %}:</td>
<td><b>{{ user_object.last_login|date:"Y-m-j H:i:s" }}</b></td>
</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>
<td>{% trans 'Comment' %}:</td>
<td><b>{{ user_object.comment }}</b></td>
......
......@@ -103,7 +103,7 @@ function initTable() {
{data: "system_users_granted", orderable: false}
]
};
asset_table = jumpserver.initDataTable(options);
asset_table = jumpserver.initServerSideDataTable(options)
}
function onSelected(event, treeNode) {
......
......@@ -58,7 +58,8 @@ $(document).ready(function() {
order: [],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options);
}).on('click', '.btn_delete_user_group', function(){
var $this = $(this);
var group_id = $this.data('gid');
......
......@@ -90,12 +90,16 @@ function initTable() {
}}],
ajax_url: '{% url "api-users:user-list" %}',
columns: [
{data: "id"}, {data: "name" }, {data: "username" }, {data: "get_role_display" },
{data: "groups_display" }, {data: "get_source_display" }, {data: "is_valid" }, {data: "id" }
{data: "id"}, {data: "name" }, {data: "username" },
{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()
};
table = jumpserver.initDataTable(options);
var table = jumpserver.initServerSideDataTable(options);
return table
}
......
......@@ -92,7 +92,7 @@
container = $('#container'),
progress = $('#id_progress'),
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,
left = 377,
i18n_fallback = {
......@@ -104,6 +104,12 @@
"veryStrong": "{% trans 'Very strong' %}"
};
jQuery.each(password_check_rules, function (idx, rules) {
if(rules.key === 'id_security_password_min_length'){
minLength = rules.value
}
});
// 初始化popover
initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback);
......
......@@ -108,6 +108,10 @@
<td class="text-navy">{% trans 'Last login' %}</td>
<td>{{ user.last_login|date:"Y-m-d H:i:s" }}</td>
</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>
<td class="text-navy">{% trans 'Date expired' %}</td>
<td>{{ user.date_expired|date:"Y-m-d H:i:s" }}</td>
......
......@@ -27,7 +27,7 @@
container = $('#container'),
progress = $('#id_progress'),
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,
left = 377,
i18n_fallback = {
......@@ -39,6 +39,12 @@
"veryStrong": "{% trans 'Very strong' %}"
};
jQuery.each(password_check_rules, function (idx, rules) {
if(rules.key === 'id_security_password_min_length'){
minLength = rules.value
}
});
// 初始化popover
initPopover(container, progress, idPassword, el, password_check_rules, i18n_fallback);
......
......@@ -15,8 +15,7 @@ router.register(r'groups', api.UserGroupViewSet, 'user-group')
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('profile/', api.UserProfileApi.as_view(), name='user-profile'),
path('auth/', api.UserAuthApi.as_view(), name='user-auth'),
......@@ -31,5 +30,6 @@ urlpatterns = [
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'),
]
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
from django.contrib.auth import authenticate
from django.utils.translation import ugettext as _
from django.core.cache import cache
from datetime import datetime
from common.tasks import send_mail_async
from common.utils import reverse, get_object_or_none
from common.models import common_settings, Setting
from common.forms import SecuritySettingForm
from common.models import Setting
from .models import User, LoginLog
......@@ -109,6 +110,44 @@ def send_reset_password_mail(user):
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):
subject = _('SSH Key Reset')
recipient_list = [user.email]
......@@ -163,24 +202,6 @@ def check_user_valid(**kwargs):
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):
try:
ipaddress.ip_address(ip)
......@@ -275,57 +296,27 @@ def check_otp_code(otp_secret_key, otp_code):
def get_password_check_rules():
check_rules = []
min_length = settings.DEFAULT_PASSWORD_MIN_LENGTH
min_name = 'SECURITY_PASSWORD_MIN_LENGTH'
base_filed = SecuritySettingForm.base_fields
password_setting = Setting.objects.filter(name__startswith='SECURITY_PASSWORD')
if not password_setting:
# 用户还没有设置过密码校验规则
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
for rule in settings.SECURITY_PASSWORD_RULES:
key = "id_{}".format(rule.lower())
value = getattr(settings, rule)
if not value:
continue
check_rules.append({'key': key, 'value': int(value)})
return check_rules
def check_password_rules(password):
min_field_name = 'SECURITY_PASSWORD_MIN_LENGTH'
upper_field_name = 'SECURITY_PASSWORD_UPPER_CASE'
lower_field_name = 'SECURITY_PASSWORD_LOWER_CASE'
number_field_name = 'SECURITY_PASSWORD_NUMBER'
special_field_name = 'SECURITY_PASSWORD_SPECIAL_CHAR'
min_length = getattr(common_settings, min_field_name) or \
settings.DEFAULT_PASSWORD_MIN_LENGTH
password_setting = Setting.objects.filter(name__startswith='SECURITY_PASSWORD')
if not password_setting:
pattern = r"^.{" + str(min_length) + ",}$"
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`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'",\.<>\/\?]'
pattern = r"^"
if settings.SECURITY_PASSWORD_UPPER_CASE:
pattern += '(?=.*[A-Z])'
if settings.SECURITY_PASSWORD_LOWER_CASE:
pattern += '(?=.*[a-z])'
if settings.SECURITY_PASSWORD_NUMBER:
pattern += '(?=.*\d)'
if settings.SECURITY_PASSWORD_SPECIAL_CHAR:
pattern += '(?=.*[`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?])'
pattern += '[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?]'
pattern += '.{' + str(settings.SECURITY_PASSWORD_MIN_LENGTH-1) + ',}$'
match_obj = re.match(pattern, password)
return bool(match_obj)
......@@ -340,8 +331,7 @@ def increase_login_failed_count(username, ip):
count = cache.get(key_limit)
count = count + 1 if count else 1
limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME or \
settings.DEFAULT_LOGIN_LIMIT_TIME
limit_time = settings.SECURITY_LOGIN_LIMIT_TIME
cache.set(key_limit, count, int(limit_time)*60)
......@@ -357,10 +347,8 @@ def is_block_login(username, ip):
key_block = key_prefix_block.format(username)
count = cache.get(key_limit, 0)
limit_count = common_settings.SECURITY_LOGIN_LIMIT_COUNT or \
settings.DEFAULT_LOGIN_LIMIT_COUNT
limit_time = common_settings.SECURITY_LOGIN_LIMIT_TIME or \
settings.DEFAULT_LOGIN_LIMIT_TIME
limit_count = settings.SECURITY_LOGIN_LIMIT_COUNT
limit_time = settings.SECURITY_LOGIN_LIMIT_TIME
if count >= limit_count:
cache.set(key_block, 1, int(limit_time)*60)
......
......@@ -17,8 +17,8 @@ from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
from formtools.wizard.views import SessionWizardView
from django.conf import settings
from formtools.wizard.views import SessionWizardView
from common.utils import get_object_or_none, get_request_ip
from ..models import User, LoginLog
......@@ -68,7 +68,20 @@ class UserLoginView(FormView):
if not self.request.session.test_cookie_worked():
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')
ip = get_request_ip(self.request)
# 登陆成功,清除缓存计数
......@@ -78,10 +91,12 @@ class UserLoginView(FormView):
def form_invalid(self, form):
# write login failed log
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 = {
'username': username,
'mfa': LoginLog.MFA_UNKNOWN,
'reason': LoginLog.REASON_PASSWORD,
'reason': reason,
'status': False
}
self.write_login_log(data)
......@@ -128,6 +143,7 @@ class UserLoginView(FormView):
def get_context_data(self, **kwargs):
context = {
'demo_mode': os.environ.get("DEMO_MODE"),
'AUTH_OPENID': settings.AUTH_OPENID,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
......@@ -196,6 +212,9 @@ class UserLogoutView(TemplateView):
def get(self, request, *args, **kwargs):
auth_logout(request)
next_uri = request.COOKIES.get("next")
if next_uri:
return redirect(next_uri)
response = super().get(request, *args, **kwargs)
return response
......@@ -218,8 +237,11 @@ class UserForgotPasswordView(TemplateView):
email = request.POST.get('email')
user = get_object_or_none(User, email=email)
if not user:
return self.get(request, errors=_('Email address invalid, '
'please input again'))
error = _('Email address invalid, 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:
send_reset_password_mail(user)
return HttpResponseRedirect(
......@@ -251,8 +273,7 @@ class UserResetPasswordSuccessView(TemplateView):
'auto_redirect': True,
}
kwargs.update(context)
return super()\
.get_context_data(**kwargs)
return super().get_context_data(**kwargs)
class UserResetPasswordView(TemplateView):
......@@ -261,13 +282,11 @@ class UserResetPasswordView(TemplateView):
def get(self, request, *args, **kwargs):
token = request.GET.get('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:
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)
def post(self, request, *args, **kwargs):
......
......@@ -14,6 +14,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth import authenticate, login as auth_login
from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache
from django.conf import settings
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect
from django.urls import reverse_lazy, reverse
......@@ -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.mixins import JSONResponseMixin
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 orgs.utils import current_org
from .. import forms
......@@ -106,12 +106,11 @@ class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
success_message = update_success_msg
def get_context_data(self, **kwargs):
check_rules, min_length = get_password_check_rules()
check_rules = get_password_check_rules()
context = {
'app': _('Users'),
'action': _('Update user'),
'password_check_rules': check_rules,
'min_length': min_length
}
kwargs.update(context)
return super().get_context_data(**kwargs)
......@@ -362,7 +361,7 @@ class UserProfileView(LoginRequiredMixin, TemplateView):
template_name = 'users/user_profile.html'
def get_context_data(self, **kwargs):
mfa_setting = common_settings.SECURITY_MFA_AUTH
mfa_setting = settings.SECURITY_MFA_AUTH
context = {
'action': _('Profile'),
'mfa_setting': mfa_setting if mfa_setting is not None else False,
......@@ -399,12 +398,11 @@ class UserPasswordUpdateView(LoginRequiredMixin, UpdateView):
return self.request.user
def get_context_data(self, **kwargs):
check_rules, min_length = get_password_check_rules()
check_rules = get_password_check_rules()
context = {
'app': _('Users'),
'action': _('Password update'),
'password_check_rules': check_rules,
'min_length': min_length,
}
kwargs.update(context)
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
~~~~~~~~~~~~~~~~~
......@@ -13,46 +15,73 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
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!
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
ALLOWED_HOSTS = ['*']
# SECURITY WARNING: keep the bootstrap token used in production secret!
# 预共享Token coco和guacamole用来注册服务账号,不在使用原来的注册接受机制
BOOTSTRAP_TOKEN = 'PleaseChangeMe'
# 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/
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 ....
# 数据库设置
# 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')
# 使用单文件sqlite数据库
# DB_ENGINE = 'sqlite3'
# DB_NAME = os.path.join(BASE_DIR, 'data', 'db.sqlite3')
# MySQL or postgres setting like:
# DB_ENGINE = os.environ.get("DB_ENGINE") or 'mysql'
# DB_HOST = os.environ.get("DB_HOST") or '127.0.0.1'
# DB_PORT = os.environ.get("DB_PORT") or 3306
# DB_USER = os.environ.get("DB_USER") or 'jumpserver'
# DB_PASSWORD = os.environ.get("DB_PASSWORD") or 'weakPassword'
# DB_NAME = os.environ.get("DB_NAME") or 'jumpserver'
# 使用Mysql作为数据库
DB_ENGINE = 'mysql'
DB_HOST = '127.0.0.1'
DB_PORT = 3306
DB_USER = 'jumpserver'
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 = os.environ.get("REDIS_HOST") or '127.0.0.1'
REDIS_PORT = os.environ.get("REDIS_PORT") or 6379
REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD") or ''
REDIS_DB_CELERY = os.environ.get('REDIS_DB') or 3
REDIS_DB_CACHE = os.environ.get('REDIS_DB') or 4
# Redis配置
REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379
REDIS_PASSWORD = ''
# 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):
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():
'-A', 'ops',
'-l', LOG_LEVEL.lower(),
'--pidfile', pid_file,
'-c', str(WORKERS),
'--autoscale', '20,4',
]
if DAEMON:
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
ForgeryPy==0.1
greenlet==0.4.14
gunicorn==19.9.0
idna==2.7
idna==2.6
itsdangerous==0.24
itypes==1.1.0
Jinja2==2.10
......@@ -61,7 +61,7 @@ pytz==2018.3
PyYAML==3.12
redis==2.10.6
requests==2.18.4
jms-storage==0.0.19
jms-storage==0.0.20
s3transfer==0.1.13
simplejson==3.13.2
six==1.11.0
......@@ -74,3 +74,6 @@ Werkzeug==0.14.1
drf-nested-routers==0.90.2
aliyun-python-sdk-core-v3==2.9.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