diff --git a/Dockerfile b/Dockerfile
index 5e87bb604..769209673 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -22,10 +22,13 @@ COPY ./requirements/deb_buster_requirements.txt ./requirements/deb_buster_requir
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt update \
+ && apt -y install telnet iproute2 redis-tools \
&& grep -v '^#' ./requirements/deb_buster_requirements.txt | xargs apt -y install \
&& rm -rf /var/lib/apt/lists/* \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
- && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
+ && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
+ && sed -i "s@# alias l@alias l@g" ~/.bashrc \
+ && echo "set mouse-=a" > ~/.vimrc
COPY ./requirements/requirements.txt ./requirements/requirements.txt
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
@@ -37,6 +40,12 @@ COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
+RUN mkdir -p /opt/jumpserver/oracle/
+ADD https://f2c-north-rel.oss-cn-qingdao.aliyuncs.com/2.0/north/jumpserver/instantclient-basiclite-linux.x64-21.1.0.0.0.tar /opt/jumpserver/oracle/
+RUN tar xvf /opt/jumpserver/oracle/instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/
+RUN sh -c "echo /opt/jumpserver/oracle/instantclient_21_1 > /etc/ld.so.conf.d/oracle-instantclient.conf"
+RUN ldconfig
+
RUN echo > config.yml
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs
diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py
index d61e7ae23..bf47fa578 100644
--- a/apps/acls/models/login_asset_acl.py
+++ b/apps/acls/models/login_asset_acl.py
@@ -83,11 +83,11 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
@classmethod
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
- from tickets.const import TicketTypeChoices
+ from tickets.const import TicketType
from tickets.models import Ticket
data = {
'title': _('Login asset confirm') + ' ({})'.format(user),
- 'type': TicketTypeChoices.login_asset_confirm,
+ 'type': TicketType.login_asset_confirm,
'meta': {
'apply_login_user': str(user),
'apply_login_asset': str(asset),
@@ -96,7 +96,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
'org_id': org_id,
}
ticket = Ticket.objects.create(**data)
- ticket.assignees.set(assignees)
+ ticket.create_process_map_and_node(assignees)
ticket.open(applicant=user)
return ticket
diff --git a/apps/applications/api/account.py b/apps/applications/api/account.py
index 6c95a73c8..3193467c3 100644
--- a/apps/applications/api/account.py
+++ b/apps/applications/api/account.py
@@ -2,74 +2,57 @@
#
from django_filters import rest_framework as filters
-from django.db.models import F, Value, CharField
-from django.db.models.functions import Concat
-from django.http import Http404
+from django.db.models import F, Q
from common.drf.filters import BaseFilterSet
-from common.drf.api import JMSModelViewSet
-from common.utils import unique
-from perms.models import ApplicationPermission
+from common.drf.api import JMSBulkModelViewSet
+from ..models import Account
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
from .. import serializers
class AccountFilterSet(BaseFilterSet):
username = filters.CharFilter(field_name='username')
- app = filters.CharFilter(field_name='applications', lookup_expr='exact')
- app_name = filters.CharFilter(field_name='app_name', lookup_expr='exact')
+ type = filters.CharFilter(field_name='type', lookup_expr='exact')
+ category = filters.CharFilter(field_name='category', lookup_expr='exact')
+ app_display = filters.CharFilter(field_name='app_display', lookup_expr='exact')
class Meta:
- model = ApplicationPermission
- fields = ['type', 'category']
+ model = Account
+ fields = ['app', 'systemuser']
+
+ @property
+ def qs(self):
+ qs = super().qs
+ qs = self.filter_username(qs)
+ return qs
+
+ def filter_username(self, qs):
+ username = self.get_query_param('username')
+ if not username:
+ return qs
+ qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
+ return qs
-class ApplicationAccountViewSet(JMSModelViewSet):
- permission_classes = (IsOrgAdmin, )
- search_fields = ['username', 'app_name']
+class ApplicationAccountViewSet(JMSBulkModelViewSet):
+ model = Account
+ search_fields = ['username', 'app_display']
filterset_class = AccountFilterSet
- filterset_fields = ['username', 'app_name', 'type', 'category']
- serializer_class = serializers.ApplicationAccountSerializer
- http_method_names = ['get', 'put', 'patch', 'options']
+ filterset_fields = ['username', 'app_display', 'type', 'category', 'app']
+ serializer_class = serializers.AppAccountSerializer
+ permission_classes = (IsOrgAdmin,)
def get_queryset(self):
- queryset = ApplicationPermission.objects\
- .exclude(system_users__isnull=True) \
- .exclude(applications__isnull=True) \
- .annotate(uid=Concat(
- 'applications', Value('_'), 'system_users', output_field=CharField()
- )) \
- .annotate(systemuser=F('system_users')) \
- .annotate(systemuser_display=F('system_users__name')) \
- .annotate(username=F('system_users__username')) \
- .annotate(password=F('system_users__password')) \
- .annotate(app=F('applications')) \
- .annotate(app_name=F("applications__name")) \
- .values('username', 'password', 'systemuser', 'systemuser_display',
- 'app', 'app_name', 'category', 'type', 'uid', 'org_id')
- return queryset
-
- def get_object(self):
- obj = self.get_queryset().filter(
- uid=self.kwargs['pk']
- ).first()
- if not obj:
- raise Http404()
- return obj
-
- def filter_queryset(self, queryset):
- queryset = super().filter_queryset(queryset)
- queryset_list = unique(queryset, key=lambda x: (x['app'], x['systemuser']))
- return queryset_list
-
- @staticmethod
- def filter_spm_queryset(resource_ids, queryset):
- queryset = queryset.filter(uid__in=resource_ids)
+ queryset = Account.objects.all() \
+ .annotate(type=F('app__type')) \
+ .annotate(app_display=F('app__name')) \
+ .annotate(systemuser_display=F('systemuser__name')) \
+ .annotate(category=F('app__category'))
return queryset
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
- serializer_class = serializers.ApplicationAccountSecretSerializer
+ serializer_class = serializers.AppAccountSecretSerializer
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
http_method_names = ['get', 'options']
-
diff --git a/apps/applications/api/application.py b/apps/applications/api/application.py
index 235f0e789..1090e0095 100644
--- a/apps/applications/api/application.py
+++ b/apps/applications/api/application.py
@@ -6,11 +6,10 @@ from rest_framework.decorators import action
from rest_framework.response import Response
from common.tree import TreeNodeSerializer
-from ..hands import IsOrgAdminOrAppUser
+from ..hands import IsOrgAdminOrAppUser, IsValidUser
from .. import serializers
from ..models import Application
-
__all__ = ['ApplicationViewSet']
@@ -24,7 +23,7 @@ class ApplicationViewSet(OrgBulkModelViewSet):
search_fields = ('name', 'type', 'category')
permission_classes = (IsOrgAdminOrAppUser,)
serializer_classes = {
- 'default': serializers.ApplicationSerializer,
+ 'default': serializers.AppSerializer,
'get_tree': TreeNodeSerializer
}
@@ -35,3 +34,9 @@ class ApplicationViewSet(OrgBulkModelViewSet):
tree_nodes = Application.create_tree_nodes(queryset, show_count=show_count)
serializer = self.get_serializer(tree_nodes, many=True)
return Response(serializer.data)
+
+ @action(methods=['get'], detail=False, permission_classes=(IsValidUser,))
+ def suggestion(self, request):
+ queryset = self.filter_queryset(self.get_queryset())[:3]
+ serializer = self.get_serializer(queryset, many=True)
+ return Response(serializer.data)
diff --git a/apps/applications/migrations/0010_appaccount_historicalappaccount.py b/apps/applications/migrations/0010_appaccount_historicalappaccount.py
new file mode 100644
index 000000000..fc2cf2ab9
--- /dev/null
+++ b/apps/applications/migrations/0010_appaccount_historicalappaccount.py
@@ -0,0 +1,76 @@
+# Generated by Django 3.1.12 on 2021-08-26 09:07
+
+import assets.models.base
+import common.fields.model
+from django.conf import settings
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import simple_history.models
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('assets', '0076_delete_assetuser'),
+ ('applications', '0009_applicationuser'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='HistoricalAccount',
+ fields=[
+ ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
+ ('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
+ ('name', models.CharField(max_length=128, verbose_name='Name')),
+ ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
+ ('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
+ ('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
+ ('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
+ ('comment', models.TextField(blank=True, verbose_name='Comment')),
+ ('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
+ ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
+ ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
+ ('version', models.IntegerField(default=1, verbose_name='Version')),
+ ('history_id', models.AutoField(primary_key=True, serialize=False)),
+ ('history_date', models.DateTimeField()),
+ ('history_change_reason', models.CharField(max_length=100, null=True)),
+ ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
+ ('app', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='applications.application', verbose_name='Database')),
+ ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+ ('systemuser', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.systemuser', verbose_name='System user')),
+ ],
+ options={
+ 'verbose_name': 'historical Account',
+ 'ordering': ('-history_date', '-history_id'),
+ 'get_latest_by': 'history_date',
+ },
+ bases=(simple_history.models.HistoricalChanges, models.Model),
+ ),
+ migrations.CreateModel(
+ name='Account',
+ fields=[
+ ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=128, verbose_name='Name')),
+ ('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
+ ('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
+ ('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
+ ('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
+ ('comment', models.TextField(blank=True, verbose_name='Comment')),
+ ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
+ ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
+ ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
+ ('version', models.IntegerField(default=1, verbose_name='Version')),
+ ('app', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='applications.application', verbose_name='Database')),
+ ('systemuser', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser', verbose_name='System user')),
+ ],
+ options={
+ 'verbose_name': 'Account',
+ 'unique_together': {('username', 'app', 'systemuser')},
+ },
+ bases=(models.Model, assets.models.base.AuthMixin),
+ ),
+ ]
diff --git a/apps/applications/migrations/0011_auto_20210826_1759.py b/apps/applications/migrations/0011_auto_20210826_1759.py
new file mode 100644
index 000000000..937102c07
--- /dev/null
+++ b/apps/applications/migrations/0011_auto_20210826_1759.py
@@ -0,0 +1,40 @@
+# Generated by Django 3.1.12 on 2021-08-26 09:59
+
+from django.db import migrations, transaction
+from django.db.models import F
+
+
+def migrate_app_account(apps, schema_editor):
+ db_alias = schema_editor.connection.alias
+ app_perm_model = apps.get_model("perms", "ApplicationPermission")
+ app_account_model = apps.get_model("applications", 'Account')
+
+ queryset = app_perm_model.objects \
+ .exclude(system_users__isnull=True) \
+ .exclude(applications__isnull=True) \
+ .annotate(systemuser=F('system_users')) \
+ .annotate(app=F('applications')) \
+ .values('app', 'systemuser', 'org_id')
+
+ accounts = []
+ for p in queryset:
+ if not p['app']:
+ continue
+ account = app_account_model(
+ app_id=p['app'], systemuser_id=p['systemuser'],
+ version=1, org_id=p['org_id']
+ )
+ accounts.append(account)
+
+ app_account_model.objects.using(db_alias).bulk_create(accounts, ignore_conflicts=True)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('applications', '0010_appaccount_historicalappaccount'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate_app_account)
+ ]
diff --git a/apps/applications/models/__init__.py b/apps/applications/models/__init__.py
index a12310aa4..4bd20e32f 100644
--- a/apps/applications/models/__init__.py
+++ b/apps/applications/models/__init__.py
@@ -1 +1,2 @@
from .application import *
+from .account import *
diff --git a/apps/applications/models/account.py b/apps/applications/models/account.py
index e69de29bb..ff514590a 100644
--- a/apps/applications/models/account.py
+++ b/apps/applications/models/account.py
@@ -0,0 +1,88 @@
+from django.db import models
+from simple_history.models import HistoricalRecords
+from django.utils.translation import ugettext_lazy as _
+
+from common.utils import lazyproperty
+from assets.models.base import BaseUser
+
+
+class Account(BaseUser):
+ app = models.ForeignKey('applications.Application', on_delete=models.CASCADE, null=True, verbose_name=_('Database'))
+ systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user"))
+ version = models.IntegerField(default=1, verbose_name=_('Version'))
+ history = HistoricalRecords()
+
+ auth_attrs = ['username', 'password', 'private_key', 'public_key']
+
+ class Meta:
+ verbose_name = _('Account')
+ unique_together = [('username', 'app', 'systemuser')]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.auth_snapshot = {}
+
+ def get_or_systemuser_attr(self, attr):
+ val = getattr(self, attr, None)
+ if val:
+ return val
+ if self.systemuser:
+ return getattr(self.systemuser, attr, '')
+ return ''
+
+ def load_auth(self):
+ for attr in self.auth_attrs:
+ value = self.get_or_systemuser_attr(attr)
+ self.auth_snapshot[attr] = [getattr(self, attr), value]
+ setattr(self, attr, value)
+
+ def unload_auth(self):
+ if not self.systemuser:
+ return
+
+ for attr, values in self.auth_snapshot.items():
+ origin_value, loaded_value = values
+ current_value = getattr(self, attr, '')
+ if current_value == loaded_value:
+ setattr(self, attr, origin_value)
+
+ def save(self, *args, **kwargs):
+ self.unload_auth()
+ instance = super().save(*args, **kwargs)
+ self.load_auth()
+ return instance
+
+ @lazyproperty
+ def category(self):
+ return self.app.category
+
+ @lazyproperty
+ def type(self):
+ return self.app.type
+
+ @lazyproperty
+ def app_display(self):
+ return self.systemuser.name
+
+ @property
+ def username_display(self):
+ return self.get_or_systemuser_attr('username') or ''
+
+ @lazyproperty
+ def systemuser_display(self):
+ if not self.systemuser:
+ return ''
+ return str(self.systemuser)
+
+ @property
+ def smart_name(self):
+ username = self.username_display
+
+ if self.app:
+ app = str(self.app)
+ else:
+ app = '*'
+ return '{}@{}'.format(username, app)
+
+ def __str__(self):
+ return self.smart_name
diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py
index b88fcef21..79cbdd3bc 100644
--- a/apps/applications/serializers/application.py
+++ b/apps/applications/serializers/application.py
@@ -4,20 +4,23 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
-from orgs.models import Organization
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
+from assets.serializers.base import AuthSerializerMixin
from common.drf.serializers import MethodSerializer
-from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping
+from .attrs import (
+ category_serializer_classes_mapping,
+ type_serializer_classes_mapping
+)
from .. import models
from .. import const
__all__ = [
- 'ApplicationSerializer', 'ApplicationSerializerMixin',
- 'ApplicationAccountSerializer', 'ApplicationAccountSecretSerializer'
+ 'AppSerializer', 'AppSerializerMixin',
+ 'AppAccountSerializer', 'AppAccountSecretSerializer'
]
-class ApplicationSerializerMixin(serializers.Serializer):
+class AppSerializerMixin(serializers.Serializer):
attrs = MethodSerializer()
def get_attrs_serializer(self):
@@ -45,8 +48,14 @@ class ApplicationSerializerMixin(serializers.Serializer):
serializer = serializer_class
return serializer
+ def create(self, validated_data):
+ return super().create(validated_data)
-class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSerializer):
+ def update(self, instance, validated_data):
+ return super().update(instance, validated_data)
+
+
+class AppSerializer(AppSerializerMixin, BulkOrgResourceModelSerializer):
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
@@ -69,42 +78,54 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
return _attrs
-class ApplicationAccountSerializer(serializers.Serializer):
- id = serializers.ReadOnlyField(label=_("Id"), source='uid')
- username = serializers.ReadOnlyField(label=_("Username"))
- password = serializers.CharField(write_only=True, label=_("Password"))
- systemuser = serializers.ReadOnlyField(label=_('System user'))
- systemuser_display = serializers.ReadOnlyField(label=_("System user display"))
- app = serializers.ReadOnlyField(label=_('App'))
- app_name = serializers.ReadOnlyField(label=_("Application name"), read_only=True)
+class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True)
category_display = serializers.SerializerMethodField(label=_('Category display'))
type = serializers.ChoiceField(label=_('Type'), choices=const.AppType.choices, read_only=True)
type_display = serializers.SerializerMethodField(label=_('Type display'))
- uid = serializers.ReadOnlyField(label=_("Union id"))
- org_id = serializers.ReadOnlyField(label=_("Organization"))
- org_name = serializers.SerializerMethodField(label=_("Org name"))
category_mapper = dict(const.AppCategory.choices)
type_mapper = dict(const.AppType.choices)
- def create(self, validated_data):
- pass
-
- def update(self, instance, validated_data):
- pass
+ class Meta:
+ model = models.Account
+ fields_mini = ['id', 'username', 'version']
+ fields_write_only = ['password', 'private_key']
+ fields_fk = ['systemuser', 'systemuser_display', 'app', 'app_display']
+ fields = fields_mini + fields_fk + fields_write_only + [
+ 'type', 'type_display', 'category', 'category_display',
+ ]
+ extra_kwargs = {
+ 'username': {'default': '', 'required': False},
+ 'password': {'write_only': True},
+ 'app_display': {'label': _('Application display')}
+ }
+ use_model_bulk_create = True
+ model_bulk_create_kwargs = {
+ 'ignore_conflicts': True
+ }
def get_category_display(self, obj):
- return self.category_mapper.get(obj['category'])
+ return self.category_mapper.get(obj.category)
def get_type_display(self, obj):
- return self.type_mapper.get(obj['type'])
+ return self.type_mapper.get(obj.type)
- @staticmethod
- def get_org_name(obj):
- org = Organization.get_instance(obj['org_id'])
- return org.name
+ @classmethod
+ def setup_eager_loading(cls, queryset):
+ """ Perform necessary eager loading of data. """
+ queryset = queryset.prefetch_related('systemuser', 'app')
+ return queryset
+
+ def to_representation(self, instance):
+ instance.load_auth()
+ return super().to_representation(instance)
-class ApplicationAccountSecretSerializer(ApplicationAccountSerializer):
- password = serializers.CharField(write_only=False, label=_("Password"))
+class AppAccountSecretSerializer(AppAccountSerializer):
+ class Meta(AppAccountSerializer.Meta):
+ extra_kwargs = {
+ 'password': {'write_only': False},
+ 'private_key': {'write_only': False},
+ 'public_key': {'write_only': False},
+ }
diff --git a/apps/assets/api/accounts.py b/apps/assets/api/accounts.py
index 778916e64..e05bee3d2 100644
--- a/apps/assets/api/accounts.py
+++ b/apps/assets/api/accounts.py
@@ -64,8 +64,8 @@ class AccountViewSet(OrgBulkModelViewSet):
permission_classes = (IsOrgAdmin,)
def get_queryset(self):
- queryset = super().get_queryset()\
- .annotate(ip=F('asset__ip'))\
+ queryset = super().get_queryset() \
+ .annotate(ip=F('asset__ip')) \
.annotate(hostname=F('asset__hostname'))
return queryset
@@ -110,4 +110,5 @@ class AccountTaskCreateAPI(CreateAPIView):
def get_exception_handler(self):
def handler(e, context):
return Response({"error": str(e)}, status=400)
+
return handler
diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py
index 788b91257..35023c39d 100644
--- a/apps/assets/api/asset.py
+++ b/apps/assets/api/asset.py
@@ -1,15 +1,17 @@
# -*- coding: utf-8 -*-
#
from assets.api import FilterAssetByNodeMixin
+from rest_framework.decorators import action
from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import RetrieveAPIView
+from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from common.utils import get_logger, get_object_or_none
-from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
+from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser, IsValidUser
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
-from ..models import Asset, Node, Platform, SystemUser
+from ..models import Asset, Node, Platform
from .. import serializers
from ..tasks import (
update_assets_hardware_info_manual, test_assets_connectivity_manual,
@@ -17,7 +19,6 @@ from ..tasks import (
)
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
-
logger = get_logger(__file__)
__all__ = [
'AssetViewSet', 'AssetPlatformRetrieveApi',
@@ -43,6 +44,7 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
serializer_classes = {
'default': serializers.AssetSerializer,
+ 'suggestion': serializers.MiniAssetSerializer
}
permission_classes = (IsOrgAdminOrAppUser,)
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
@@ -62,6 +64,12 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
assets = serializer.save()
self.set_assets_node(assets)
+ @action(methods=['get'], detail=False, permission_classes=(IsValidUser,))
+ def suggestion(self, request):
+ queryset = self.filter_queryset(self.get_queryset())[:3]
+ serializer = self.get_serializer(queryset, many=True)
+ return Response(serializer.data)
+
class AssetPlatformRetrieveApi(RetrieveAPIView):
queryset = Platform.objects.all()
diff --git a/apps/assets/api/system_user_relation.py b/apps/assets/api/system_user_relation.py
index cd7b64a68..374d45cc2 100644
--- a/apps/assets/api/system_user_relation.py
+++ b/apps/assets/api/system_user_relation.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
from collections import defaultdict
-from django.db.models import F, Value
+from django.db.models import F, Value, Model
from django.db.models.signals import m2m_changed
from django.db.models.functions import Concat
@@ -13,13 +13,15 @@ from .. import models, serializers
__all__ = [
'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet',
- 'SystemUserUserRelationViewSet',
+ 'SystemUserUserRelationViewSet', 'BaseRelationViewSet',
]
logger = get_logger(__name__)
class RelationMixin:
+ model: Model
+
def get_queryset(self):
queryset = self.model.objects.all()
if not current_org.is_root():
diff --git a/apps/assets/migrations/0071_systemuser_type.py b/apps/assets/migrations/0071_systemuser_type.py
index c7a3a88fe..8ca48370a 100644
--- a/apps/assets/migrations/0071_systemuser_type.py
+++ b/apps/assets/migrations/0071_systemuser_type.py
@@ -1,7 +1,8 @@
# Generated by Django 3.1.6 on 2021-06-04 16:46
-
+import uuid
from django.db import migrations, models, transaction
import django.db.models.deletion
+from django.db import IntegrityError
from django.db.models import F
@@ -15,7 +16,7 @@ def migrate_admin_user_to_system_user(apps, schema_editor):
for admin_user in admin_users:
kwargs = {}
for attr in [
- 'id', 'org_id', 'username', 'password', 'private_key', 'public_key',
+ 'org_id', 'username', 'password', 'private_key', 'public_key',
'comment', 'date_created', 'date_updated', 'created_by',
]:
value = getattr(admin_user, attr)
@@ -27,7 +28,16 @@ def migrate_admin_user_to_system_user(apps, schema_editor):
).exists()
if exist:
name = admin_user.name + '_' + str(admin_user.id)[:5]
+
+ i = admin_user.id
+ exist = system_user_model.objects.using(db_alias).filter(
+ id=i, org_id=admin_user.org_id
+ ).exists()
+ if exist:
+ i = uuid.uuid4()
+
kwargs.update({
+ 'id': i,
'name': name,
'type': 'admin',
'protocol': 'ssh',
@@ -36,7 +46,11 @@ def migrate_admin_user_to_system_user(apps, schema_editor):
with transaction.atomic():
s = system_user_model(**kwargs)
- s.save()
+ try:
+ s.save()
+ except IntegrityError:
+ s.id = None
+ s.save()
print(" Migrate admin user to system user: {} => {}".format(admin_user.name, s.name))
assets = admin_user.assets.all()
s.assets.set(assets)
diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py
index 3153608cd..c4e39bcd4 100644
--- a/apps/assets/models/authbook.py
+++ b/apps/assets/models/authbook.py
@@ -16,7 +16,6 @@ class AuthBook(BaseUser, AbsConnectivity):
systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user"))
version = models.IntegerField(default=1, verbose_name=_('Version'))
history = HistoricalRecords()
- _systemuser_display = ''
auth_attrs = ['username', 'password', 'private_key', 'public_key']
@@ -64,8 +63,6 @@ class AuthBook(BaseUser, AbsConnectivity):
@lazyproperty
def systemuser_display(self):
- if self._systemuser_display:
- return self._systemuser_display
if not self.systemuser:
return ''
return str(self.systemuser)
diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py
index 1ef14bad0..bf91a16b2 100644
--- a/apps/assets/models/cmd_filter.py
+++ b/apps/assets/models/cmd_filter.py
@@ -105,11 +105,11 @@ class CommandFilterRule(OrgModelMixin):
return '{} % {}'.format(self.type, self.content)
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
- from tickets.const import TicketTypeChoices
+ from tickets.const import TicketType
from tickets.models import Ticket
data = {
'title': _('Command confirm') + ' ({})'.format(session.user),
- 'type': TicketTypeChoices.command_confirm,
+ 'type': TicketType.command_confirm,
'meta': {
'apply_run_user': session.user,
'apply_run_asset': session.asset,
@@ -122,6 +122,6 @@ class CommandFilterRule(OrgModelMixin):
'org_id': org_id,
}
ticket = Ticket.objects.create(**data)
- ticket.assignees.set(self.reviewers.all())
+ ticket.create_process_map_and_node(self.reviewers.all())
ticket.open(applicant=session.user_obj)
return ticket
diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py
index ee802e311..3677144c2 100644
--- a/apps/assets/models/user.py
+++ b/apps/assets/models/user.py
@@ -73,6 +73,10 @@ class ProtocolMixin:
def can_perm_to_asset(self):
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
+ @property
+ def is_asset_protocol(self):
+ return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
+
class AuthMixin:
username_same_with_user: bool
diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py
index f387df071..8266c69c2 100644
--- a/apps/assets/serializers/asset.py
+++ b/apps/assets/serializers/asset.py
@@ -8,7 +8,7 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Asset, Node, Platform, SystemUser
__all__ = [
- 'AssetSerializer', 'AssetSimpleSerializer',
+ 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
'ProtocolsField', 'PlatformSerializer',
'AssetTaskSerializer', 'AssetsTaskSerializer', 'ProtocolsField'
]
@@ -69,6 +69,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
"""
资产的数据结构
"""
+
class Meta:
model = Asset
fields_mini = ['id', 'hostname', 'ip', 'platform', 'protocols']
@@ -157,6 +158,12 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
return instance
+class MiniAssetSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Asset
+ fields = AssetSerializer.Meta.fields_mini
+
+
class PlatformSerializer(serializers.ModelSerializer):
meta = serializers.DictField(required=False, allow_null=True, label=_('Meta'))
@@ -177,7 +184,6 @@ class PlatformSerializer(serializers.ModelSerializer):
class AssetSimpleSerializer(serializers.ModelSerializer):
-
class Meta:
model = Asset
fields = ['id', 'hostname', 'ip', 'port', 'connectivity', 'date_verified']
diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py
index c5b9c2064..69848ce20 100644
--- a/apps/assets/serializers/system_user.py
+++ b/apps/assets/serializers/system_user.py
@@ -14,7 +14,7 @@ __all__ = [
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
- 'SystemUserTempAuthSerializer',
+ 'SystemUserTempAuthSerializer', 'RelationMixin',
]
@@ -31,12 +31,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'public_key', 'private_key']
fields_small = fields_mini + fields_write_only + [
- 'type', 'type_display', 'protocol', 'login_mode', 'login_mode_display',
- 'priority', 'sudo', 'shell', 'sftp_root', 'token', 'ssh_key_fingerprint',
- 'home', 'system_groups', 'ad_domain',
+ 'token', 'ssh_key_fingerprint',
+ 'type', 'type_display', 'protocol', 'is_asset_protocol',
+ 'login_mode', 'login_mode_display', 'priority',
+ 'sudo', 'shell', 'sftp_root', 'home', 'system_groups', 'ad_domain',
'username_same_with_user', 'auto_push', 'auto_generate_key',
- 'date_created', 'date_updated',
- 'comment', 'created_by',
+ 'date_created', 'date_updated', 'comment', 'created_by',
]
fields_m2m = ['cmd_filters', 'assets_amount']
fields = fields_small + fields_m2m
@@ -53,6 +53,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True},
'ad_domain': {'required': False, 'allow_blank': True, 'label': _('Ad domain')},
+ 'is_asset_protocol': {'label': _('Is asset protocol')}
}
def validate_auto_push(self, value):
diff --git a/apps/assets/signals_handler/system_user.py b/apps/assets/signals_handler/system_user.py
index 9e1ee2045..00111030c 100644
--- a/apps/assets/signals_handler/system_user.py
+++ b/apps/assets/signals_handler/system_user.py
@@ -131,8 +131,8 @@ def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
@on_transaction_commit
def on_system_user_update(instance: SystemUser, created, **kwargs):
"""
- 当系统用户更新时,可能更新了秘钥,用户名等,这时要自动推送系统用户到资产上,
- 其实应该当 用户名,密码,秘钥 sudo等更新时再推送,这里偷个懒,
+ 当系统用户更新时,可能更新了密钥,用户名等,这时要自动推送系统用户到资产上,
+ 其实应该当 用户名,密码,密钥 sudo等更新时再推送,这里偷个懒,
这里直接取了 instance.assets 因为nodes和系统用户发生变化时,会自动将nodes下的资产
关联到上面
"""
diff --git a/apps/audits/tasks.py b/apps/audits/tasks.py
index 467967b82..171fbe633 100644
--- a/apps/audits/tasks.py
+++ b/apps/audits/tasks.py
@@ -5,14 +5,12 @@ from django.utils import timezone
from celery import shared_task
from ops.celery.decorator import (
- register_as_period_task, after_app_shutdown_clean_periodic
+ register_as_period_task
)
from .models import UserLoginLog, OperateLog
from common.utils import get_log_keep_day
-@shared_task
-@after_app_shutdown_clean_periodic
def clean_login_log_period():
now = timezone.now()
days = get_log_keep_day('LOGIN_LOG_KEEP_DAYS')
@@ -20,8 +18,6 @@ def clean_login_log_period():
UserLoginLog.objects.filter(datetime__lt=expired_day).delete()
-@shared_task
-@after_app_shutdown_clean_periodic
def clean_operation_log_period():
now = timezone.now()
days = get_log_keep_day('OPERATE_LOG_KEEP_DAYS')
@@ -29,7 +25,6 @@ def clean_operation_log_period():
OperateLog.objects.filter(datetime__lt=expired_day).delete()
-@shared_task
def clean_ftp_log_period():
now = timezone.now()
days = get_log_keep_day('FTP_LOG_KEEP_DAYS')
diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py
index 383a87c32..f13254b45 100644
--- a/apps/authentication/api/connection_token.py
+++ b/apps/authentication/api/connection_token.py
@@ -74,6 +74,7 @@ class ClientProtocolMixin:
'bookmarktype:i': '3',
'use redirection server name:i': '0',
'smart sizing:i': '0',
+ #'drivestoredirect:s': '*',
# 'domain:s': ''
# 'alternate shell:s:': '||MySQLWorkbench',
# 'remoteapplicationname:s': 'Firefox',
@@ -84,8 +85,11 @@ class ClientProtocolMixin:
height = self.request.query_params.get('height')
width = self.request.query_params.get('width')
full_screen = is_true(self.request.query_params.get('full_screen'))
+ drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
token = self.create_token(user, asset, application, system_user)
+ if drives_redirect:
+ options['drivestoredirect:s'] = '*'
options['screen mode id:i'] = '2' if full_screen else '1'
address = settings.TERMINAL_RDP_ADDR
if not address or address == 'localhost:3389':
diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py
index 527e473d8..93b33c4f9 100644
--- a/apps/authentication/api/login_confirm.py
+++ b/apps/authentication/api/login_confirm.py
@@ -45,5 +45,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
ticket = self.get_ticket()
if ticket:
request.session.pop('auth_ticket_id', '')
- ticket.close(processor=request.user)
+ ticket.close(processor=self.get_user_from_session())
return Response('', status=200)
diff --git a/apps/authentication/api/mfa.py b/apps/authentication/api/mfa.py
index b81eeee29..f067d5c5c 100644
--- a/apps/authentication/api/mfa.py
+++ b/apps/authentication/api/mfa.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
+import builtins
import time
from django.utils.translation import ugettext as _
from django.conf import settings
@@ -8,14 +9,28 @@ from rest_framework.generics import CreateAPIView
from rest_framework.serializers import ValidationError
from rest_framework.response import Response
-from common.permissions import IsValidUser, NeedMFAVerify
+from authentication.sms_verify_code import VerifyCodeUtil
+from common.exceptions import JMSException
+from common.permissions import IsValidUser, NeedMFAVerify, IsAppUser
+from users.models.user import MFAType
from ..serializers import OtpVerifySerializer
from .. import serializers
from .. import errors
from ..mixins import AuthMixin
-__all__ = ['MFAChallengeApi', 'UserOtpVerifyApi']
+__all__ = ['MFAChallengeApi', 'UserOtpVerifyApi', 'SendSMSVerifyCodeApi', 'MFASelectTypeApi']
+
+
+class MFASelectTypeApi(AuthMixin, CreateAPIView):
+ permission_classes = (AllowAny,)
+ serializer_class = serializers.MFASelectTypeSerializer
+
+ def perform_create(self, serializer):
+ mfa_type = serializer.validated_data['type']
+ if mfa_type == MFAType.SMS_CODE:
+ user = self.get_user_from_session()
+ user.send_sms_code()
class MFAChallengeApi(AuthMixin, CreateAPIView):
@@ -26,7 +41,9 @@ class MFAChallengeApi(AuthMixin, CreateAPIView):
try:
user = self.get_user_from_session()
code = serializer.validated_data.get('code')
- valid = user.check_mfa(code)
+ mfa_type = serializer.validated_data.get('type', MFAType.OTP)
+
+ valid = user.check_mfa(code, mfa_type=mfa_type)
if not valid:
self.request.session['auth_mfa'] = ''
raise errors.MFAFailedError(
@@ -67,3 +84,12 @@ class UserOtpVerifyApi(CreateAPIView):
if self.request.method.lower() == 'get' and settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [NeedMFAVerify]
return super().get_permissions()
+
+
+class SendSMSVerifyCodeApi(AuthMixin, CreateAPIView):
+ permission_classes = (AllowAny,)
+
+ def create(self, request, *args, **kwargs):
+ user = self.get_user_from_session()
+ timeout = user.send_sms_code()
+ return Response({'code': 'ok','timeout': timeout})
diff --git a/apps/authentication/backends/cas/__init__.py b/apps/authentication/backends/cas/__init__.py
index bf0101c81..bbdbdb814 100644
--- a/apps/authentication/backends/cas/__init__.py
+++ b/apps/authentication/backends/cas/__init__.py
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
from .backends import *
-from .callback import *
diff --git a/apps/authentication/backends/cas/callback.py b/apps/authentication/backends/cas/callback.py
deleted file mode 100644
index 64201e607..000000000
--- a/apps/authentication/backends/cas/callback.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-from django.contrib.auth import get_user_model
-
-
-User = get_user_model()
-
-
-def cas_callback(response):
- username = response['username']
- user, user_created = User.objects.get_or_create(username=username)
- profile, created = user.get_profile()
-
- profile.role = response['attributes']['role']
- profile.birth_date = response['attributes']['birth_date']
- profile.save()
diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py
index ad8148182..c8005ba95 100644
--- a/apps/authentication/errors.py
+++ b/apps/authentication/errors.py
@@ -4,9 +4,11 @@ from django.utils.translation import ugettext_lazy as _
from django.urls import reverse
from django.conf import settings
+from authentication import sms_verify_code
from common.exceptions import JMSException
from .signals import post_auth_failed
from users.utils import LoginBlockUtil, MFABlockUtils
+from users.models import MFAType
reason_password_failed = 'password_failed'
reason_password_decrypt_failed = 'password_decrypt_failed'
@@ -58,8 +60,18 @@ block_mfa_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
-mfa_failed_msg = _(
- "MFA code invalid, or ntp sync server time, "
+otp_failed_msg = _(
+ "One-time password invalid, or ntp sync server time, "
+ "You can also try {times_try} times "
+ "(The account will be temporarily locked for {block_time} minutes)"
+)
+sms_failed_msg = _(
+ "SMS verify code invalid,"
+ "You can also try {times_try} times "
+ "(The account will be temporarily locked for {block_time} minutes)"
+)
+mfa_type_failed_msg = _(
+ "The MFA type({mfa_type}) is not supported"
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
@@ -134,7 +146,7 @@ class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
error = reason_mfa_failed
msg: str
- def __init__(self, username, request, ip):
+ def __init__(self, username, request, ip, mfa_type=MFAType.OTP):
util = MFABlockUtils(username, ip)
util.incr_failed_count()
@@ -142,9 +154,18 @@ class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder:
- self.msg = mfa_failed_msg.format(
- times_try=times_remainder, block_time=block_time
- )
+ if mfa_type == MFAType.OTP:
+ self.msg = otp_failed_msg.format(
+ times_try=times_remainder, block_time=block_time
+ )
+ elif mfa_type == MFAType.SMS_CODE:
+ self.msg = sms_failed_msg.format(
+ times_try=times_remainder, block_time=block_time
+ )
+ else:
+ self.msg = mfa_type_failed_msg.format(
+ mfa_type=mfa_type, times_try=times_remainder, block_time=block_time
+ )
else:
self.msg = block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, request=request)
@@ -202,12 +223,16 @@ class MFARequiredError(NeedMoreInfoError):
msg = mfa_required_msg
error = 'mfa_required'
+ def __init__(self, error='', msg='', mfa_types=tuple(MFAType)):
+ super().__init__(error=error, msg=msg)
+ self.choices = mfa_types
+
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
'data': {
- 'choices': ['code'],
+ 'choices': self.choices,
'url': reverse('api-auth:mfa-challenge')
}
}
diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py
index a4b07700c..948ceabff 100644
--- a/apps/authentication/forms.py
+++ b/apps/authentication/forms.py
@@ -43,7 +43,8 @@ class UserLoginForm(forms.Form):
class UserCheckOtpCodeForm(forms.Form):
- otp_code = forms.CharField(label=_('MFA code'), max_length=6)
+ code = forms.CharField(label=_('MFA Code'), max_length=6)
+ mfa_type = forms.CharField(label=_('MFA type'), max_length=6)
class CustomCaptchaTextInput(CaptchaTextInput):
diff --git a/apps/authentication/middleware.py b/apps/authentication/middleware.py
new file mode 100644
index 000000000..59eabff75
--- /dev/null
+++ b/apps/authentication/middleware.py
@@ -0,0 +1,14 @@
+from django.shortcuts import redirect
+
+
+class MFAMiddleware:
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ def __call__(self, request):
+ response = self.get_response(request)
+ if request.path.find('/auth/login/otp/') > -1:
+ return response
+ if request.session.get('auth_mfa_required'):
+ return redirect('authentication:login-otp')
+ return response
diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py
index 1eb39fee1..3a2f9c09b 100644
--- a/apps/authentication/mixins.py
+++ b/apps/authentication/mixins.py
@@ -14,14 +14,15 @@ from django.contrib.auth import (
PermissionDenied, user_login_failed, _clean_credentials
)
from django.shortcuts import reverse, redirect
+from django.views.generic.edit import FormView
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
-from users.models import User
+from users.models import User, MFAType
from users.utils import LoginBlockUtil, MFABlockUtils
from . import errors
-from .utils import rsa_decrypt
+from .utils import rsa_decrypt, gen_key_pair
from .signals import post_auth_success, post_auth_failed
-from .const import RSA_PRIVATE_KEY
+from .const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
logger = get_logger(__name__)
@@ -79,7 +80,70 @@ def authenticate(request=None, **credentials):
auth.authenticate = authenticate
-class AuthMixin:
+class PasswordEncryptionViewMixin:
+ request = None
+
+ def get_decrypted_password(self, password=None, username=None):
+ request = self.request
+ if hasattr(request, 'data'):
+ data = request.data
+ else:
+ data = request.POST
+
+ username = username or data.get('username')
+ password = password or data.get('password')
+
+ password = self.decrypt_passwd(password)
+ if not password:
+ self.raise_password_decrypt_failed(username=username)
+ return password
+
+ def raise_password_decrypt_failed(self, username):
+ ip = self.get_request_ip()
+ raise errors.CredentialError(
+ error=errors.reason_password_decrypt_failed,
+ username=username, ip=ip, request=self.request
+ )
+
+ def decrypt_passwd(self, raw_passwd):
+ # 获取解密密钥,对密码进行解密
+ rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
+ if rsa_private_key is not None:
+ try:
+ return rsa_decrypt(raw_passwd, rsa_private_key)
+ except Exception as e:
+ logger.error(e, exc_info=True)
+ logger.error(
+ f'Decrypt password failed: password[{raw_passwd}] '
+ f'rsa_private_key[{rsa_private_key}]'
+ )
+ return None
+ return raw_passwd
+
+ def get_request_ip(self):
+ ip = ''
+ if hasattr(self.request, 'data'):
+ ip = self.request.data.get('remote_addr', '')
+ ip = ip or get_request_ip(self.request)
+ return ip
+
+ def get_context_data(self, **kwargs):
+ # 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用
+ rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
+ rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
+ if not all((rsa_private_key, rsa_public_key)):
+ rsa_private_key, rsa_public_key = gen_key_pair()
+ rsa_public_key = rsa_public_key.replace('\n', '\\n')
+ self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
+ self.request.session[RSA_PUBLIC_KEY] = rsa_public_key
+
+ kwargs.update({
+ 'rsa_public_key': rsa_public_key,
+ })
+ return super().get_context_data(**kwargs)
+
+
+class AuthMixin(PasswordEncryptionViewMixin):
request = None
partial_credential_error = None
@@ -106,13 +170,6 @@ class AuthMixin:
user.backend = self.request.session.get("auth_backend")
return user
- def get_request_ip(self):
- ip = ''
- if hasattr(self.request, 'data'):
- ip = self.request.data.get('remote_addr', '')
- ip = ip or get_request_ip(self.request)
- return ip
-
def _check_is_block(self, username, raise_exception=True):
ip = self.get_request_ip()
if LoginBlockUtil(username, ip).is_block():
@@ -130,19 +187,6 @@ class AuthMixin:
username = self.request.POST.get("username")
self._check_is_block(username, raise_exception)
- def decrypt_passwd(self, raw_passwd):
- # 获取解密密钥,对密码进行解密
- rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
- if rsa_private_key is not None:
- try:
- return rsa_decrypt(raw_passwd, rsa_private_key)
- except Exception as e:
- logger.error(e, exc_info=True)
- logger.error(f'Decrypt password failed: password[{raw_passwd}] '
- f'rsa_private_key[{rsa_private_key}]')
- return None
- return raw_passwd
-
def raise_credential_error(self, error):
raise self.partial_credential_error(error=error)
@@ -158,14 +202,12 @@ class AuthMixin:
items = ['username', 'password', 'challenge', 'public_key', 'auto_login']
username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='')
- password = password + challenge.strip()
ip = self.get_request_ip()
self._set_partial_credential_error(username=username, ip=ip, request=request)
+ password = password + challenge.strip()
if decrypt_passwd:
- password = self.decrypt_passwd(password)
- if not password:
- self.raise_credential_error(errors.reason_password_decrypt_failed)
+ password = self.get_decrypted_password()
return username, password, public_key, ip, auto_login
def _check_only_allow_exists_user_auth(self, username):
@@ -309,12 +351,13 @@ class AuthMixin:
unset, url = user.mfa_enabled_but_not_set()
if unset:
raise errors.MFAUnsetError(user, self.request, url)
- raise errors.MFARequiredError()
+ raise errors.MFARequiredError(mfa_types=user.get_supported_mfa_types())
- def mark_mfa_ok(self):
+ def mark_mfa_ok(self, mfa_type=MFAType.OTP):
self.request.session['auth_mfa'] = 1
self.request.session['auth_mfa_time'] = time.time()
- self.request.session['auth_mfa_type'] = 'otp'
+ self.request.session['auth_mfa_required'] = ''
+ self.request.session['auth_mfa_type'] = mfa_type
def check_mfa_is_block(self, username, ip, raise_exception=True):
if MFABlockUtils(username, ip).is_block():
@@ -325,11 +368,11 @@ class AuthMixin:
else:
return exception
- def check_user_mfa(self, code):
+ def check_user_mfa(self, code, mfa_type=MFAType.OTP):
user = self.get_user_from_session()
ip = self.get_request_ip()
self.check_mfa_is_block(user.username, ip)
- ok = user.check_mfa(code)
+ ok = user.check_mfa(code, mfa_type=mfa_type)
if ok:
self.mark_mfa_ok()
return
@@ -337,7 +380,7 @@ class AuthMixin:
raise errors.MFAFailedError(
username=user.username,
request=self.request,
- ip=ip
+ ip=ip, mfa_type=mfa_type,
)
def get_ticket(self):
@@ -363,14 +406,14 @@ class AuthMixin:
raise errors.LoginConfirmOtherError('', "Not found")
if ticket.status_open:
raise errors.LoginConfirmWaitError(ticket.id)
- elif ticket.action_approve:
+ elif ticket.state_approve:
self.request.session["auth_confirm"] = "1"
return
- elif ticket.action_reject:
+ elif ticket.state_reject:
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_action_display()
)
- elif ticket.action_close:
+ elif ticket.state_close:
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_action_display()
)
@@ -391,7 +434,6 @@ class AuthMixin:
def clear_auth_mark(self):
self.request.session['auth_password'] = ''
self.request.session['auth_user_id'] = ''
- self.request.session['auth_mfa'] = ''
self.request.session['auth_confirm'] = ''
self.request.session['auth_ticket_id'] = ''
diff --git a/apps/authentication/models.py b/apps/authentication/models.py
index f4db736af..c74d06953 100644
--- a/apps/authentication/models.py
+++ b/apps/authentication/models.py
@@ -71,15 +71,14 @@ class LoginConfirmSetting(CommonModelMixin):
from orgs.models import Organization
ticket_title = _('Login confirm') + ' {}'.format(self.user)
ticket_meta = self.construct_confirm_ticket_meta(request)
- ticket_assignees = self.reviewers.all()
data = {
'title': ticket_title,
- 'type': const.TicketTypeChoices.login_confirm.value,
+ 'type': const.TicketType.login_confirm.value,
'meta': ticket_meta,
'org_id': Organization.ROOT_ID,
}
ticket = Ticket.objects.create(**data)
- ticket.assignees.set(ticket_assignees)
+ ticket.create_process_map_and_node(self.reviewers.all())
ticket.open(self.user)
return ticket
diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py
index 989d94d62..b571dea01 100644
--- a/apps/authentication/serializers.py
+++ b/apps/authentication/serializers.py
@@ -16,8 +16,8 @@ from .models import AccessKey, LoginConfirmSetting
__all__ = [
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
- 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer',
- 'PasswordVerifySerializer',
+ 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer',
+ 'PasswordVerifySerializer', 'MFASelectTypeSerializer',
]
@@ -77,6 +77,10 @@ class BearerTokenSerializer(serializers.Serializer):
return instance
+class MFASelectTypeSerializer(serializers.Serializer):
+ type = serializers.CharField()
+
+
class MFAChallengeSerializer(serializers.Serializer):
type = serializers.CharField(write_only=True, required=False, allow_blank=True)
code = serializers.CharField(write_only=True)
@@ -166,7 +170,7 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
- fields = ['id', 'name', 'username', 'password', 'private_key']
+ fields = ['id', 'name', 'username', 'password', 'private_key', 'ad_domain']
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
diff --git a/apps/authentication/signals_handlers.py b/apps/authentication/signals_handlers.py
index 8e353ddf6..c6c1db680 100644
--- a/apps/authentication/signals_handlers.py
+++ b/apps/authentication/signals_handlers.py
@@ -13,6 +13,10 @@ from .signals import post_auth_success, post_auth_failed
@receiver(user_logged_in)
def on_user_auth_login_success(sender, user, request, **kwargs):
+ # 开启了 MFA,且没有校验过
+ if user.mfa_enabled and not request.session.get('auth_mfa'):
+ request.session['auth_mfa_required'] = 1
+
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
user_id = 'single_machine_login_' + str(user.id)
session_key = cache.get(user_id)
diff --git a/apps/authentication/sms_verify_code.py b/apps/authentication/sms_verify_code.py
new file mode 100644
index 000000000..33d17b207
--- /dev/null
+++ b/apps/authentication/sms_verify_code.py
@@ -0,0 +1,97 @@
+import random
+
+from django.conf import settings
+from django.core.cache import cache
+from django.utils.translation import gettext_lazy as _
+
+from common.message.backends.sms.alibaba import AlibabaSMS
+from common.message.backends.sms import SMS
+from common.utils import get_logger
+from common.exceptions import JMSException
+
+logger = get_logger(__file__)
+
+
+class CodeExpired(JMSException):
+ default_code = 'verify_code_expired'
+ default_detail = _('The verification code has expired. Please resend it')
+
+
+class CodeError(JMSException):
+ default_code = 'verify_code_error'
+ default_detail = _('The verification code is incorrect')
+
+
+class CodeSendTooFrequently(JMSException):
+ default_code = 'code_send_too_frequently'
+ default_detail = _('Please wait {} seconds before sending')
+
+ def __init__(self, ttl):
+ super().__init__(detail=self.default_detail.format(ttl))
+
+
+class VerifyCodeUtil:
+ KEY_TMPL = 'auth-verify_code-{}'
+ TIMEOUT = 60
+
+ def __init__(self, account, key_suffix=None, timeout=None):
+ self.account = account
+ self.key_suffix = key_suffix
+ self.code = ''
+
+ if key_suffix is not None:
+ self.key = self.KEY_TMPL.format(key_suffix)
+ else:
+ self.key = self.KEY_TMPL.format(account)
+ self.timeout = self.TIMEOUT if timeout is None else timeout
+
+ def touch(self):
+ """
+ 生成,保存,发送
+ """
+ ttl = self.ttl()
+ if ttl > 0:
+ raise CodeSendTooFrequently(ttl)
+
+ self.generate()
+ self.save()
+ self.send()
+
+ def generate(self):
+ code = ''.join(random.sample('0123456789', 4))
+ self.code = code
+ return code
+
+ def clear(self):
+ cache.delete(self.key)
+
+ def save(self):
+ cache.set(self.key, self.code, self.timeout)
+
+ def send(self):
+ """
+ 发送信息的方法,如果有错误直接抛出 api 异常
+ """
+ account = self.account
+ code = self.code
+
+ sms = SMS()
+ sms.send_verify_code(account, code)
+ logger.info(f'Send sms verify code: account={account} code={code}')
+
+ def verify(self, code):
+ right = cache.get(self.key)
+ if not right:
+ raise CodeExpired
+
+ if right != code:
+ raise CodeError
+
+ self.clear()
+ return True
+
+ def ttl(self):
+ return cache.ttl(self.key)
+
+ def get_code(self):
+ return cache.get(self.key)
diff --git a/apps/authentication/templates/authentication/login_otp.html b/apps/authentication/templates/authentication/login_otp.html
index f17451949..858c2737d 100644
--- a/apps/authentication/templates/authentication/login_otp.html
+++ b/apps/authentication/templates/authentication/login_otp.html
@@ -9,24 +9,60 @@
{% block content %}
+
{% endblock %}
diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py
index d8613adf4..2dea0da7b 100644
--- a/apps/authentication/urls/api_urls.py
+++ b/apps/authentication/urls/api_urls.py
@@ -27,7 +27,9 @@ urlpatterns = [
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
+ path('mfa/select/', api.MFASelectTypeApi.as_view(), name='mfa-select'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
+ path('sms/verify-code/send/', api.SendSMSVerifyCodeApi.as_view(), name='sms-verify-code-send'),
path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'),
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
path('login-confirm-settings//', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py
index f8571d6d7..594e4e68a 100644
--- a/apps/authentication/utils.py
+++ b/apps/authentication/utils.py
@@ -4,7 +4,6 @@ import base64
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5
from Cryptodome import Random
-
from common.utils import get_logger
logger = get_logger(__file__)
diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py
index 3fc62e08d..4fc39b4ac 100644
--- a/apps/authentication/views/login.py
+++ b/apps/authentication/views/login.py
@@ -51,7 +51,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
if settings.AUTH_OPENID:
auth_type = 'OIDC'
- openid_auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) + f'?next={next_url}'
+ openid_auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME)
+ openid_auth_url = openid_auth_url + f'?next={next_url}'
else:
openid_auth_url = None
@@ -64,16 +65,13 @@ class UserLoginView(mixins.AuthMixin, FormView):
if not any([openid_auth_url, cas_auth_url]):
return None
- if settings.LOGIN_REDIRECT_TO_BACKEND == 'OPENID' and openid_auth_url:
+ login_redirect = settings.LOGIN_REDIRECT_TO_BACKEND.lower()
+ if login_redirect == ['CAS', 'cas'] and cas_auth_url:
+ auth_url = cas_auth_url
+ else:
auth_url = openid_auth_url
- elif settings.LOGIN_REDIRECT_TO_BACKEND == 'CAS' and cas_auth_url:
- auth_url = cas_auth_url
-
- else:
- auth_url = openid_auth_url or cas_auth_url
-
- if settings.LOGIN_REDIRECT_TO_BACKEND:
+ if settings.LOGIN_REDIRECT_TO_BACKEND or not settings.LOGIN_REDIRECT_MSG_ENABLED:
redirect_url = auth_url
else:
message_data = {
@@ -137,15 +135,6 @@ class UserLoginView(mixins.AuthMixin, FormView):
self.request.session[RSA_PUBLIC_KEY] = None
def get_context_data(self, **kwargs):
- # 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用
- rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
- rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
- if not all((rsa_private_key, rsa_public_key)):
- rsa_private_key, rsa_public_key = utils.gen_key_pair()
- rsa_public_key = rsa_public_key.replace('\n', '\\n')
- self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
- self.request.session[RSA_PUBLIC_KEY] = rsa_public_key
-
forgot_password_url = reverse('authentication:forgot-password')
has_other_auth_backend = settings.AUTHENTICATION_BACKENDS[0] != settings.AUTH_BACKEND_MODEL
if has_other_auth_backend and settings.FORGOT_PASSWORD_URL:
@@ -158,7 +147,6 @@ class UserLoginView(mixins.AuthMixin, FormView):
'AUTH_WECOM': settings.AUTH_WECOM,
'AUTH_DINGTALK': settings.AUTH_DINGTALK,
'AUTH_FEISHU': settings.AUTH_FEISHU,
- 'rsa_public_key': rsa_public_key,
'forgot_password_url': forgot_password_url
}
kwargs.update(context)
diff --git a/apps/authentication/views/mfa.py b/apps/authentication/views/mfa.py
index f3c2602cb..35fa58f94 100644
--- a/apps/authentication/views/mfa.py
+++ b/apps/authentication/views/mfa.py
@@ -3,6 +3,8 @@
from __future__ import unicode_literals
from django.views.generic.edit import FormView
+from django.utils.translation import gettext_lazy as _
+from django.conf import settings
from .. import forms, errors, mixins
from .utils import redirect_to_guard_view
@@ -18,12 +20,14 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
redirect_field_name = 'next'
def form_valid(self, form):
- otp_code = form.cleaned_data.get('otp_code')
+ otp_code = form.cleaned_data.get('code')
+ mfa_type = form.cleaned_data.get('mfa_type')
+
try:
- self.check_user_mfa(otp_code)
+ self.check_user_mfa(otp_code, mfa_type)
return redirect_to_guard_view()
except (errors.MFAFailedError, errors.BlockMFAError) as e:
- form.add_error('otp_code', e.msg)
+ form.add_error('code', e.msg)
return super().form_invalid(form)
except Exception as e:
logger.error(e)
@@ -31,3 +35,28 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
traceback.print_exception()
return redirect_to_guard_view()
+ def get_context_data(self, **kwargs):
+ user = self.get_user_from_session()
+ context = {
+ 'methods': [
+ {
+ 'name': 'otp',
+ 'label': _('One-time password'),
+ 'enable': bool(user.otp_secret_key),
+ 'selected': False,
+ },
+ {
+ 'name': 'sms',
+ 'label': _('SMS'),
+ 'enable': bool(user.phone) and settings.SMS_ENABLED and settings.XPACK_ENABLED,
+ 'selected': False,
+ },
+ ]
+ }
+
+ for item in context['methods']:
+ if item['enable']:
+ item['selected'] = True
+ break
+ context.update(kwargs)
+ return context
diff --git a/apps/common/db/encoder.py b/apps/common/db/encoder.py
new file mode 100644
index 000000000..314ea071d
--- /dev/null
+++ b/apps/common/db/encoder.py
@@ -0,0 +1,20 @@
+import json
+from datetime import datetime
+import uuid
+
+from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
+
+
+class ModelJSONFieldEncoder(json.JSONEncoder):
+ """ 解决一些类型的字段不能序列化的问题 """
+
+ def default(self, obj):
+ if isinstance(obj, datetime):
+ return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
+ if isinstance(obj, uuid.UUID):
+ return str(obj)
+ if isinstance(obj, type(_("ugettext_lazy"))):
+ return str(obj)
+ else:
+ return super().default(obj)
diff --git a/apps/common/management/commands/services/services/base.py b/apps/common/management/commands/services/services/base.py
index 5063fb92e..0fb6cb1b3 100644
--- a/apps/common/management/commands/services/services/base.py
+++ b/apps/common/management/commands/services/services/base.py
@@ -160,7 +160,7 @@ class BaseService(object):
if self.process:
try:
self.process.wait(1) # 不wait,子进程可能无法回收
- except subprocess.TimeoutExpired:
+ except:
pass
if self.is_running:
diff --git a/apps/common/message/backends/dingtalk/__init__.py b/apps/common/message/backends/dingtalk/__init__.py
index e98bdee04..1c8a5b59c 100644
--- a/apps/common/message/backends/dingtalk/__init__.py
+++ b/apps/common/message/backends/dingtalk/__init__.py
@@ -2,9 +2,12 @@ import time
import hmac
import base64
+from common.utils import get_logger
from common.message.backends.utils import digest, as_request
from common.message.backends.mixin import BaseRequest
+logger = get_logger(__file__)
+
def sign(secret, data):
@@ -160,6 +163,7 @@ class DingTalk:
}
}
}
+ logger.info(f'Dingtalk send text: user_ids={user_ids} msg={msg}')
data = self._request.post(URL.SEND_MESSAGE, json=body, with_token=True)
return data
diff --git a/apps/common/message/backends/feishu/__init__.py b/apps/common/message/backends/feishu/__init__.py
index 7f70fd35d..3bc67b1b5 100644
--- a/apps/common/message/backends/feishu/__init__.py
+++ b/apps/common/message/backends/feishu/__init__.py
@@ -106,6 +106,7 @@ class FeiShu(RequestMixin):
body['receive_id'] = user_id
try:
+ logger.info(f'Feishu send text: user_ids={user_ids} msg={msg}')
self._requests.post(URL.SEND_MESSAGE, params=params, json=body)
except APIException as e:
# 只处理可预知的错误
diff --git a/apps/common/message/backends/sms/__init__.py b/apps/common/message/backends/sms/__init__.py
new file mode 100644
index 000000000..be32c6e94
--- /dev/null
+++ b/apps/common/message/backends/sms/__init__.py
@@ -0,0 +1,84 @@
+from collections import OrderedDict
+import importlib
+
+from django.utils.translation import gettext_lazy as _
+from django.db.models import TextChoices
+from django.conf import settings
+
+from common.utils import get_logger
+from common.exceptions import JMSException
+
+logger = get_logger(__file__)
+
+
+class SMS_MESSAGE(TextChoices):
+ """
+ 定义短信的各种消息类型,会存到类似 `ALIBABA_SMS_SIGN_AND_TEMPLATES` settings 里
+
+ {
+ 'verification_code': {'sign_name': 'Jumpserver', 'template_code': 'SMS_222870834'},
+ ...
+ }
+ """
+
+ """
+ 验证码签名和模板。模板例子:
+ `您的验证码:${code},您正进行身份验证,打死不告诉别人!`
+ 其中必须包含 `code` 变量
+ """
+ VERIFICATION_CODE = 'verification_code'
+
+ def get_sign_and_tmpl(self, config: dict):
+ try:
+ data = config[self]
+ return data['sign_name'], data['template_code']
+ except KeyError as e:
+ raise JMSException(
+ code=f'{settings.SMS_BACKEND}_sign_and_tmpl_bad',
+ detail=_('Invalid SMS sign and template: {}').format(e)
+ )
+
+
+class BACKENDS(TextChoices):
+ ALIBABA = 'alibaba', _('Alibaba')
+ TENCENT = 'tencent', _('Tencent')
+
+
+class BaseSMSClient:
+ """
+ 短信终端的基类
+ """
+
+ SIGN_AND_TMPL_SETTING_FIELD: str
+
+ @property
+ def sign_and_tmpl(self):
+ return getattr(settings, self.SIGN_AND_TMPL_SETTING_FIELD, {})
+
+ @classmethod
+ def new_from_settings(cls):
+ raise NotImplementedError
+
+ def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
+ raise NotImplementedError
+
+
+class SMS:
+ client: BaseSMSClient
+
+ def __init__(self, backend=None):
+ m = importlib.import_module(f'.{backend or settings.SMS_BACKEND}', __package__)
+ self.client = m.client.new_from_settings()
+
+ def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
+ return self.client.send_sms(
+ phone_numbers=phone_numbers,
+ sign_name=sign_name,
+ template_code=template_code,
+ template_param=template_param,
+ **kwargs
+ )
+
+ def send_verify_code(self, phone_number, code):
+ sign_name, template_code = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(self.client.sign_and_tmpl)
+ return self.send_sms([phone_number], sign_name, template_code, OrderedDict(code=code))
diff --git a/apps/common/message/backends/sms/alibaba.py b/apps/common/message/backends/sms/alibaba.py
new file mode 100644
index 000000000..b664f4401
--- /dev/null
+++ b/apps/common/message/backends/sms/alibaba.py
@@ -0,0 +1,61 @@
+import json
+
+from django.utils.translation import gettext_lazy as _
+from django.conf import settings
+from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
+from alibabacloud_tea_openapi import models as open_api_models
+from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
+from Tea.exceptions import TeaException
+
+from common.utils import get_logger
+from common.exceptions import JMSException
+from . import BaseSMSClient
+
+logger = get_logger(__file__)
+
+
+class AlibabaSMS(BaseSMSClient):
+ SIGN_AND_TMPL_SETTING_FIELD = 'ALIBABA_SMS_SIGN_AND_TEMPLATES'
+
+ @classmethod
+ def new_from_settings(cls):
+ return cls(
+ access_key_id=settings.ALIBABA_ACCESS_KEY_ID,
+ access_key_secret=settings.ALIBABA_ACCESS_KEY_SECRET
+ )
+
+ def __init__(self, access_key_id: str, access_key_secret: str):
+ config = open_api_models.Config(
+ # 您的AccessKey ID,
+ access_key_id=access_key_id,
+ # 您的AccessKey Secret,
+ access_key_secret=access_key_secret
+ )
+ # 访问的域名
+ config.endpoint = 'dysmsapi.aliyuncs.com'
+ self.client = Dysmsapi20170525Client(config)
+
+ def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
+ phone_numbers_str = ','.join(phone_numbers)
+ send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
+ phone_numbers=phone_numbers_str, sign_name=sign_name,
+ template_code=template_code, template_param=json.dumps(template_param)
+ )
+ try:
+ logger.info(f'Alibaba sms send: '
+ f'phone_numbers={phone_numbers} '
+ f'sign_name={sign_name} '
+ f'template_code={template_code} '
+ f'template_param={template_param}')
+ response = self.client.send_sms(send_sms_request)
+ # 这里只判断是否成功,失败抛出异常
+ if response.body.code != 'OK':
+ raise JMSException(detail=response.body.message, code=response.body.code)
+ except TeaException as e:
+ if e.code == 'SignatureDoesNotMatch':
+ raise JMSException(code=e.code, detail=_('Signature does not match'))
+ raise JMSException(code=e.code, detail=e.message)
+ return response
+
+
+client = AlibabaSMS
diff --git a/apps/common/message/backends/sms/tencent.py b/apps/common/message/backends/sms/tencent.py
new file mode 100644
index 000000000..6f796bb15
--- /dev/null
+++ b/apps/common/message/backends/sms/tencent.py
@@ -0,0 +1,90 @@
+import json
+from collections import OrderedDict
+
+from django.conf import settings
+from common.exceptions import JMSException
+from common.utils import get_logger
+from tencentcloud.common import credential
+from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
+# 导入对应产品模块的client models。
+from tencentcloud.sms.v20210111 import sms_client, models
+# 导入可选配置类
+from tencentcloud.common.profile.client_profile import ClientProfile
+from tencentcloud.common.profile.http_profile import HttpProfile
+from . import BaseSMSClient
+
+logger = get_logger(__file__)
+
+
+class TencentSMS(BaseSMSClient):
+ """
+ https://cloud.tencent.com/document/product/382/43196#.E5.8F.91.E9.80.81.E7.9F.AD.E4.BF.A1
+ """
+ SIGN_AND_TMPL_SETTING_FIELD = 'TENCENT_SMS_SIGN_AND_TEMPLATES'
+
+ @classmethod
+ def new_from_settings(cls):
+ return cls(
+ secret_id=settings.TENCENT_SECRET_ID,
+ secret_key=settings.TENCENT_SECRET_KEY,
+ sdkappid=settings.TENCENT_SDKAPPID
+ )
+
+ def __init__(self, secret_id: str, secret_key: str, sdkappid: str):
+ self.sdkappid = sdkappid
+
+ cred = credential.Credential(secret_id, secret_key)
+ httpProfile = HttpProfile()
+ httpProfile.reqMethod = "POST" # post请求(默认为post请求)
+ httpProfile.reqTimeout = 30 # 请求超时时间,单位为秒(默认60秒)
+ httpProfile.endpoint = "sms.tencentcloudapi.com"
+
+ clientProfile = ClientProfile()
+ clientProfile.signMethod = "TC3-HMAC-SHA256" # 指定签名算法
+ clientProfile.language = "en-US"
+ clientProfile.httpProfile = httpProfile
+ self.client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)
+
+ def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: OrderedDict, **kwargs):
+ try:
+ req = models.SendSmsRequest()
+ # 基本类型的设置:
+ # SDK采用的是指针风格指定参数,即使对于基本类型你也需要用指针来对参数赋值。
+ # SDK提供对基本类型的指针引用封装函数
+ # 帮助链接:
+ # 短信控制台: https://console.cloud.tencent.com/smsv2
+ # sms helper: https://cloud.tencent.com/document/product/382/3773
+
+ # 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666
+ req.SmsSdkAppId = self.sdkappid
+ # 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看
+ req.SignName = sign_name
+ # 短信码号扩展号: 默认未开通,如需开通请联系 [sms helper]
+ req.ExtendCode = ""
+ # 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回
+ req.SessionContext = "Jumpserver"
+ # 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper]
+ req.SenderId = ""
+ # 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
+ # 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号
+ req.PhoneNumberSet = phone_numbers
+ # 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看
+ req.TemplateId = template_code
+ # 模板参数: 若无模板参数,则设置为空
+ req.TemplateParamSet = list(template_param.values())
+ # 通过client对象调用DescribeInstances方法发起请求。注意请求方法名与请求对象是对应的。
+ # 返回的resp是一个DescribeInstancesResponse类的实例,与请求对象对应。
+ logger.info(f'Tencent sms send: '
+ f'phone_numbers={phone_numbers} '
+ f'sign_name={sign_name} '
+ f'template_code={template_code} '
+ f'template_param={template_param}')
+
+ resp = self.client.SendSms(req)
+
+ return resp
+ except TencentCloudSDKException as e:
+ raise JMSException(code=e.code, detail=e.message)
+
+
+client = TencentSMS
diff --git a/apps/common/message/backends/wecom/__init__.py b/apps/common/message/backends/wecom/__init__.py
index 661a8276c..8ba593d2c 100644
--- a/apps/common/message/backends/wecom/__init__.py
+++ b/apps/common/message/backends/wecom/__init__.py
@@ -115,6 +115,7 @@ class WeCom(RequestMixin):
},
**extra_params
}
+ logger.info(f'Wecom send text: users={users} msg={msg}')
data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False)
errcode = data['errcode']
diff --git a/apps/common/permissions.py b/apps/common/permissions.py
index 71dca0b3c..b0f217c1c 100644
--- a/apps/common/permissions.py
+++ b/apps/common/permissions.py
@@ -191,3 +191,11 @@ class HasQueryParamsUserAndIsCurrentOrgMember(permissions.BasePermission):
return False
query_user = current_org.get_members().filter(id=query_user_id).first()
return bool(query_user)
+
+
+class OnlySuperUserCanList(IsValidUser):
+ def has_permission(self, request, view):
+ user = request.user
+ if view.action == 'list' and not user.is_superuser:
+ return False
+ return True
diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py
index 311a58159..74fd3b708 100644
--- a/apps/jumpserver/conf.py
+++ b/apps/jumpserver/conf.py
@@ -179,13 +179,14 @@ class Config(dict):
'AUTH_OPENID_CLIENT_SECRET': 'client-secret',
'AUTH_OPENID_SHARE_SESSION': True,
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
+
# OpenID 新配置参数 (version >= 1.5.9)
- 'AUTH_OPENID_PROVIDER_ENDPOINT': 'https://op-example.com/',
- 'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://op-example.com/authorize',
- 'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT': 'https://op-example.com/token',
- 'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT': 'https://op-example.com/jwks',
- 'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT': 'https://op-example.com/userinfo',
- 'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT': 'https://op-example.com/logout',
+ 'AUTH_OPENID_PROVIDER_ENDPOINT': 'https://oidc.example.com/',
+ 'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://oidc.example.com/authorize',
+ 'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT': 'https://oidc.example.com/token',
+ 'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT': 'https://oidc.example.com/jwks',
+ 'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT': 'https://oidc.example.com/userinfo',
+ 'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT': 'https://oidc.example.com/logout',
'AUTH_OPENID_PROVIDER_SIGNATURE_ALG': 'HS256',
'AUTH_OPENID_PROVIDER_SIGNATURE_KEY': None,
'AUTH_OPENID_SCOPES': 'openid profile email',
@@ -194,10 +195,13 @@ class Config(dict):
'AUTH_OPENID_USE_STATE': True,
'AUTH_OPENID_USE_NONCE': True,
'AUTH_OPENID_ALWAYS_UPDATE_USER': True,
- # OpenID 旧配置参数 (version <= 1.5.8 (discarded))
- 'AUTH_OPENID_SERVER_URL': 'http://openid',
+
+ # Keycloak 旧配置参数 (version <= 1.5.8 (discarded))
+ 'AUTH_OPENID_KEYCLOAK': True,
+ 'AUTH_OPENID_SERVER_URL': 'https://keycloak.example.com',
'AUTH_OPENID_REALM_NAME': None,
+ # Raidus 认证
'AUTH_RADIUS': False,
'RADIUS_SERVER': 'localhost',
'RADIUS_PORT': 1812,
@@ -205,8 +209,9 @@ class Config(dict):
'RADIUS_ENCRYPT_PASSWORD': True,
'OTP_IN_RADIUS': False,
+ # Cas 认证
'AUTH_CAS': False,
- 'CAS_SERVER_URL': "http://host/cas/",
+ 'CAS_SERVER_URL': "https://example.com/cas/",
'CAS_ROOT_PROXIED_AS': '',
'CAS_LOGOUT_COMPLETELY': True,
'CAS_VERSION': 3,
@@ -218,24 +223,44 @@ class Config(dict):
'AUTH_SSO': False,
'AUTH_SSO_AUTHKEY_TTL': 60 * 15,
+ # 企业微信
'AUTH_WECOM': False,
'WECOM_CORPID': '',
'WECOM_AGENTID': '',
'WECOM_SECRET': '',
+ # 钉钉
'AUTH_DINGTALK': False,
'DINGTALK_AGENTID': '',
'DINGTALK_APPKEY': '',
'DINGTALK_APPSECRET': '',
+ # 飞书
'AUTH_FEISHU': False,
'FEISHU_APP_ID': '',
'FEISHU_APP_SECRET': '',
+ 'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS
+ 'LOGIN_REDIRECT_MSG_ENABLED': True,
+
+ 'SMS_ENABLED': False,
+ 'SMS_BACKEND': '',
+ 'SMS_TEST_PHONE': '',
+
+ 'ALIBABA_ACCESS_KEY_ID': '',
+ 'ALIBABA_ACCESS_KEY_SECRET': '',
+ 'ALIBABA_SMS_SIGN_AND_TEMPLATES': {},
+
+ 'TENCENT_SECRET_ID': '',
+ 'TENCENT_SECRET_KEY': '',
+ 'TENCENT_SDKAPPID': '',
+ 'TENCENT_SMS_SIGN_AND_TEMPLATES': {},
+
'OTP_VALID_WINDOW': 2,
'OTP_ISSUER_NAME': 'JumpServer',
- 'EMAIL_SUFFIX': 'jumpserver.org',
+ 'EMAIL_SUFFIX': 'example.com',
+ # Terminal配置
'TERMINAL_PASSWORD_AUTH': True,
'TERMINAL_PUBLIC_KEY_AUTH': True,
'TERMINAL_HEARTBEAT_INTERVAL': 20,
@@ -245,7 +270,10 @@ class Config(dict):
'TERMINAL_HOST_KEY': '',
'TERMINAL_TELNET_REGEX': '',
'TERMINAL_COMMAND_STORAGE': {},
+ 'TERMINAL_RDP_ADDR': '',
+ 'XRDP_ENABLED': True,
+ # 安全配置
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
'SECURITY_COMMAND_EXECUTION': True,
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
@@ -262,57 +290,60 @@ class Config(dict):
'SECURITY_PASSWORD_SPECIAL_CHAR': False,
'SECURITY_LOGIN_CHALLENGE_ENABLED': False,
'SECURITY_LOGIN_CAPTCHA_ENABLED': True,
- 'SECURITY_DATA_CRYPTO_ALGO': 'aes',
'SECURITY_INSECURE_COMMAND': False,
'SECURITY_INSECURE_COMMAND_LEVEL': 5,
'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '',
'SECURITY_LUNA_REMEMBER_AUTH': True,
'SECURITY_WATERMARK_ENABLED': True,
+ 'SECURITY_MFA_VERIFY_TTL': 3600,
+ 'SECURITY_SESSION_SHARE': True,
+ 'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5,
+ 'LOGIN_CONFIRM_ENABLE': False, # 准备废弃,放到 acl 中
+ 'CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED': True,
+ 'USER_LOGIN_SINGLE_MACHINE_ENABLED': False,
+ 'ONLY_ALLOW_EXIST_USER_AUTH': False,
+ 'ONLY_ALLOW_AUTH_FROM_SOURCE': False,
+ # 启动前
'HTTP_BIND_HOST': '0.0.0.0',
'HTTP_LISTEN_PORT': 8080,
'WS_LISTEN_PORT': 8070,
+ 'SYSLOG_ADDR': '', # '192.168.0.1:514'
+ 'SYSLOG_FACILITY': 'user',
+ 'SYSLOG_SOCKTYPE': 2,
+ 'PERM_EXPIRED_CHECK_PERIODIC': 60 * 60,
+ 'FLOWER_URL': "127.0.0.1:5555",
+ 'LANGUAGE_CODE': 'zh',
+ 'TIME_ZONE': 'Asia/Shanghai',
+ 'FORCE_SCRIPT_NAME': '',
+ 'SESSION_COOKIE_SECURE': False,
+ 'CSRF_COOKIE_SECURE': False,
+ 'REFERER_CHECK_ENABLED': False,
+ 'SESSION_SAVE_EVERY_REQUEST': True,
+ 'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
+ 'SERVER_REPLAY_STORAGE': {},
+ 'SECURITY_DATA_CRYPTO_ALGO': 'aes',
+
+ # 记录清理清理
'LOGIN_LOG_KEEP_DAYS': 200,
'TASK_LOG_KEEP_DAYS': 90,
'OPERATE_LOG_KEEP_DAYS': 200,
'FTP_LOG_KEEP_DAYS': 200,
- 'ASSETS_PERM_CACHE_TIME': 3600 * 24,
- 'SECURITY_MFA_VERIFY_TTL': 3600,
- 'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5,
- 'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK,
- 'SYSLOG_ADDR': '', # '192.168.0.1:514'
- 'SYSLOG_FACILITY': 'user',
- 'SYSLOG_SOCKTYPE': 2,
- 'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False,
- 'PERM_EXPIRED_CHECK_PERIODIC': 60 * 60,
- 'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
- 'FLOWER_URL': "127.0.0.1:5555",
- 'DEFAULT_ORG_SHOW_ALL_USERS': True,
- 'PERIOD_TASK_ENABLED': True,
- 'FORCE_SCRIPT_NAME': '',
- 'LOGIN_CONFIRM_ENABLE': False,
- 'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False,
- 'ORG_CHANGE_TO_URL': '',
- 'LANGUAGE_CODE': 'zh',
- 'TIME_ZONE': 'Asia/Shanghai',
- 'CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED': True,
- 'USER_LOGIN_SINGLE_MACHINE_ENABLED': False,
- 'TICKETS_ENABLED': True,
- 'SESSION_COOKIE_SECURE': False,
- 'CSRF_COOKIE_SECURE': False,
- 'REFERER_CHECK_ENABLED': False,
- 'SERVER_REPLAY_STORAGE': {},
- 'CONNECTION_TOKEN_ENABLED': False,
- 'ONLY_ALLOW_EXIST_USER_AUTH': False,
- 'ONLY_ALLOW_AUTH_FROM_SOURCE': False,
- 'SESSION_SAVE_EVERY_REQUEST': True,
- 'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
- 'FORGOT_PASSWORD_URL': '',
- 'HEALTH_CHECK_TOKEN': '',
- 'LOGIN_REDIRECT_TO_BACKEND': None, # 'OPENID / CAS
'CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS': 30,
- 'TERMINAL_RDP_ADDR': ''
+ # 废弃的
+ 'DEFAULT_ORG_SHOW_ALL_USERS': True,
+ 'ORG_CHANGE_TO_URL': '',
+ 'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False,
+ 'CONNECTION_TOKEN_ENABLED': False,
+
+ 'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False,
+ 'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
+ 'PERIOD_TASK_ENABLED': True,
+
+ 'TICKETS_ENABLED': True,
+ 'FORGOT_PASSWORD_URL': '',
+ 'HEALTH_CHECK_TOKEN': '',
}
def compatible_auth_openid_of_key(self):
@@ -323,6 +354,9 @@ class Config(dict):
构造出新配置中标准OpenID协议中所需的Endpoint即可
(Keycloak说明文档参考: https://www.keycloak.org/docs/latest/securing_apps/)
"""
+ if self.AUTH_OPENID and not self.AUTH_OPENID_REALM_NAME:
+ self['AUTH_OPENID_KEYCLOAK'] = False
+
if not self.AUTH_OPENID:
return
diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py
index d8e96e673..ac9fdb631 100644
--- a/apps/jumpserver/settings/auth.py
+++ b/apps/jumpserver/settings/auth.py
@@ -122,6 +122,22 @@ AUTH_FEISHU = CONFIG.AUTH_FEISHU
FEISHU_APP_ID = CONFIG.FEISHU_APP_ID
FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET
+# SMS auth
+SMS_ENABLED = CONFIG.SMS_ENABLED
+SMS_BACKEND = CONFIG.SMS_BACKEND
+SMS_TEST_PHONE = CONFIG.SMS_TEST_PHONE
+
+# Alibaba
+ALIBABA_ACCESS_KEY_ID = CONFIG.ALIBABA_ACCESS_KEY_ID
+ALIBABA_ACCESS_KEY_SECRET = CONFIG.ALIBABA_ACCESS_KEY_SECRET
+ALIBABA_SMS_SIGN_AND_TEMPLATES = CONFIG.ALIBABA_SMS_SIGN_AND_TEMPLATES
+
+# TENCENT
+TENCENT_SECRET_ID = CONFIG.TENCENT_SECRET_ID
+TENCENT_SECRET_KEY = CONFIG.TENCENT_SECRET_KEY
+TENCENT_SDKAPPID = CONFIG.TENCENT_SDKAPPID
+TENCENT_SMS_SIGN_AND_TEMPLATES = CONFIG.TENCENT_SMS_SIGN_AND_TEMPLATES
+
# Other setting
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE
diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py
index a3b2c7cf6..fd8feeaad 100644
--- a/apps/jumpserver/settings/base.py
+++ b/apps/jumpserver/settings/base.py
@@ -87,6 +87,7 @@ MIDDLEWARE = [
'orgs.middleware.OrgMiddleware',
'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware',
'authentication.backends.cas.middleware.CASMiddleware',
+ 'authentication.middleware.MFAMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
]
diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py
index 2e31eb536..0ef447621 100644
--- a/apps/jumpserver/settings/custom.py
+++ b/apps/jumpserver/settings/custom.py
@@ -72,14 +72,9 @@ TERMINAL_HOST_KEY = CONFIG.TERMINAL_HOST_KEY
TERMINAL_HEADER_TITLE = CONFIG.TERMINAL_HEADER_TITLE
TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX
-# User or user group permission cache time, default 3600 seconds
-ASSETS_PERM_CACHE_ENABLE = CONFIG.ASSETS_PERM_CACHE_ENABLE
-ASSETS_PERM_CACHE_TIME = CONFIG.ASSETS_PERM_CACHE_TIME
-
# Asset user auth external backend, default AuthBook backend
BACKEND_ASSET_USER_AUTH_VAULT = False
-DEFAULT_ORG_SHOW_ALL_USERS = CONFIG.DEFAULT_ORG_SHOW_ALL_USERS
PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE
PERM_EXPIRED_CHECK_PERIODIC = CONFIG.PERM_EXPIRED_CHECK_PERIODIC
WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL
@@ -129,7 +124,11 @@ HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN
TERMINAL_RDP_ADDR = CONFIG.TERMINAL_RDP_ADDR
SECURITY_LUNA_REMEMBER_AUTH = CONFIG.SECURITY_LUNA_REMEMBER_AUTH
SECURITY_WATERMARK_ENABLED = CONFIG.SECURITY_WATERMARK_ENABLED
+SECURITY_SESSION_SHARE = CONFIG.SECURITY_SESSION_SHARE
LOGIN_REDIRECT_TO_BACKEND = CONFIG.LOGIN_REDIRECT_TO_BACKEND
+LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED
CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS
+
+XRDP_ENABLED = CONFIG.XRDP_ENABLED
diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py
index 43d0e6cb0..ea41cc7ee 100644
--- a/apps/jumpserver/urls.py
+++ b/apps/jumpserver/urls.py
@@ -32,6 +32,7 @@ app_view_patterns = [
path('ops/', include('ops.urls.view_urls'), name='ops'),
path('common/', include('common.urls.view_urls'), name='common'),
re_path(r'flower/(?P.*)', views.celery_flower_view, name='flower-view'),
+ path('download/', views.ResourceDownload.as_view(), name='download')
]
if settings.XPACK_ENABLED:
diff --git a/apps/jumpserver/views/other.py b/apps/jumpserver/views/other.py
index 293177615..9cf5a5500 100644
--- a/apps/jumpserver/views/other.py
+++ b/apps/jumpserver/views/other.py
@@ -4,7 +4,7 @@ import re
from django.http import HttpResponseRedirect, JsonResponse, Http404
from django.conf import settings
-from django.views.generic import View
+from django.views.generic import View, TemplateView
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
@@ -16,7 +16,8 @@ from common.http import HttpResponseTemporaryRedirect
__all__ = [
'LunaView', 'I18NView', 'KokoView', 'WsView',
- 'redirect_format_api', 'redirect_old_apps_view', 'UIView'
+ 'redirect_format_api', 'redirect_old_apps_view', 'UIView',
+ 'ResourceDownload',
]
@@ -84,3 +85,6 @@ class KokoView(View):
"If you see this page, prove that you are not accessing the nginx listening port. Good luck.")
return HttpResponse(msg)
+
+class ResourceDownload(TemplateView):
+ template_name = 'resource_download.html'
diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 2f75f059b..c873509ac 100644
Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index f9bf84e0c..e36335bda 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-08-18 18:56+0800\n"
+"POT-Creation-Date: 2021-09-09 20:13+0800\n"
"PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler \n"
"Language-Team: JumpServer team\n"
@@ -22,10 +22,10 @@ msgstr ""
#: assets/models/base.py:175 assets/models/cluster.py:18
#: assets/models/cmd_filter.py:21 assets/models/domain.py:24
#: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24
-#: orgs/models.py:24 perms/models/base.py:49 settings/models.py:29
-#: terminal/models/storage.py:23 terminal/models/task.py:16
-#: terminal/models/terminal.py:100 users/forms/profile.py:32
-#: users/models/group.py:15 users/models/user.py:561
+#: orgs/models.py:24 perms/models/base.py:44 settings/models.py:29
+#: settings/serializers/sms.py:6 terminal/models/storage.py:23
+#: terminal/models/task.py:16 terminal/models/terminal.py:100
+#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:604
#: users/templates/users/_select_user_modal.html:13
#: users/templates/users/user_asset_permission.html:37
#: users/templates/users/user_asset_permission.html:154
@@ -35,39 +35,38 @@ msgid "Name"
msgstr "名称"
#: acls/models/base.py:27 assets/models/cmd_filter.py:54
-#: assets/models/user.py:203
+#: assets/models/user.py:207
msgid "Priority"
msgstr "优先级"
#: acls/models/base.py:28 assets/models/cmd_filter.py:54
-#: assets/models/user.py:203
+#: assets/models/user.py:207
msgid "1-100, the lower the value will be match first"
msgstr "优先级可选范围为 1-100 (数值越小越优先)"
#: acls/models/base.py:31 authentication/models.py:20
#: authentication/templates/authentication/_access_key_modal.html:32
-#: perms/models/base.py:52 users/templates/users/_select_user_modal.html:18
+#: perms/models/base.py:48 terminal/models/sharing.py:24
+#: users/templates/users/_select_user_modal.html:18
msgid "Active"
msgstr "激活中"
-# msgid "Date created"
-# msgstr "创建日期"
#: acls/models/base.py:32 applications/models/application.py:179
#: assets/models/asset.py:144 assets/models/asset.py:220
#: assets/models/base.py:180 assets/models/cluster.py:29
#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:64
#: assets/models/domain.py:25 assets/models/domain.py:65
#: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37
-#: orgs/models.py:27 perms/models/base.py:57 settings/models.py:34
+#: orgs/models.py:27 perms/models/base.py:53 settings/models.py:34
#: terminal/models/storage.py:26 terminal/models/terminal.py:114
-#: tickets/models/ticket.py:73 users/models/group.py:16
-#: users/models/user.py:594 xpack/plugins/change_auth_plan/models.py:88
-#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:113
+#: tickets/models/ticket.py:71 users/models/group.py:16
+#: users/models/user.py:637 xpack/plugins/change_auth_plan/models.py:77
+#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:108
#: xpack/plugins/gathered_user/models.py:26
msgid "Comment"
msgstr "备注"
-#: acls/models/login_acl.py:16 tickets/const.py:19
+#: acls/models/login_acl.py:16 tickets/const.py:38
msgid "Reject"
msgstr "拒绝"
@@ -83,7 +82,7 @@ msgstr "登录IP"
#: acls/serializers/login_acl.py:34 acls/serializers/login_asset_acl.py:75
#: assets/models/cmd_filter.py:57 audits/models.py:57
#: authentication/templates/authentication/_access_key_modal.html:34
-#: tickets/models/ticket.py:43 users/templates/users/_granted_assets.html:29
+#: users/templates/users/_granted_assets.html:29
#: users/templates/users/user_asset_permission.html:44
#: users/templates/users/user_asset_permission.html:79
#: users/templates/users/user_database_app_permission.html:42
@@ -94,12 +93,12 @@ msgstr "动作"
#: acls/serializers/login_acl.py:33 assets/models/label.py:15
#: audits/models.py:36 audits/models.py:56 audits/models.py:74
#: audits/serializers.py:93 authentication/models.py:44
-#: authentication/models.py:97 orgs/models.py:19 orgs/models.py:433
-#: perms/models/base.py:50 templates/index.html:78
+#: authentication/models.py:96 orgs/models.py:19 orgs/models.py:433
+#: perms/models/base.py:45 templates/index.html:78
#: terminal/backends/command/models.py:18
#: terminal/backends/command/serializers.py:12 terminal/models/session.py:38
-#: tickets/models/comment.py:17 users/const.py:14 users/models/user.py:176
-#: users/models/user.py:762 users/models/user.py:788
+#: tickets/models/comment.py:17 users/const.py:14 users/models/user.py:181
+#: users/models/user.py:809 users/models/user.py:835
#: users/serializers/group.py:19
#: users/templates/users/user_asset_permission.html:38
#: users/templates/users/user_asset_permission.html:64
@@ -120,14 +119,14 @@ msgstr "系统用户"
#: acls/models/login_asset_acl.py:22
#: applications/serializers/attrs/application_category/remote_app.py:37
#: assets/models/asset.py:357 assets/models/authbook.py:15
-#: assets/models/gathered_user.py:14 assets/serializers/system_user.py:200
+#: assets/models/gathered_user.py:14 assets/serializers/system_user.py:201
#: audits/models.py:38 perms/models/asset_permission.py:99
#: templates/index.html:82 terminal/backends/command/models.py:19
#: terminal/backends/command/serializers.py:13 terminal/models/session.py:40
#: users/templates/users/user_asset_permission.html:40
#: users/templates/users/user_asset_permission.html:70
-#: xpack/plugins/change_auth_plan/models.py:315
-#: xpack/plugins/cloud/models.py:217
+#: xpack/plugins/change_auth_plan/models.py:282
+#: xpack/plugins/cloud/models.py:212
msgid "Asset"
msgstr "资产"
@@ -156,7 +155,7 @@ msgstr ""
#: acls/serializers/login_acl.py:30 acls/serializers/login_asset_acl.py:31
#: applications/serializers/attrs/application_type/mysql_workbench.py:18
#: assets/models/asset.py:180 assets/models/domain.py:61
-#: assets/serializers/account.py:12 settings/serializers/settings.py:114
+#: assets/serializers/account.py:12 settings/serializers/terminal.py:8
#: users/templates/users/_granted_assets.html:26
#: users/templates/users/user_asset_permission.html:156
msgid "IP"
@@ -172,17 +171,16 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. "
#: acls/serializers/login_asset_acl.py:17
#: acls/serializers/login_asset_acl.py:51
-#: applications/serializers/application.py:74
#: applications/serializers/attrs/application_type/chrome.py:20
#: applications/serializers/attrs/application_type/custom.py:21
#: applications/serializers/attrs/application_type/mysql_workbench.py:30
#: applications/serializers/attrs/application_type/vmware_client.py:26
#: assets/models/base.py:176 assets/models/gathered_user.py:15
#: audits/models.py:105 authentication/forms.py:15 authentication/forms.py:17
-#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:559
+#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:602
#: users/templates/users/_select_user_modal.html:14
-#: xpack/plugins/change_auth_plan/models.py:51
-#: xpack/plugins/change_auth_plan/models.py:311
+#: xpack/plugins/change_auth_plan/models.py:47
+#: xpack/plugins/change_auth_plan/models.py:278
#: xpack/plugins/cloud/serializers.py:67
msgid "Username"
msgstr "用户名"
@@ -198,7 +196,7 @@ msgstr ""
#: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:181
#: assets/serializers/account.py:13 assets/serializers/gathered_user.py:23
-#: settings/serializers/settings.py:113
+#: settings/serializers/terminal.py:7
#: users/templates/users/_granted_assets.html:25
#: users/templates/users/user_asset_permission.html:157
msgid "Hostname"
@@ -211,7 +209,7 @@ msgid ""
msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}"
#: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:184
-#: assets/models/domain.py:63 assets/models/user.py:204
+#: assets/models/domain.py:63 assets/models/user.py:208
#: terminal/serializers/session.py:30 terminal/serializers/storage.py:69
msgid "Protocol"
msgstr "协议"
@@ -221,7 +219,7 @@ msgid "Unsupported protocols: {}"
msgstr "不支持的协议: {}"
#: acls/serializers/login_asset_acl.py:98
-#: tickets/serializers/ticket/ticket.py:111
+#: tickets/serializers/ticket/ticket.py:105
msgid "The organization `{}` does not exist"
msgstr "组织 `{}` 不存在"
@@ -233,7 +231,7 @@ msgstr "所有复核人都不属于组织 `{}`"
msgid "My applications"
msgstr "我的应用"
-#: applications/const.py:8
+#: applications/const.py:8 applications/models/account.py:10
#: applications/serializers/attrs/application_category/db.py:14
#: applications/serializers/attrs/application_type/mysql_workbench.py:26
msgid "Database"
@@ -247,79 +245,8 @@ msgstr "远程应用"
msgid "Custom"
msgstr "自定义"
-#: applications/models/application.py:50 templates/_nav.html:60
-msgid "Applications"
-msgstr "应用管理"
-
-#: applications/models/application.py:168
-#: applications/serializers/application.py:80 assets/models/label.py:21
-#: perms/models/application_permission.py:20
-#: perms/serializers/application/user_permission.py:33
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:20
-msgid "Category"
-msgstr "类别"
-
-#: applications/models/application.py:171
-#: applications/serializers/application.py:82 assets/models/cmd_filter.py:53
-#: assets/models/user.py:202 perms/models/application_permission.py:23
-#: perms/serializers/application/user_permission.py:34
-#: terminal/models/storage.py:55 terminal/models/storage.py:116
-#: tickets/models/ticket.py:38
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:27
-msgid "Type"
-msgstr "类型"
-
-#: applications/models/application.py:175 assets/models/asset.py:188
-#: assets/models/domain.py:30 assets/models/domain.py:64
-msgid "Domain"
-msgstr "网域"
-
-#: applications/models/application.py:177 xpack/plugins/cloud/models.py:33
-msgid "Attrs"
-msgstr ""
-
-#: applications/serializers/application.py:50
-#: applications/serializers/application.py:81 assets/serializers/label.py:13
-#: perms/serializers/application/permission.py:16
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:24
-msgid "Category display"
-msgstr "类别名称"
-
-#: applications/serializers/application.py:51
-#: applications/serializers/application.py:83
-#: assets/serializers/system_user.py:26 audits/serializers.py:29
-#: perms/serializers/application/permission.py:17
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:31
-#: tickets/serializers/ticket/ticket.py:19
-msgid "Type display"
-msgstr "类型名称"
-
-#: applications/serializers/application.py:73
-msgid "Id"
-msgstr ""
-
-#: applications/serializers/application.py:75
-#: applications/serializers/application.py:110
-#: applications/serializers/attrs/application_type/chrome.py:23
-#: applications/serializers/attrs/application_type/custom.py:25
-#: applications/serializers/attrs/application_type/mysql_workbench.py:34
-#: applications/serializers/attrs/application_type/vmware_client.py:30
-#: assets/models/base.py:177 audits/signals_handler.py:63
-#: authentication/forms.py:22
-#: authentication/templates/authentication/login.html:164
-#: settings/serializers/settings.py:95 users/forms/profile.py:21
-#: users/templates/users/user_otp_check_password.html:13
-#: users/templates/users/user_password_update.html:43
-#: users/templates/users/user_password_verify.html:18
-#: xpack/plugins/change_auth_plan/models.py:72
-#: xpack/plugins/change_auth_plan/models.py:207
-#: xpack/plugins/change_auth_plan/models.py:318
-#: xpack/plugins/cloud/serializers.py:69
-msgid "Password"
-msgstr "密码"
-
-#: applications/serializers/application.py:76 assets/models/authbook.py:16
-#: assets/models/user.py:277 audits/models.py:39
+#: applications/models/account.py:11 assets/models/authbook.py:16
+#: assets/models/user.py:281 audits/models.py:39
#: perms/models/application_permission.py:31
#: perms/models/asset_permission.py:101 templates/_nav.html:45
#: terminal/backends/command/models.py:20
@@ -333,40 +260,76 @@ msgstr "密码"
msgid "System user"
msgstr "系统用户"
-#: applications/serializers/application.py:77 assets/serializers/account.py:31
-msgid "System user display"
-msgstr "系统用户名称"
+#: applications/models/account.py:12 assets/models/authbook.py:17
+#: settings/serializers/auth/cas.py:14
+msgid "Version"
+msgstr "版本"
-#: applications/serializers/application.py:78
-msgid "App"
-msgstr "应用"
+#: applications/models/account.py:18 xpack/plugins/cloud/models.py:82
+#: xpack/plugins/cloud/serializers.py:204
+msgid "Account"
+msgstr "账户"
-#: applications/serializers/application.py:79
-msgid "Application name"
+#: applications/models/application.py:50 templates/_nav.html:60
+msgid "Applications"
+msgstr "应用管理"
+
+#: applications/models/application.py:168
+#: applications/serializers/application.py:82 assets/models/label.py:21
+#: perms/models/application_permission.py:20
+#: perms/serializers/application/user_permission.py:33
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:20
+msgid "Category"
+msgstr "类别"
+
+#: applications/models/application.py:171
+#: applications/serializers/application.py:84 assets/models/cmd_filter.py:53
+#: assets/models/user.py:206 perms/models/application_permission.py:23
+#: perms/serializers/application/user_permission.py:34
+#: terminal/models/storage.py:55 terminal/models/storage.py:116
+#: tickets/models/flow.py:50 tickets/models/ticket.py:48
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:27
+msgid "Type"
+msgstr "类型"
+
+#: applications/models/application.py:175 assets/models/asset.py:188
+#: assets/models/domain.py:30 assets/models/domain.py:64
+msgid "Domain"
+msgstr "网域"
+
+#: applications/models/application.py:177 xpack/plugins/cloud/models.py:33
+msgid "Attrs"
+msgstr ""
+
+#: applications/serializers/application.py:59
+#: applications/serializers/application.py:83 assets/serializers/label.py:13
+#: perms/serializers/application/permission.py:16
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:24
+msgid "Category display"
+msgstr "类别名称"
+
+#: applications/serializers/application.py:60
+#: applications/serializers/application.py:85
+#: assets/serializers/system_user.py:26 audits/serializers.py:29
+#: perms/serializers/application/permission.py:17
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:31
+#: tickets/serializers/ticket/ticket.py:22
+#: tickets/serializers/ticket/ticket.py:162
+msgid "Type display"
+msgstr "类型名称"
+
+#: applications/serializers/application.py:101
+msgid "Application display"
msgstr "应用名称"
-#: applications/serializers/application.py:84
-msgid "Union id"
-msgstr "联合ID"
-
-#: applications/serializers/application.py:85 orgs/mixins/models.py:45
-#: orgs/mixins/serializers.py:25 orgs/models.py:37 orgs/models.py:432
-#: orgs/serializers.py:106 tickets/serializers/ticket/ticket.py:83
-msgid "Organization"
-msgstr "组织"
-
-#: applications/serializers/application.py:86 assets/serializers/asset.py:97
-#: assets/serializers/system_user.py:217 orgs/mixins/serializers.py:26
-msgid "Org name"
-msgstr "组织名称"
-
#: applications/serializers/attrs/application_category/cloud.py:9
#: assets/models/cluster.py:40
msgid "Cluster"
msgstr "集群"
#: applications/serializers/attrs/application_category/db.py:11
-#: ops/models/adhoc.py:146 xpack/plugins/cloud/serializers.py:65
+#: ops/models/adhoc.py:146 settings/serializers/auth/radius.py:14
+#: xpack/plugins/cloud/serializers.py:65
msgid "Host"
msgstr "主机"
@@ -376,7 +339,7 @@ msgstr "主机"
#: applications/serializers/attrs/application_type/oracle.py:11
#: applications/serializers/attrs/application_type/pgsql.py:11
#: assets/models/asset.py:185 assets/models/domain.py:62
-#: xpack/plugins/cloud/serializers.py:66
+#: settings/serializers/auth/radius.py:15 xpack/plugins/cloud/serializers.py:66
msgid "Port"
msgstr "端口"
@@ -388,7 +351,6 @@ msgid "Application path"
msgstr "应用路径"
#: applications/serializers/attrs/application_category/remote_app.py:45
-#: xpack/plugins/cloud/serializers.py:51
msgid "This field is required."
msgstr "该字段是必填项。"
@@ -397,6 +359,24 @@ msgstr "该字段是必填项。"
msgid "Target URL"
msgstr "目标URL"
+#: applications/serializers/attrs/application_type/chrome.py:23
+#: applications/serializers/attrs/application_type/custom.py:25
+#: applications/serializers/attrs/application_type/mysql_workbench.py:34
+#: applications/serializers/attrs/application_type/vmware_client.py:30
+#: assets/models/base.py:177 audits/signals_handler.py:63
+#: authentication/forms.py:22
+#: authentication/templates/authentication/login.html:164
+#: settings/serializers/auth/ldap.py:44 users/forms/profile.py:21
+#: users/templates/users/user_otp_check_password.html:13
+#: users/templates/users/user_password_update.html:43
+#: users/templates/users/user_password_verify.html:18
+#: xpack/plugins/change_auth_plan/models.py:68
+#: xpack/plugins/change_auth_plan/models.py:190
+#: xpack/plugins/change_auth_plan/models.py:285
+#: xpack/plugins/cloud/serializers.py:69
+msgid "Password"
+msgstr "密码"
+
#: applications/serializers/attrs/application_type/custom.py:13
msgid "Operating parameter"
msgstr "运行参数"
@@ -429,8 +409,8 @@ msgstr "基础"
msgid "Charset"
msgstr "编码"
-#: assets/models/asset.py:142 assets/serializers/asset.py:161
-#: tickets/models/ticket.py:40
+#: assets/models/asset.py:142 assets/serializers/asset.py:168
+#: tickets/models/ticket.py:50
msgid "Meta"
msgstr "元数据"
@@ -445,13 +425,13 @@ msgstr "系统平台"
#: assets/models/asset.py:186 assets/serializers/asset.py:65
#: perms/serializers/asset/user_permission.py:41
-#: xpack/plugins/cloud/models.py:104 xpack/plugins/cloud/serializers.py:184
+#: xpack/plugins/cloud/models.py:99 xpack/plugins/cloud/serializers.py:183
msgid "Protocols"
msgstr "协议组"
-#: assets/models/asset.py:189 assets/models/user.py:194
+#: assets/models/asset.py:189 assets/models/user.py:198
#: perms/models/asset_permission.py:100
-#: xpack/plugins/change_auth_plan/models.py:60
+#: xpack/plugins/change_auth_plan/models.py:56
#: xpack/plugins/gathered_user/models.py:24
msgid "Nodes"
msgstr "节点"
@@ -463,7 +443,8 @@ msgid "Is active"
msgstr "激活"
#: assets/models/asset.py:193 assets/models/cluster.py:19
-#: assets/models/user.py:191 assets/models/user.py:326 templates/_nav.html:44
+#: assets/models/user.py:195 assets/models/user.py:330 templates/_nav.html:44
+#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers.py:205
msgid "Admin user"
msgstr "特权用户"
@@ -539,29 +520,23 @@ msgstr "标签管理"
#: assets/models/cluster.py:28 assets/models/cmd_filter.py:26
#: assets/models/cmd_filter.py:67 assets/models/group.py:21
#: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:25
-#: orgs/models.py:437 perms/models/base.py:55 users/models/user.py:602
-#: users/serializers/group.py:33 xpack/plugins/change_auth_plan/models.py:92
-#: xpack/plugins/cloud/models.py:119 xpack/plugins/gathered_user/models.py:30
+#: orgs/models.py:437 perms/models/base.py:51 users/models/user.py:645
+#: users/serializers/group.py:33 xpack/plugins/change_auth_plan/models.py:81
+#: xpack/plugins/cloud/models.py:114 xpack/plugins/gathered_user/models.py:30
msgid "Created by"
msgstr "创建者"
-# msgid "Created by"
-# msgstr "创建者"
#: assets/models/asset.py:219 assets/models/base.py:181
#: assets/models/cluster.py:26 assets/models/domain.py:27
#: assets/models/gathered_user.py:19 assets/models/group.py:22
#: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50
#: ops/models/adhoc.py:38 ops/models/command.py:29 orgs/models.py:26
-#: orgs/models.py:435 perms/models/base.py:56 users/models/group.py:18
-#: users/models/user.py:789 xpack/plugins/cloud/models.py:122
+#: orgs/models.py:435 perms/models/base.py:52 users/models/group.py:18
+#: users/models/user.py:836 xpack/plugins/cloud/models.py:117
msgid "Date created"
msgstr "创建日期"
-#: assets/models/authbook.py:17
-msgid "Version"
-msgstr "版本"
-
-#: assets/models/authbook.py:24
+#: assets/models/authbook.py:23
msgid "AuthBook"
msgstr "账号"
@@ -586,15 +561,15 @@ msgstr "可连接性"
msgid "Date verified"
msgstr "校验日期"
-#: assets/models/base.py:178 xpack/plugins/change_auth_plan/models.py:82
-#: xpack/plugins/change_auth_plan/models.py:214
-#: xpack/plugins/change_auth_plan/models.py:325
+#: assets/models/base.py:178 xpack/plugins/change_auth_plan/models.py:72
+#: xpack/plugins/change_auth_plan/models.py:197
+#: xpack/plugins/change_auth_plan/models.py:292
msgid "SSH private key"
msgstr "SSH密钥"
-#: assets/models/base.py:179 xpack/plugins/change_auth_plan/models.py:85
-#: xpack/plugins/change_auth_plan/models.py:210
-#: xpack/plugins/change_auth_plan/models.py:321
+#: assets/models/base.py:179 xpack/plugins/change_auth_plan/models.py:75
+#: xpack/plugins/change_auth_plan/models.py:193
+#: xpack/plugins/change_auth_plan/models.py:288
msgid "SSH public key"
msgstr "SSH公钥"
@@ -612,7 +587,7 @@ msgstr "带宽"
msgid "Contact"
msgstr "联系人"
-#: assets/models/cluster.py:22 users/models/user.py:580
+#: assets/models/cluster.py:22 users/models/user.py:623
msgid "Phone"
msgstr "手机"
@@ -638,7 +613,7 @@ msgid "Default"
msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14
-#: users/models/user.py:774
+#: users/models/user.py:821
msgid "System"
msgstr "系统"
@@ -646,7 +621,7 @@ msgstr "系统"
msgid "Default Cluster"
msgstr "默认Cluster"
-#: assets/models/cmd_filter.py:33 assets/models/user.py:209
+#: assets/models/cmd_filter.py:33 assets/models/user.py:213
msgid "Command filter"
msgstr "命令过滤器"
@@ -748,73 +723,73 @@ msgstr "全称"
msgid "Parent key"
msgstr "ssh私钥"
-#: assets/models/node.py:559 assets/serializers/system_user.py:199
+#: assets/models/node.py:559 assets/serializers/system_user.py:200
#: users/templates/users/user_asset_permission.html:41
#: users/templates/users/user_asset_permission.html:73
#: users/templates/users/user_asset_permission.html:158
-#: xpack/plugins/cloud/models.py:93 xpack/plugins/cloud/serializers.py:210
+#: xpack/plugins/cloud/models.py:93 xpack/plugins/cloud/serializers.py:206
msgid "Node"
msgstr "节点"
-#: assets/models/user.py:185
+#: assets/models/user.py:189
msgid "Automatic managed"
msgstr "托管密码"
-#: assets/models/user.py:186
+#: assets/models/user.py:190
msgid "Manually input"
msgstr "手动输入"
-#: assets/models/user.py:190
+#: assets/models/user.py:194
msgid "Common user"
msgstr "普通用户"
-#: assets/models/user.py:193
+#: assets/models/user.py:197
msgid "Username same with user"
msgstr "用户名与用户相同"
-#: assets/models/user.py:196 assets/serializers/domain.py:28
-#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:56
+#: assets/models/user.py:200 assets/serializers/domain.py:28
+#: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:52
msgid "Assets"
msgstr "资产"
-#: assets/models/user.py:200 templates/_nav.html:17
+#: assets/models/user.py:204 templates/_nav.html:17
#: users/views/profile/pubkey.py:37
msgid "Users"
msgstr "用户管理"
-#: assets/models/user.py:201
+#: assets/models/user.py:205
msgid "User groups"
msgstr "用户组"
-#: assets/models/user.py:205
+#: assets/models/user.py:209
msgid "Auto push"
msgstr "自动推送"
-#: assets/models/user.py:206
+#: assets/models/user.py:210
msgid "Sudo"
msgstr "Sudo"
-#: assets/models/user.py:207
+#: assets/models/user.py:211
msgid "Shell"
msgstr "Shell"
-#: assets/models/user.py:208
+#: assets/models/user.py:212
msgid "Login mode"
msgstr "认证方式"
-#: assets/models/user.py:210
+#: assets/models/user.py:214
msgid "SFTP Root"
msgstr "SFTP根路径"
-#: assets/models/user.py:211 authentication/models.py:95
+#: assets/models/user.py:215 authentication/models.py:94
msgid "Token"
msgstr ""
-#: assets/models/user.py:212
+#: assets/models/user.py:216
msgid "Home"
msgstr "家目录"
-#: assets/models/user.py:213
+#: assets/models/user.py:217
msgid "System groups"
msgstr "用户组"
@@ -823,6 +798,10 @@ msgstr "用户组"
msgid "%(value)s is not an even number"
msgstr "%(value)s is not an even number"
+#: assets/serializers/account.py:31 assets/serializers/account.py:52
+msgid "System user display"
+msgstr "系统用户名称"
+
#: assets/serializers/asset.py:20
msgid "Protocol format should {}/{}"
msgstr "协议格式 {}/{}"
@@ -839,11 +818,16 @@ msgstr "网域名称"
msgid "Nodes name"
msgstr "节点名称"
-#: assets/serializers/asset.py:96
+#: assets/serializers/asset.py:97
msgid "Hardware info"
msgstr "硬件信息"
-#: assets/serializers/asset.py:98
+#: assets/serializers/asset.py:98 assets/serializers/system_user.py:218
+#: orgs/mixins/serializers.py:26
+msgid "Org name"
+msgstr "用户名"
+
+#: assets/serializers/asset.py:99
msgid "Admin user display"
msgstr "特权用户名称"
@@ -887,7 +871,7 @@ msgstr "密钥指纹"
msgid "Nodes amount"
msgstr "节点数量"
-#: assets/serializers/system_user.py:53 assets/serializers/system_user.py:201
+#: assets/serializers/system_user.py:53 assets/serializers/system_user.py:202
msgid "Login mode display"
msgstr "认证方式名称"
@@ -895,27 +879,31 @@ msgstr "认证方式名称"
msgid "Ad domain"
msgstr "Ad 网域"
-#: assets/serializers/system_user.py:95
+#: assets/serializers/system_user.py:56
+msgid "Is asset protocol"
+msgstr ""
+
+#: assets/serializers/system_user.py:96
msgid "Username same with user with protocol {} only allow 1"
msgstr "用户名和用户相同的一种协议只允许存在一个"
-#: assets/serializers/system_user.py:109
+#: assets/serializers/system_user.py:110
msgid "* Automatic login mode must fill in the username."
msgstr "自动登录模式,必须填写用户名"
-#: assets/serializers/system_user.py:123
+#: assets/serializers/system_user.py:124
msgid "Path should starts with /"
msgstr "路径应该以 / 开头"
-#: assets/serializers/system_user.py:148
+#: assets/serializers/system_user.py:149
msgid "Password or private key required"
msgstr "密码或密钥密码需要一个"
-#: assets/serializers/system_user.py:216
+#: assets/serializers/system_user.py:217
msgid "System user name"
msgstr "系统用户名称"
-#: assets/serializers/system_user.py:226
+#: assets/serializers/system_user.py:227
msgid "Asset hostname"
msgstr "资产主机名"
@@ -1069,7 +1057,7 @@ msgid "Symlink"
msgstr "建立软链接"
#: audits/models.py:37 audits/models.py:60 audits/models.py:76
-#: terminal/models/session.py:45
+#: terminal/models/session.py:45 terminal/models/sharing.py:76
msgid "Remote addr"
msgstr "远端地址"
@@ -1081,18 +1069,16 @@ msgstr "操作"
msgid "Filename"
msgstr "文件名"
-#: audits/models.py:42 audits/models.py:101
+#: audits/models.py:42 audits/models.py:101 terminal/models/sharing.py:84
msgid "Success"
msgstr "成功"
-#: audits/models.py:43 ops/models/command.py:30 perms/models/base.py:53
+#: audits/models.py:43 ops/models/command.py:30 perms/models/base.py:49
#: terminal/models/session.py:52
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:43
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:74
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:40
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:78
-#: xpack/plugins/change_auth_plan/models.py:194
-#: xpack/plugins/change_auth_plan/models.py:340
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:53
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:45
+#: xpack/plugins/change_auth_plan/models.py:177
+#: xpack/plugins/change_auth_plan/models.py:307
#: xpack/plugins/gathered_user/models.py:76
msgid "Date start"
msgstr "开始日期"
@@ -1158,18 +1144,19 @@ msgstr "用户代理"
#: audits/models.py:110
#: authentication/templates/authentication/_mfa_confirm_modal.html:14
#: authentication/templates/authentication/login_otp.html:6
-#: users/forms/profile.py:64 users/models/user.py:583
+#: users/forms/profile.py:64 users/models/user.py:626
#: users/serializers/profile.py:102
msgid "MFA"
msgstr "多因子认证"
-#: audits/models.py:111 xpack/plugins/change_auth_plan/models.py:336
-#: xpack/plugins/cloud/models.py:176
+#: audits/models.py:111 terminal/models/sharing.py:88
+#: xpack/plugins/change_auth_plan/models.py:303
+#: xpack/plugins/cloud/models.py:171
msgid "Reason"
msgstr "原因"
-#: audits/models.py:112 tickets/models/ticket.py:47
-#: xpack/plugins/cloud/models.py:172 xpack/plugins/cloud/models.py:221
+#: audits/models.py:112 tickets/models/ticket.py:57
+#: xpack/plugins/cloud/models.py:167 xpack/plugins/cloud/models.py:216
msgid "Status"
msgstr "状态"
@@ -1185,7 +1172,7 @@ msgstr "认证方式"
msgid "Operate display"
msgstr "操作名称"
-#: audits/serializers.py:30 tickets/serializers/ticket/ticket.py:24
+#: audits/serializers.py:30 tickets/serializers/ticket/ticket.py:23
msgid "Status display"
msgstr "状态名称"
@@ -1203,7 +1190,7 @@ msgid "Hosts display"
msgstr "主机名称"
#: audits/serializers.py:89 ops/models/command.py:26
-#: xpack/plugins/cloud/models.py:170
+#: xpack/plugins/cloud/models.py:165
msgid "Result"
msgstr "结果"
@@ -1237,13 +1224,13 @@ msgstr "认证令牌"
#: audits/signals_handler.py:66
#: authentication/templates/authentication/login.html:210
-#: notifications/backends/__init__.py:13
+#: notifications/backends/__init__.py:11
msgid "WeCom"
msgstr "企业微信"
#: audits/signals_handler.py:67
#: authentication/templates/authentication/login.html:215
-#: notifications/backends/__init__.py:14
+#: notifications/backends/__init__.py:12
msgid "DingTalk"
msgstr "钉钉"
@@ -1430,11 +1417,11 @@ msgstr "{ApplicationPermission} *添加了* {SystemUser}"
msgid "{ApplicationPermission} *REMOVE* {SystemUser}"
msgstr "{ApplicationPermission} *移除了* {SystemUser}"
-#: authentication/api/connection_token.py:222
+#: authentication/api/connection_token.py:226
msgid "Invalid token"
msgstr "无效的令牌"
-#: authentication/api/mfa.py:64
+#: authentication/api/mfa.py:81
msgid "Code is invalid"
msgstr "Code无效"
@@ -1489,59 +1476,59 @@ msgstr ""
msgid "Invalid token or cache refreshed."
msgstr ""
-#: authentication/errors.py:25
+#: authentication/errors.py:27
msgid "Username/password check failed"
msgstr "用户名/密码 校验失败"
-#: authentication/errors.py:26
+#: authentication/errors.py:28
msgid "Password decrypt failed"
msgstr "密码解密失败"
-#: authentication/errors.py:27
+#: authentication/errors.py:29
msgid "MFA failed"
msgstr "多因子认证失败"
-#: authentication/errors.py:28
+#: authentication/errors.py:30
msgid "MFA unset"
msgstr "多因子认证没有设定"
-#: authentication/errors.py:29
+#: authentication/errors.py:31
msgid "Username does not exist"
msgstr "用户名不存在"
-#: authentication/errors.py:30
+#: authentication/errors.py:32
msgid "Password expired"
msgstr "密码已过期"
-#: authentication/errors.py:31
+#: authentication/errors.py:33
msgid "Disabled or expired"
msgstr "禁用或失效"
-#: authentication/errors.py:32
+#: authentication/errors.py:34
msgid "This account is inactive."
msgstr "此账户已禁用"
-#: authentication/errors.py:33
+#: authentication/errors.py:35
msgid "This account is expired"
msgstr "此账户已过期"
-#: authentication/errors.py:34
+#: authentication/errors.py:36
msgid "Auth backend not match"
msgstr "没有匹配到认证后端"
-#: authentication/errors.py:35
+#: authentication/errors.py:37
msgid "ACL is not allowed"
msgstr "ACL 不被允许"
-#: authentication/errors.py:36
+#: authentication/errors.py:38
msgid "Only local users are allowed"
msgstr "仅允许本地用户"
-#: authentication/errors.py:46
+#: authentication/errors.py:48
msgid "No session found, check your cookie"
msgstr "会话已变更,刷新页面"
-#: authentication/errors.py:48
+#: authentication/errors.py:50
#, python-brace-format
msgid ""
"The username or password you entered is incorrect, please enter it again. "
@@ -1551,62 +1538,78 @@ msgstr ""
"您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将"
"被临时 锁定 {block_time} 分钟)"
-#: authentication/errors.py:54 authentication/errors.py:58
+#: authentication/errors.py:56 authentication/errors.py:60
msgid ""
"The account has been locked (please contact admin to unlock it or try again "
"after {} minutes)"
msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)"
-#: authentication/errors.py:62
-#, python-brace-format
+#: authentication/errors.py:64
msgid ""
-"MFA code invalid, or ntp sync server time, You can also try {times_try} "
-"times (The account will be temporarily locked for {block_time} minutes)"
+"One-time password invalid, or ntp sync server time, You can also try "
+"{times_try} times (The account will be temporarily locked for {block_time} "
+"minutes)"
msgstr ""
-"MFA验证码不正确,或者服务器端时间不对。 您还可以尝试 {times_try} 次(账号将被"
+"虚拟MFA 不正确,或者服务器端时间不对。 您还可以尝试 {times_try} 次(账号将被"
"临时 锁定 {block_time} 分钟)"
-#: authentication/errors.py:67
+#: authentication/errors.py:69
+msgid ""
+"SMS verify code invalid,You can also try {times_try} times (The account will "
+"be temporarily locked for {block_time} minutes)"
+msgstr ""
+"短信验证码不正确,或者服务器端时间不对。 您还可以尝试 {times_try} 次(账号将被"
+"临时 锁定 {block_time} 分钟)"
+
+#: authentication/errors.py:74
+msgid ""
+"The MFA type({mfa_type}) is not supported, You can also try {times_try} times "
+"(The account will be temporarily locked for {block_time} minutes)"
+msgstr ""
+"该({mfa_type}) MFA 类型不支持。 您还可以尝试 {times_try} 次(账号将被"
+"临时 锁定 {block_time} 分钟)"
+
+#: authentication/errors.py:79
msgid "MFA required"
msgstr "需要多因子认证"
-#: authentication/errors.py:68
+#: authentication/errors.py:80
msgid "MFA not set, please set it first"
msgstr "多因子认证没有设置,请先完成设置"
-#: authentication/errors.py:69
+#: authentication/errors.py:81
msgid "Login confirm required"
msgstr "需要登录复核"
-#: authentication/errors.py:70
+#: authentication/errors.py:82
msgid "Wait login confirm ticket for accept"
msgstr "等待登录复核处理"
-#: authentication/errors.py:71
+#: authentication/errors.py:83
msgid "Login confirm ticket was {}"
msgstr "登录复核 {}"
-#: authentication/errors.py:235
+#: authentication/errors.py:260
msgid "IP is not allowed"
msgstr "来源 IP 不被允许登录"
-#: authentication/errors.py:268
+#: authentication/errors.py:293
msgid "SSO auth closed"
msgstr "SSO 认证关闭了"
-#: authentication/errors.py:273 authentication/mixins.py:277
+#: authentication/errors.py:298 authentication/mixins.py:319
msgid "Your password is too simple, please change it for security"
msgstr "你的密码过于简单,为了安全,请修改"
-#: authentication/errors.py:282 authentication/mixins.py:284
+#: authentication/errors.py:307 authentication/mixins.py:326
msgid "You should to change your password before login"
msgstr "登录完成前,请先修改密码"
-#: authentication/errors.py:291 authentication/mixins.py:291
+#: authentication/errors.py:316 authentication/mixins.py:333
msgid "Your password has expired, please reset before logging in"
msgstr "您的密码已过期,先修改再登录"
-#: authentication/errors.py:325
+#: authentication/errors.py:350
msgid "Your password is invalid"
msgstr "您的密码无效"
@@ -1614,12 +1617,20 @@ msgstr "您的密码无效"
msgid "{} days auto login"
msgstr "{} 天内自动登录"
-#: authentication/forms.py:46 authentication/forms.py:59
-#: authentication/forms.py:61 users/forms/profile.py:27
+#: authentication/forms.py:46
+msgid "MFA Code"
+msgstr "MFA 验证码"
+
+#: authentication/forms.py:47
+msgid "MFA type"
+msgstr "MFA 类型"
+
+#: authentication/forms.py:60 authentication/forms.py:62
+#: users/forms/profile.py:27
msgid "MFA code"
msgstr "多因子认证验证码"
-#: authentication/mixins.py:267
+#: authentication/mixins.py:309
msgid "Please change your password"
msgstr "请修改密码"
@@ -1627,10 +1638,22 @@ msgstr "请修改密码"
msgid "Private Token"
msgstr "SSH密钥"
-#: authentication/models.py:96
+#: authentication/models.py:95
msgid "Expired"
msgstr "过期时间"
+#: authentication/sms_verify_code.py:17
+msgid "The verification code has expired. Please resend it"
+msgstr "验证码已过期,请重新发送"
+
+#: authentication/sms_verify_code.py:22
+msgid "The verification code is incorrect"
+msgstr "验证码错误"
+
+#: authentication/sms_verify_code.py:27
+msgid "Please wait {} seconds before sending"
+msgstr "请在 {} 秒后发送"
+
#: authentication/templates/authentication/_access_key_modal.html:6
msgid "API key list"
msgstr "API Key列表"
@@ -1649,8 +1672,9 @@ msgid "ID"
msgstr "ID"
#: authentication/templates/authentication/_access_key_modal.html:31
+#: settings/serializers/auth/radius.py:17
msgid "Secret"
-msgstr "秘钥"
+msgstr "密钥"
#: authentication/templates/authentication/_access_key_modal.html:33
msgid "Date"
@@ -1662,14 +1686,14 @@ msgid "Show"
msgstr "显示"
#: authentication/templates/authentication/_access_key_modal.html:66
-#: settings/serializers/settings.py:149 users/models/user.py:468
+#: settings/serializers/security.py:25 users/models/user.py:470
#: users/serializers/profile.py:99
#: users/templates/users/user_verify_mfa.html:32
msgid "Disable"
msgstr "禁用"
#: authentication/templates/authentication/_access_key_modal.html:67
-#: users/models/user.py:469 users/serializers/profile.py:100
+#: users/models/user.py:471 users/serializers/profile.py:100
msgid "Enable"
msgstr "启用"
@@ -1679,7 +1703,7 @@ msgstr "删除成功"
#: authentication/templates/authentication/_access_key_modal.html:155
#: authentication/templates/authentication/_mfa_confirm_modal.html:53
-#: templates/_modal.html:22 tickets/const.py:20
+#: templates/_modal.html:22 tickets/const.py:36
msgid "Close"
msgstr "关闭"
@@ -1737,28 +1761,27 @@ msgid "CAS"
msgstr "CAS"
#: authentication/templates/authentication/login.html:220
-#: notifications/backends/__init__.py:16
+#: notifications/backends/__init__.py:14
msgid "FeiShu"
msgstr "飞书"
-#: authentication/templates/authentication/login_otp.html:17
-msgid "One-time password"
-msgstr "一次性密码"
+#: authentication/templates/authentication/login_otp.html:25
+msgid "Please enter the verification code"
+msgstr "请输入验证码"
-#: authentication/templates/authentication/login_otp.html:23
-#: users/templates/users/user_verify_mfa.html:13
-msgid "Open MFA Authenticator and enter the 6-bit dynamic code"
-msgstr "请打开MFA验证器,输入6位动态码"
+#: authentication/templates/authentication/login_otp.html:28
+msgid "Send verification code"
+msgstr "发送验证码"
-#: authentication/templates/authentication/login_otp.html:26
-#: users/templates/users/user_otp_check_password.html:15
+#: authentication/templates/authentication/login_otp.html:29
+#: users/templates/users/user_otp_check_password.html:16
#: users/templates/users/user_otp_enable_bind.html:24
#: users/templates/users/user_otp_enable_install_app.html:29
#: users/templates/users/user_verify_mfa.html:26
msgid "Next"
msgstr "下一步"
-#: authentication/templates/authentication/login_otp.html:29
+#: authentication/templates/authentication/login_otp.html:32
msgid "Can't provide security? Please contact the administrator!"
msgstr "如果不能提供多因子认证验证码,请联系管理员!"
@@ -1882,7 +1905,7 @@ msgstr "正在跳转到 {} 认证"
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
-#: authentication/views/login.py:224
+#: authentication/views/login.py:215
msgid ""
"Wait for {} confirm, You also can copy link to her/him
\n"
" Don't close this page"
@@ -1890,18 +1913,26 @@ msgstr ""
"等待 {} 确认, 你也可以复制链接发给他/她
\n"
" 不要关闭本页面"
-#: authentication/views/login.py:229
+#: authentication/views/login.py:220
msgid "No ticket found"
msgstr "没有发现工单"
-#: authentication/views/login.py:261
+#: authentication/views/login.py:252
msgid "Logout success"
msgstr "退出登录成功"
-#: authentication/views/login.py:262
+#: authentication/views/login.py:253
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
+#: authentication/views/mfa.py:44 users/models/user.py:37
+msgid "One-time password"
+msgstr "一次性密码"
+
+#: authentication/views/mfa.py:50 notifications/backends/__init__.py:15
+msgid "SMS"
+msgstr "短信"
+
#: authentication/views/wecom.py:41
msgid "WeCom Error, Please contact your system administrator"
msgstr "企业微信错误,请联系系统管理员"
@@ -1949,6 +1980,10 @@ msgstr "%(name)s 创建成功"
msgid "%(name)s was updated successfully"
msgstr "%(name)s 更新成功"
+#: common/db/encoder.py:17
+msgid "ugettext_lazy"
+msgstr ""
+
#: common/db/models.py:71
msgid "Updated by"
msgstr "更新人"
@@ -2022,6 +2057,22 @@ msgstr "加密的字段"
msgid "Network error, please contact system administrator"
msgstr "网络错误,请联系系统管理员"
+#: common/message/backends/sms/__init__.py:38
+msgid "Invalid SMS sign and template: {}"
+msgstr "无效的短信签名和模版: {}"
+
+#: common/message/backends/sms/__init__.py:43
+msgid "Alibaba"
+msgstr "阿里云"
+
+#: common/message/backends/sms/__init__.py:44
+msgid "Tencent"
+msgstr "腾讯云"
+
+#: common/message/backends/sms/alibaba.py:56
+msgid "Signature does not match"
+msgstr "签名不匹配"
+
#: common/message/backends/wecom/__init__.py:15
msgid "WeCom error, please contact system administrator"
msgstr "企业微信错误,请联系系统管理员"
@@ -2062,7 +2113,7 @@ msgstr "JumpServer 开源堡垒机"
msgid "Flower service unavailable, check it
"
msgstr "Flower 服务不可用,请检查"
-#: jumpserver/views/other.py:25
+#: jumpserver/views/other.py:26
msgid ""
"Luna is a separately deployed program, you need to deploy Luna, koko, "
"configure nginx for url distribution,
If you see this page, "
@@ -2071,11 +2122,11 @@ msgstr ""
"Luna是单独部署的一个程序,你需要部署luna,koko,
如果你看到了"
"这个页面,证明你访问的不是nginx监听的端口,祝你好运
"
-#: jumpserver/views/other.py:69
+#: jumpserver/views/other.py:70
msgid "Websocket server run on port: {}, you should proxy it on nginx"
msgstr "Websocket 服务运行在端口: {}, 请检查nginx是否代理是否设置"
-#: jumpserver/views/other.py:83
+#: jumpserver/views/other.py:84
msgid ""
"Koko is a separately deployed program, you need to deploy Koko, "
"configure nginx for url distribution,
If you see this page, "
@@ -2085,12 +2136,12 @@ msgstr ""
"div>如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运"
"div>"
-#: notifications/backends/__init__.py:12 users/forms/profile.py:101
-#: users/models/user.py:563
+#: notifications/backends/__init__.py:10 users/forms/profile.py:101
+#: users/models/user.py:606
msgid "Email"
msgstr "邮件"
-#: notifications/backends/__init__.py:15
+#: notifications/backends/__init__.py:13
msgid "Site message"
msgstr "站内信"
@@ -2111,15 +2162,16 @@ msgid "Cycle perform"
msgstr "周期执行"
#: ops/mixin.py:33 ops/mixin.py:90 ops/mixin.py:109 ops/mixin.py:150
+#: settings/serializers/auth/ldap.py:64
msgid "Regularly perform"
msgstr "定期执行"
#: ops/mixin.py:106 ops/mixin.py:147
-#: xpack/plugins/change_auth_plan/serializers.py:60
+#: xpack/plugins/change_auth_plan/serializers.py:55
msgid "Periodic perform"
msgstr "定时执行"
-#: ops/mixin.py:112
+#: ops/mixin.py:112 settings/serializers/auth/ldap.py:61
msgid "Interval"
msgstr "间隔"
@@ -2146,9 +2198,9 @@ msgstr ""
"分 时 日 月 星期> (
在线工"
"具)
注意: 如果同时设置了定期执行和周期执行,优先使用定期执行"
-#: ops/mixin.py:162
-msgid "Tips: (Units: hour)"
-msgstr "提示:(单位: 时)"
+#: ops/mixin.py:162 settings/serializers/auth/ldap.py:61
+msgid "Unit: hour"
+msgstr "单位: 时"
#: ops/models/adhoc.py:35
msgid "Callback"
@@ -2194,8 +2246,8 @@ msgstr "开始时间"
msgid "End time"
msgstr "完成时间"
-#: ops/models/adhoc.py:246 xpack/plugins/change_auth_plan/models.py:197
-#: xpack/plugins/change_auth_plan/models.py:343
+#: ops/models/adhoc.py:246 xpack/plugins/change_auth_plan/models.py:180
+#: xpack/plugins/change_auth_plan/models.py:310
#: xpack/plugins/gathered_user/models.py:79
msgid "Time"
msgstr "时间"
@@ -2233,22 +2285,22 @@ msgstr "任务结束"
msgid "Server performance"
msgstr "监控告警"
-#: ops/notifications.py:36
+#: ops/notifications.py:39
#, python-brace-format
msgid "The terminal is offline: {name}"
msgstr "终端已离线: {name}"
-#: ops/notifications.py:42
+#: ops/notifications.py:45
#, python-brace-format
msgid "[Disk] Disk used more than {max_threshold}%: => {value} ({name})"
msgstr "[Disk] 硬盘使用率超过 {max_threshold}%: => {value} ({name})"
-#: ops/notifications.py:49
+#: ops/notifications.py:52
#, python-brace-format
msgid "[Memory] Memory used more than {max_threshold}%: => {value} ({name})"
msgstr "[Memory] 内存使用率超过 {max_threshold}%: => {value} ({name})"
-#: ops/notifications.py:56
+#: ops/notifications.py:59
#, python-brace-format
msgid "[CPU] CPU load more than {max_threshold}: => {value} ({name})"
msgstr "[CPU] CPU 使用率超过 {max_threshold}: => {value} ({name})"
@@ -2277,6 +2329,12 @@ msgstr "当前组织 ({}) 不能被删除"
msgid "The organization have resource ({}) cannot be deleted"
msgstr "组织存在资源 ({}) 不能被删除"
+#: orgs/mixins/models.py:46 orgs/mixins/serializers.py:25 orgs/models.py:37
+#: orgs/models.py:432 orgs/serializers.py:106
+#: tickets/serializers/ticket/ticket.py:77
+msgid "Organization"
+msgstr "组织审计员"
+
#: orgs/models.py:17 users/const.py:12
msgid "Organization administrator"
msgstr "组织管理员"
@@ -2289,7 +2347,7 @@ msgstr "组织审计员"
msgid "GLOBAL"
msgstr "全局组织"
-#: orgs/models.py:434 users/models/user.py:571 users/serializers/user.py:36
+#: orgs/models.py:434 users/models/user.py:614 users/serializers/user.py:36
#: users/templates/users/_select_user_modal.html:15
msgid "Role"
msgstr "角色"
@@ -2302,11 +2360,11 @@ msgstr "管理员正在修改授权,请稍等"
msgid "The authorization cannot be revoked for the time being"
msgstr "该授权暂时不能撤销"
-#: perms/models/application_permission.py:27 users/models/user.py:177
+#: perms/models/application_permission.py:27 users/models/user.py:182
msgid "Application"
msgstr "应用程序"
-#: perms/models/asset_permission.py:37 settings/serializers/settings.py:118
+#: perms/models/asset_permission.py:37 settings/serializers/terminal.py:12
msgid "All"
msgstr "全部"
@@ -2353,8 +2411,8 @@ msgstr "未分组"
msgid "Favorite"
msgstr "收藏夹"
-#: perms/models/base.py:51 templates/_nav.html:21 users/models/group.py:31
-#: users/models/user.py:567 users/templates/users/_select_user_modal.html:16
+#: perms/models/base.py:47 templates/_nav.html:21 users/models/group.py:31
+#: users/models/user.py:610 users/templates/users/_select_user_modal.html:16
#: users/templates/users/user_asset_permission.html:39
#: users/templates/users/user_asset_permission.html:67
#: users/templates/users/user_database_app_permission.html:38
@@ -2362,15 +2420,17 @@ msgstr "收藏夹"
msgid "User group"
msgstr "用户组"
-#: perms/models/base.py:54
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:46
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:77
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:43
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:81
-#: users/models/user.py:599
+#: perms/models/base.py:50
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:56
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:48
+#: users/models/user.py:642
msgid "Date expired"
msgstr "失效日期"
+#: perms/models/base.py:54
+msgid "From ticket"
+msgstr "来自工单"
+
#: perms/serializers/application/permission.py:18
#: perms/serializers/application/permission.py:38
#: perms/serializers/asset/permission.py:42
@@ -2427,20 +2487,20 @@ msgstr "节点名称"
msgid "System users display"
msgstr "系统用户名称"
-#: settings/api/common.py:25
-msgid "Test mail sent to {}, please check"
-msgstr "邮件已经发送{}, 请检查"
+#: settings/api/alibaba_sms.py:30 settings/api/tencent_sms.py:34
+msgid "test_phone is required"
+msgstr "测试手机号 该字段是必填项。"
-#: settings/api/common.py:100 xpack/plugins/interface/api.py:18
-#: xpack/plugins/interface/models.py:36
-msgid "Welcome to the JumpServer open source Bastion Host"
-msgstr "欢迎使用JumpServer开源堡垒机"
-
-#: settings/api/dingtalk.py:36 settings/api/feishu.py:35
+#: settings/api/alibaba_sms.py:52 settings/api/dingtalk.py:36
+#: settings/api/feishu.py:35 settings/api/tencent_sms.py:57
#: settings/api/wecom.py:36
msgid "Test success"
msgstr "测试成功"
+#: settings/api/email.py:21
+msgid "Test mail sent to {}, please check"
+msgstr "邮件已经发送{}, 请检查"
+
#: settings/api/ldap.py:194
msgid "Get ldap users is None"
msgstr "获取 LDAP 用户为 None"
@@ -2449,170 +2509,125 @@ msgstr "获取 LDAP 用户为 None"
msgid "Imported {} users successfully (Organization: {})"
msgstr "成功导入 {} 个用户 ( 组织: {} )"
+#: settings/api/public.py:41 xpack/plugins/interface/api.py:18
+#: xpack/plugins/interface/models.py:36
+msgid "Welcome to the JumpServer open source Bastion Host"
+msgstr "欢迎使用JumpServer开源堡垒机"
+
#: settings/models.py:123 users/templates/users/reset_password.html:29
msgid "Setting"
msgstr "设置"
-#: settings/serializers/settings.py:16
-msgid "Site url"
-msgstr "当前站点URL"
+#: settings/serializers/auth/base.py:10
+msgid "CAS Auth"
+msgstr "CAS 认证"
-#: settings/serializers/settings.py:17
-msgid "eg: http://dev.jumpserver.org:8080"
-msgstr "如: http://dev.jumpserver.org:8080"
+#: settings/serializers/auth/base.py:11
+msgid "OPENID Auth"
+msgstr "OIDC 认证"
-#: settings/serializers/settings.py:21
-msgid "User guide url"
-msgstr "用户向导URL"
+#: settings/serializers/auth/base.py:12
+msgid "RADIUS Auth"
+msgstr "RADIUS 认证"
-#: settings/serializers/settings.py:22
-msgid "User first login update profile done redirect to it"
-msgstr "用户第一次登录,修改profile后重定向到地址, 可以是 wiki 或 其他说明文档"
+#: settings/serializers/auth/base.py:13
+msgid "DingTalk Auth"
+msgstr "钉钉 认证"
-#: settings/serializers/settings.py:25
+#: settings/serializers/auth/base.py:14
+msgid "FeiShu Auth"
+msgstr "飞书 认证"
+
+#: settings/serializers/auth/base.py:15
+msgid "WeCom Auth"
+msgstr "企业微信 认证"
+
+#: settings/serializers/auth/base.py:16
+msgid "SSO Auth"
+msgstr "SSO Token 认证"
+
+#: settings/serializers/auth/base.py:18 settings/serializers/basic.py:15
msgid "Forgot password url"
-msgstr "忘记密码URL"
+msgstr "忘记密码 URL"
-#: settings/serializers/settings.py:26
-msgid ""
-"The forgot password url on login page, If you use ldap or cas external "
-"authentication, you can set it"
-msgstr ""
-"登录页面忘记密码URL, 如果使用了 LDAP, OPENID 等外部认证系统,可以自定义用户重"
-"置密码访问的地址"
+#: settings/serializers/auth/base.py:21
+msgid "Health check token"
+msgstr "健康检查 Token"
-#: settings/serializers/settings.py:30
-msgid "Global organization name"
-msgstr "全局组织名"
+#: settings/serializers/auth/base.py:24
+msgid "Enable login redirect msg"
+msgstr "启用登录跳转提示"
-#: settings/serializers/settings.py:31
-msgid "The name of global organization to display"
-msgstr "全局组织的显示名称,默认为 全局组织"
+#: settings/serializers/auth/cas.py:11
+msgid "Enable CAS Auth"
+msgstr "启用 CAS 认证"
-#: settings/serializers/settings.py:38
-msgid "SMTP host"
-msgstr "SMTP 主机"
+#: settings/serializers/auth/cas.py:12 settings/serializers/auth/oidc.py:32
+msgid "Server url"
+msgstr "服务端地址"
-#: settings/serializers/settings.py:39
-msgid "SMTP port"
-msgstr "SMTP 端口"
+#: settings/serializers/auth/cas.py:13
+msgid "Logout completely"
+msgstr "同步注销"
-#: settings/serializers/settings.py:40
-msgid "SMTP account"
-msgstr "SMTP 账号"
+#: settings/serializers/auth/cas.py:15
+msgid "Username attr"
+msgstr "用户名属性"
-#: settings/serializers/settings.py:42
-msgid "SMTP password"
-msgstr "SMTP 密码"
+#: settings/serializers/auth/cas.py:16
+msgid "Enable attributes map"
+msgstr "启用属性映射"
-#: settings/serializers/settings.py:43
-msgid "Tips: Some provider use token except password"
-msgstr "提示:一些邮件提供商需要输入的是授权码"
+#: settings/serializers/auth/cas.py:17
+msgid "Rename attr"
+msgstr "映射属性"
-#: settings/serializers/settings.py:46
-msgid "Send user"
-msgstr "发件人"
+#: settings/serializers/auth/cas.py:18
+msgid "Create user if not"
+msgstr "创建用户(如果不存在)"
-#: settings/serializers/settings.py:47
-msgid "Tips: Send mail account, default SMTP account as the send account"
-msgstr "提示:发送邮件账号,默认使用 SMTP 账号作为发送账号"
+#: settings/serializers/auth/dingtalk.py:11
+msgid "Enable DingTalk Auth"
+msgstr "启用钉钉认证"
-#: settings/serializers/settings.py:50
-msgid "Test recipient"
-msgstr "测试收件人"
+#: settings/serializers/auth/feishu.py:10
+msgid "Enable FeiShu Auth"
+msgstr "启用飞书认证"
-#: settings/serializers/settings.py:51
-msgid "Tips: Used only as a test mail recipient"
-msgstr "提示:仅用来作为测试邮件收件人"
-
-#: settings/serializers/settings.py:54
-msgid "Use SSL"
-msgstr "使用 SSL"
-
-#: settings/serializers/settings.py:55
-msgid "If SMTP port is 465, may be select"
-msgstr "如果SMTP端口是465,通常需要启用 SSL"
-
-#: settings/serializers/settings.py:58
-msgid "Use TLS"
-msgstr "使用 TLS"
-
-#: settings/serializers/settings.py:59
-msgid "If SMTP port is 587, may be select"
-msgstr "如果SMTP端口是587,通常需要启用 TLS"
-
-#: settings/serializers/settings.py:62
-msgid "Subject prefix"
-msgstr "主题前缀"
-
-#: settings/serializers/settings.py:69
-msgid "Create user email subject"
-msgstr "邮件主题"
-
-#: settings/serializers/settings.py:70
-msgid ""
-"Tips: When creating a user, send the subject of the email (eg:Create account "
-"successfully)"
-msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)"
-
-#: settings/serializers/settings.py:74
-msgid "Create user honorific"
-msgstr "邮件的敬语"
-
-#: settings/serializers/settings.py:75
-msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)"
-msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)"
-
-#: settings/serializers/settings.py:79
-msgid "Create user email content"
-msgstr "邮件的内容"
-
-#: settings/serializers/settings.py:80
-msgid "Tips:When creating a user, send the content of the email"
-msgstr "提示: 创建用户时,发送设置密码邮件的内容"
-
-#: settings/serializers/settings.py:83
-msgid "Signature"
-msgstr "署名"
-
-#: settings/serializers/settings.py:84
-msgid "Tips: Email signature (eg:jumpserver)"
-msgstr "邮件署名 (如:jumpserver)"
-
-#: settings/serializers/settings.py:92
+#: settings/serializers/auth/ldap.py:39
msgid "LDAP server"
msgstr "LDAP 地址"
-#: settings/serializers/settings.py:92
+#: settings/serializers/auth/ldap.py:40
msgid "eg: ldap://localhost:389"
msgstr "如: ldap://localhost:389"
-#: settings/serializers/settings.py:94
+#: settings/serializers/auth/ldap.py:42
msgid "Bind DN"
msgstr "绑定 DN"
-#: settings/serializers/settings.py:97
+#: settings/serializers/auth/ldap.py:46
msgid "User OU"
msgstr "用户 OU"
-#: settings/serializers/settings.py:98
+#: settings/serializers/auth/ldap.py:47
msgid "Use | split multi OUs"
msgstr "多个 OU 使用 | 分割"
-#: settings/serializers/settings.py:101
+#: settings/serializers/auth/ldap.py:50
msgid "User search filter"
msgstr "用户过滤器"
-#: settings/serializers/settings.py:102
+#: settings/serializers/auth/ldap.py:51
#, python-format
msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)"
msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)"
-#: settings/serializers/settings.py:105
+#: settings/serializers/auth/ldap.py:54
msgid "User attr map"
msgstr "用户属性映射"
-#: settings/serializers/settings.py:106
+#: settings/serializers/auth/ldap.py:55
msgid ""
"User attr map present how to map LDAP user attr to jumpserver, username,name,"
"email is jumpserver attr"
@@ -2620,23 +2635,526 @@ msgstr ""
"用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name,"
"email 是jumpserver的用户需要属性"
-#: settings/serializers/settings.py:108
+#: settings/serializers/auth/ldap.py:58 xpack/plugins/cloud/serializers.py:207
+#: xpack/plugins/gathered_user/serializers.py:20
+msgid "Periodic display"
+msgstr "定时执行"
+
+#: settings/serializers/auth/ldap.py:66
+msgid "Connect timeout"
+msgstr "连接超时时间"
+
+#: settings/serializers/auth/ldap.py:67
+msgid "Search paged size"
+msgstr "搜索分页数量"
+
+#: settings/serializers/auth/ldap.py:69
msgid "Enable LDAP auth"
msgstr "启用 LDAP 认证"
-#: settings/serializers/settings.py:119
+#: settings/serializers/auth/oidc.py:12
+msgid "Base site url"
+msgstr "JumpServer 地址"
+
+#: settings/serializers/auth/oidc.py:15
+msgid "Client Id"
+msgstr "客户端 ID"
+
+#: settings/serializers/auth/oidc.py:18 xpack/plugins/cloud/serializers.py:33
+msgid "Client Secret"
+msgstr "客户端密钥"
+
+#: settings/serializers/auth/oidc.py:20
+msgid "Share session"
+msgstr "共享会话"
+
+#: settings/serializers/auth/oidc.py:22
+msgid "Ignore ssl verification"
+msgstr "忽略 SSL 证书验证"
+
+#: settings/serializers/auth/oidc.py:29
+msgid "Use Keycloak"
+msgstr "使用 Keycloak"
+
+#: settings/serializers/auth/oidc.py:35
+msgid "Realm name"
+msgstr "域"
+
+#: settings/serializers/auth/oidc.py:41
+msgid "Enable OPENID Auth"
+msgstr "启用 OIDC 认证"
+
+#: settings/serializers/auth/oidc.py:43
+msgid "Provider endpoint"
+msgstr "端点地址"
+
+#: settings/serializers/auth/oidc.py:46
+msgid "Provider auth endpoint"
+msgstr "授权端点地址"
+
+#: settings/serializers/auth/oidc.py:49
+msgid "Provider token endpoint"
+msgstr "token 端点地址"
+
+#: settings/serializers/auth/oidc.py:52
+msgid "Provider jwks endpoint"
+msgstr "jwks 端点地址"
+
+#: settings/serializers/auth/oidc.py:55
+msgid "Provider userinfo endpoint"
+msgstr "用户信息端点地址"
+
+#: settings/serializers/auth/oidc.py:58
+msgid "Provider end session endpoint"
+msgstr "注销会话端点地址"
+
+#: settings/serializers/auth/oidc.py:61
+msgid "Provider sign alg"
+msgstr "签名算法"
+
+#: settings/serializers/auth/oidc.py:64
+msgid "Provider sign key"
+msgstr "签名 Key"
+
+#: settings/serializers/auth/oidc.py:66
+msgid "Scopes"
+msgstr "连接范围"
+
+#: settings/serializers/auth/oidc.py:68
+msgid "Id token max age"
+msgstr "令牌有效时间"
+
+#: settings/serializers/auth/oidc.py:71
+msgid "Id token include claims"
+msgstr "声明"
+
+#: settings/serializers/auth/oidc.py:73
+msgid "Use state"
+msgstr "使用状态"
+
+#: settings/serializers/auth/oidc.py:74
+msgid "Use nonce"
+msgstr "临时使用"
+
+#: settings/serializers/auth/oidc.py:76
+msgid "Always update user"
+msgstr "总是更新用户信息"
+
+#: settings/serializers/auth/radius.py:13
+msgid "Enable RADIUS Auth"
+msgstr "启用 RADIUS 认证"
+
+#: settings/serializers/auth/radius.py:19
+msgid "OTP in radius"
+msgstr "使用 Radius OTP"
+
+#: settings/serializers/auth/sms.py:10
+msgid "Enable SMS"
+msgstr "启用 SMS"
+
+#: settings/serializers/auth/sms.py:11
+msgid "Test phone"
+msgstr "测试手机号"
+
+#: settings/serializers/auth/sms.py:25 settings/serializers/auth/sms.py:43
+msgid "Signatures and Templates"
+msgstr "签名和模版"
+
+#: settings/serializers/auth/sms.py:25 settings/serializers/auth/sms.py:43
+msgid ""
+"\n"
+" Filling in JSON Data: \n"
+" {\n"
+" \"verification_code\": {\n"
+" \"sign_name\": \"
\", \n"
+" \"template_code\": \"\"\n"
+" }\n"
+" }\n"
+" "
+msgstr ""
+
+#: settings/serializers/auth/sso.py:12
+msgid "Enable SSO auth"
+msgstr "启用 SSO Token 认证"
+
+#: settings/serializers/auth/sso.py:13
+msgid "Other service can using SSO token login to JumpServer without password"
+msgstr "其它系统可以使用 SSO Token 对接 JumpServer, 免去登录的过程"
+
+#: settings/serializers/auth/sso.py:16
+msgid "SSO auth key TTL"
+msgstr "Token 有效期"
+
+#: settings/serializers/auth/sso.py:16 settings/serializers/security.py:72
+msgid "Unit: second"
+msgstr "单位: 秒"
+
+#: settings/serializers/auth/wecom.py:11
+msgid "Enable WeCom Auth"
+msgstr "启用企业微信认证"
+
+#: settings/serializers/basic.py:7
+msgid "Site url"
+msgstr "当前站点URL"
+
+#: settings/serializers/basic.py:8
+msgid "eg: http://dev.jumpserver.org:8080"
+msgstr "如: http://dev.jumpserver.org:8080"
+
+#: settings/serializers/basic.py:11
+msgid "User guide url"
+msgstr "用户向导URL"
+
+#: settings/serializers/basic.py:12
+msgid "User first login update profile done redirect to it"
+msgstr "用户第一次登录,修改profile后重定向到地址, 可以是 wiki 或 其他说明文档"
+
+#: settings/serializers/basic.py:16
+msgid ""
+"The forgot password url on login page, If you use ldap or cas external "
+"authentication, you can set it"
+msgstr ""
+"登录页面忘记密码URL, 如果使用了 LDAP, OPENID 等外部认证系统,可以自定义用户重"
+"置密码访问的地址"
+
+#: settings/serializers/basic.py:20
+msgid "Global organization name"
+msgstr "全局组织名"
+
+#: settings/serializers/basic.py:21
+msgid "The name of global organization to display"
+msgstr "全局组织的显示名称,默认为 全局组织"
+
+#: settings/serializers/cleaning.py:9
+msgid "Login log keep days"
+msgstr "登录日志"
+
+#: settings/serializers/cleaning.py:9 settings/serializers/cleaning.py:12
+#: settings/serializers/cleaning.py:15 settings/serializers/cleaning.py:18
+#: settings/serializers/cleaning.py:21
+msgid "Unit: day"
+msgstr "单位: 天"
+
+#: settings/serializers/cleaning.py:12
+msgid "Task log keep days"
+msgstr "任务日志"
+
+#: settings/serializers/cleaning.py:15
+msgid "Operate log keep days"
+msgstr "操作日志"
+
+#: settings/serializers/cleaning.py:18
+msgid "FTP log keep days"
+msgstr "上传下载"
+
+#: settings/serializers/cleaning.py:21
+msgid "Cloud sync record keep days"
+msgstr "云同步记录"
+
+#: settings/serializers/email.py:24
+msgid "SMTP host"
+msgstr "SMTP 主机"
+
+#: settings/serializers/email.py:25
+msgid "SMTP port"
+msgstr "SMTP 端口"
+
+#: settings/serializers/email.py:26
+msgid "SMTP account"
+msgstr "SMTP 账号"
+
+#: settings/serializers/email.py:28
+msgid "SMTP password"
+msgstr "SMTP 密码"
+
+#: settings/serializers/email.py:29
+msgid "Tips: Some provider use token except password"
+msgstr "提示:一些邮件提供商需要输入的是授权码"
+
+#: settings/serializers/email.py:32
+msgid "Send user"
+msgstr "发件人"
+
+#: settings/serializers/email.py:33
+msgid "Tips: Send mail account, default SMTP account as the send account"
+msgstr "提示:发送邮件账号,默认使用 SMTP 账号作为发送账号"
+
+#: settings/serializers/email.py:36
+msgid "Test recipient"
+msgstr "测试收件人"
+
+#: settings/serializers/email.py:37
+msgid "Tips: Used only as a test mail recipient"
+msgstr "提示:仅用来作为测试邮件收件人"
+
+#: settings/serializers/email.py:40
+msgid "Use SSL"
+msgstr "使用 SSL"
+
+#: settings/serializers/email.py:41
+msgid "If SMTP port is 465, may be select"
+msgstr "如果SMTP端口是465,通常需要启用 SSL"
+
+#: settings/serializers/email.py:44
+msgid "Use TLS"
+msgstr "使用 TLS"
+
+#: settings/serializers/email.py:45
+msgid "If SMTP port is 587, may be select"
+msgstr "如果SMTP端口是587,通常需要启用 TLS"
+
+#: settings/serializers/email.py:48
+msgid "Subject prefix"
+msgstr "主题前缀"
+
+#: settings/serializers/email.py:55
+msgid "Create user email subject"
+msgstr "邮件主题"
+
+#: settings/serializers/email.py:56
+msgid ""
+"Tips: When creating a user, send the subject of the email (eg:Create account "
+"successfully)"
+msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)"
+
+#: settings/serializers/email.py:60
+msgid "Create user honorific"
+msgstr "邮件的敬语"
+
+#: settings/serializers/email.py:61
+msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)"
+msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)"
+
+#: settings/serializers/email.py:65
+msgid "Create user email content"
+msgstr "邮件的内容"
+
+#: settings/serializers/email.py:66
+msgid "Tips:When creating a user, send the content of the email"
+msgstr "提示: 创建用户时,发送设置密码邮件的内容"
+
+#: settings/serializers/email.py:69
+msgid "Signature"
+msgstr "署名"
+
+#: settings/serializers/email.py:70
+msgid "Tips: Email signature (eg:jumpserver)"
+msgstr "邮件署名 (如:jumpserver)"
+
+#: settings/serializers/other.py:9
+msgid "Email suffix"
+msgstr "邮件后缀"
+
+#: settings/serializers/other.py:10
+msgid ""
+"This is used by default if no email is returned during SSO authentication"
+msgstr "SSO认证时,如果没有返回邮件地址,将使用该后缀"
+
+#: settings/serializers/other.py:12
+msgid "Enable tickets"
+msgstr "启用工单系统"
+
+#: settings/serializers/other.py:15
+msgid "OTP issuer name"
+msgstr "OTP 扫描后的名称"
+
+#: settings/serializers/other.py:17
+msgid "OTP valid window"
+msgstr "OTP 延迟有效次数"
+
+#: settings/serializers/other.py:19
+msgid "Enable period task"
+msgstr "启用周期任务"
+
+#: settings/serializers/other.py:21
+msgid "Ansible windows default shell"
+msgstr "Ansible windows shell"
+
+#: settings/serializers/other.py:25
+msgid "Perm single to ungroup node"
+msgstr "直接授权资产放在未分组节点"
+
+#: settings/serializers/security.py:8
+msgid "Password minimum length"
+msgstr "密码最小长度"
+
+#: settings/serializers/security.py:12
+msgid "Admin user password minimum length"
+msgstr "管理员密码最小长度"
+
+#: settings/serializers/security.py:15
+msgid "Must contain capital"
+msgstr "必须包含大写字符"
+
+#: settings/serializers/security.py:17
+msgid "Must contain lowercase"
+msgstr "必须包含小写字符"
+
+#: settings/serializers/security.py:18
+msgid "Must contain numeric"
+msgstr "必须包含数字"
+
+#: settings/serializers/security.py:19
+msgid "Must contain special"
+msgstr "必须包含特殊字符"
+
+#: settings/serializers/security.py:26
+msgid "All users"
+msgstr "所有用户"
+
+#: settings/serializers/security.py:27
+msgid "Only admin users"
+msgstr "仅管理员"
+
+#: settings/serializers/security.py:29
+msgid "Global MFA auth"
+msgstr "全局启用 MFA 认证"
+
+#: settings/serializers/security.py:33
+msgid "Limit the number of login failures"
+msgstr "限制登录失败次数"
+
+#: settings/serializers/security.py:37
+msgid "Block logon interval"
+msgstr "禁止登录时间间隔"
+
+#: settings/serializers/security.py:39
+msgid ""
+"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 "单位:分, 当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录"
+
+#: settings/serializers/security.py:45
+msgid "User password expiration"
+msgstr "用户密码过期时间"
+
+#: settings/serializers/security.py:47
+msgid ""
+"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 ""
+"单位:天, 如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期提醒邮件"
+"将在密码过期前5天内由系统(每天)自动发送给用户"
+
+#: settings/serializers/security.py:54
+msgid "Number of repeated historical passwords"
+msgstr "不能设置近几次密码"
+
+#: settings/serializers/security.py:56
+msgid ""
+"Tip: When the user resets the password, it cannot be the previous n "
+"historical passwords of the user"
+msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码"
+
+#: settings/serializers/security.py:61
+msgid "Only single device login"
+msgstr "仅一台设备登录"
+
+#: settings/serializers/security.py:62
+msgid "Next device login, pre login will be logout"
+msgstr "下个设备登录,上次登录会被顶掉"
+
+#: settings/serializers/security.py:65
+msgid "Only exist user login"
+msgstr "仅已存在用户登录"
+
+#: settings/serializers/security.py:66 settings/serializers/security.py:70
+msgid "If enable, CAS、OIDC auth will be failed, if user not exist yet"
+msgstr "开启后,如果系统中不存在该用户,CAS、OIDC 登录将会失败"
+
+#: settings/serializers/security.py:69
+msgid "Only from source login"
+msgstr "仅从用户来源登录"
+
+#: settings/serializers/security.py:72
+msgid "MFA verify TTL"
+msgstr "MFA 校验有效期"
+
+#: settings/serializers/security.py:75
+msgid "Enable Login captcha"
+msgstr "启用登录验证码"
+
+#: settings/serializers/security.py:81
+msgid "Enable terminal register"
+msgstr "终端注册"
+
+#: settings/serializers/security.py:82
+msgid ""
+"Allow terminal register, after all terminal setup, you should disable this "
+"for security"
+msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭"
+
+#: settings/serializers/security.py:85
+msgid "Replay watermark"
+msgstr "录像水印"
+
+#: settings/serializers/security.py:86
+msgid "Enabled, the session replay contains watermark information"
+msgstr "启用后,会话录像将包含水印信息"
+
+#: settings/serializers/security.py:90
+msgid "Connection max idle time"
+msgstr "连接最大空闲时间"
+
+#: settings/serializers/security.py:91
+msgid "If idle time more than it, disconnect connection Unit: minute"
+msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)"
+
+#: settings/serializers/security.py:94
+msgid "Remember manual auth"
+msgstr "保存手动输入密码"
+
+#: settings/serializers/security.py:97
+msgid "Enable change auth secure mode"
+msgstr "启用改密安全模式"
+
+#: settings/serializers/security.py:100
+msgid "Insecure command alert"
+msgstr "危险命令告警"
+
+#: settings/serializers/security.py:103
+msgid "Email recipient"
+msgstr "邮件收件人"
+
+#: settings/serializers/security.py:104
+msgid "Multiple user using , split"
+msgstr "多个用户,使用 , 分割"
+
+#: settings/serializers/security.py:107
+msgid "Batch command execution"
+msgstr "批量命令执行"
+
+#: settings/serializers/security.py:108
+msgid "Allow user run batch command or not using ansible"
+msgstr "是否允许用户使用 ansible 执行批量命令"
+
+#: settings/serializers/security.py:111
+msgid "Session share"
+msgstr "会话分享"
+
+#: settings/serializers/security.py:112
+msgid "Enabled, Allows user active session to be shared with other users"
+msgstr "开启后允许用户分享已连接的资产会话给它人,协同工作"
+
+#: settings/serializers/sms.py:7
+msgid "Label"
+msgstr "标签"
+
+#: settings/serializers/terminal.py:13
msgid "Auto"
msgstr "自动"
-#: settings/serializers/settings.py:125
+#: settings/serializers/terminal.py:19
msgid "Password auth"
msgstr "密码认证"
-#: settings/serializers/settings.py:127
+#: settings/serializers/terminal.py:21
msgid "Public key auth"
msgstr "密钥认证"
-#: settings/serializers/settings.py:128
+#: settings/serializers/terminal.py:22
msgid ""
"Tips: If use other auth method, like AD/LDAP, you should disable this to "
"avoid being able to log in after deleting"
@@ -2644,170 +3162,47 @@ msgstr ""
"提示:如果你使用其它认证方式,如 AD/LDAP,你应该禁用此项,以避免第三方系统删"
"除后,还可以登录"
-#: settings/serializers/settings.py:131
+#: settings/serializers/terminal.py:25
msgid "List sort by"
msgstr "资产列表排序"
-#: settings/serializers/settings.py:132
+#: settings/serializers/terminal.py:27
msgid "List page size"
msgstr "资产列表每页数量"
-#: settings/serializers/settings.py:134
+#: settings/serializers/terminal.py:29
msgid "Session keep duration"
msgstr "会话日志保存时间"
-#: settings/serializers/settings.py:135
+#: settings/serializers/terminal.py:30
msgid ""
-"Units: days, Session, record, command will be delete if more than duration, "
+"Unit: days, Session, record, command will be delete if more than duration, "
"only in database"
msgstr ""
"单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不"
"受影响)"
-#: settings/serializers/settings.py:137
+#: settings/serializers/terminal.py:33
msgid "Telnet login regex"
msgstr "Telnet 成功正则表达式"
-#: settings/serializers/settings.py:139
+#: settings/serializers/terminal.py:34
+msgid ""
+"The login success message varies with devices. if you cannot log in to the "
+"device through Telnet, set this parameter"
+msgstr "不同设备登录成功提示不一样,所以如果 telnet 不能正常登录,可以这里设置"
+
+#: settings/serializers/terminal.py:38
msgid "RDP address"
msgstr "RDP 地址"
-#: settings/serializers/settings.py:142
+#: settings/serializers/terminal.py:39
msgid "RDP visit address, eg: dev.jumpserver.org:3389"
msgstr "RDP 访问地址, 如: dev.jumpserver.org:3389"
-#: settings/serializers/settings.py:150
-msgid "All users"
-msgstr "所有用户"
-
-#: settings/serializers/settings.py:151
-msgid "Only admin users"
-msgstr "仅管理员"
-
-#: settings/serializers/settings.py:153
-msgid "Global MFA auth"
-msgstr "全局启用 MFA 认证"
-
-#: settings/serializers/settings.py:156
-msgid "Batch command execution"
-msgstr "批量命令执行"
-
-#: settings/serializers/settings.py:157
-msgid "Allow user run batch command or not using ansible"
-msgstr "是否允许用户使用 ansible 执行批量命令"
-
-#: settings/serializers/settings.py:160
-msgid "Enable terminal register"
-msgstr "终端注册"
-
-#: settings/serializers/settings.py:161
-msgid ""
-"Allow terminal register, after all terminal setup, you should disable this "
-"for security"
-msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭"
-
-#: settings/serializers/settings.py:164
-msgid "Replay watermark"
-msgstr "录像水印"
-
-#: settings/serializers/settings.py:165
-msgid "Enabled, the session replay contains watermark information"
-msgstr "启用后,会话录像将包含水印信息"
-
-#: settings/serializers/settings.py:169
-msgid "Limit the number of login failures"
-msgstr "限制登录失败次数"
-
-#: settings/serializers/settings.py:173
-msgid "Block logon interval"
-msgstr "禁止登录时间间隔"
-
-#: settings/serializers/settings.py:174
-msgid ""
-"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 ""
-"提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录"
-
-#: settings/serializers/settings.py:178
-msgid "Connection max idle time"
-msgstr "连接最大空闲时间"
-
-#: settings/serializers/settings.py:179
-msgid "If idle time more than it, disconnect connection Unit: minute"
-msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)"
-
-#: settings/serializers/settings.py:183
-msgid "User password expiration"
-msgstr "用户密码过期时间"
-
-#: settings/serializers/settings.py:184
-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 ""
-"提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期"
-"提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户"
-
-#: settings/serializers/settings.py:188
-msgid "Number of repeated historical passwords"
-msgstr "不能设置近几次密码"
-
-#: settings/serializers/settings.py:189
-msgid ""
-"Tip: When the user resets the password, it cannot be the previous n "
-"historical passwords of the user"
-msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码"
-
-#: settings/serializers/settings.py:193
-msgid "Password minimum length"
-msgstr "密码最小长度"
-
-#: settings/serializers/settings.py:197
-msgid "Admin user password minimum length"
-msgstr "管理员密码最小长度"
-
-#: settings/serializers/settings.py:200
-msgid "Must contain capital"
-msgstr "必须包含大写字符"
-
-#: settings/serializers/settings.py:202
-msgid "Must contain lowercase"
-msgstr "必须包含小写字符"
-
-#: settings/serializers/settings.py:203
-msgid "Must contain numeric"
-msgstr "必须包含数字"
-
-#: settings/serializers/settings.py:204
-msgid "Must contain special"
-msgstr "必须包含特殊字符"
-
-#: settings/serializers/settings.py:205
-msgid "Insecure command alert"
-msgstr "危险命令告警"
-
-#: settings/serializers/settings.py:207
-msgid "Email recipient"
-msgstr "邮件收件人"
-
-#: settings/serializers/settings.py:208
-msgid "Multiple user using , split"
-msgstr "多个用户,使用 , 分割"
-
-#: settings/serializers/settings.py:216
-msgid "Enable WeCom Auth"
-msgstr "启用企业微信认证"
-
-#: settings/serializers/settings.py:223
-msgid "Enable DingTalk Auth"
-msgstr "启用钉钉认证"
-
-#: settings/serializers/settings.py:229
-msgid "Enable FeiShu Auth"
-msgstr "启用飞书认证"
+#: settings/serializers/terminal.py:42
+msgid "Enable XRDP"
+msgstr "启用 XRDP 服务"
#: settings/utils/ldap.py:412
msgid "ldap:// or ldaps:// protocol is used."
@@ -3375,6 +3770,10 @@ msgstr "用户不存在: {}"
msgid "User does not have permission"
msgstr "用户没有权限"
+#: terminal/api/sharing.py:28
+msgid "Secure session sharing settings is disabled"
+msgstr "未开启会话共享"
+
#: terminal/api/storage.py:30
msgid "Deleting the default storage is not allowed"
msgstr "不允许删除默认存储配置"
@@ -3428,7 +3827,8 @@ msgstr "输入"
msgid "Output"
msgstr "输出"
-#: terminal/backends/command/models.py:23
+#: terminal/backends/command/models.py:23 terminal/models/sharing.py:15
+#: terminal/models/sharing.py:58
msgid "Session"
msgstr "会话"
@@ -3474,7 +3874,7 @@ msgstr "不支持批量创建"
msgid "Storage is invalid"
msgstr "存储无效"
-#: terminal/models/session.py:44
+#: terminal/models/session.py:44 terminal/models/sharing.py:81
msgid "Login from"
msgstr "登录来源"
@@ -3486,6 +3886,50 @@ msgstr "回放"
msgid "Date end"
msgstr "结束日期"
+#: terminal/models/sharing.py:20
+msgid "Creator"
+msgstr "创建者"
+
+#: terminal/models/sharing.py:22 terminal/models/sharing.py:60
+msgid "Verify code"
+msgstr "验证码"
+
+#: terminal/models/sharing.py:27
+msgid "Expired time (min)"
+msgstr "过期时间 (分)"
+
+#: terminal/models/sharing.py:48
+msgid "Link not active"
+msgstr "链接失效"
+
+#: terminal/models/sharing.py:50
+msgid "Link expired"
+msgstr "是否过期"
+
+#: terminal/models/sharing.py:63
+msgid "Session sharing"
+msgstr "会话分享"
+
+#: terminal/models/sharing.py:67 terminal/serializers/sharing.py:49
+msgid "Joiner"
+msgstr "加入者"
+
+#: terminal/models/sharing.py:70
+msgid "Date joined"
+msgstr "加入日期"
+
+#: terminal/models/sharing.py:73
+msgid "Date left"
+msgstr "结束日期"
+
+#: terminal/models/sharing.py:91 xpack/plugins/change_auth_plan/models.py:274
+msgid "Finished"
+msgstr "结束"
+
+#: terminal/models/sharing.py:111
+msgid "Invalid verification code"
+msgstr "验证码不正确"
+
#: terminal/models/status.py:18
msgid "Session Online"
msgstr "在线会话"
@@ -3584,18 +4028,18 @@ msgstr ""
"
\n"
" "
-#: terminal/notifications.py:94
+#: terminal/notifications.py:99
#, python-format
msgid ""
"Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $"
"%(command)s"
msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s"
-#: terminal/notifications.py:112
+#: terminal/notifications.py:117
msgid "Batch danger command alert"
msgstr "批量危险命令告警"
-#: terminal/notifications.py:123
+#: terminal/notifications.py:128
#, python-format
msgid ""
"\n"
@@ -3626,7 +4070,7 @@ msgstr ""
" ----------------- 命令 ----------------
\n"
" "
-#: terminal/notifications.py:148
+#: terminal/notifications.py:150
#, python-format
msgid "Insecure Web Command Execution Alert: [%(name)s]"
msgstr "批量危险命令告警: [%(name)s]"
@@ -3676,7 +4120,7 @@ msgstr "Secret key"
msgid "Endpoint"
msgstr "端点"
-#: terminal/serializers/storage.py:66 xpack/plugins/cloud/models.py:214
+#: terminal/serializers/storage.py:66 xpack/plugins/cloud/models.py:209
msgid "Region"
msgstr "地域"
@@ -3724,10 +4168,6 @@ msgstr "忽略证书认证"
msgid "Not found"
msgstr "没有发现"
-#: tickets/api/ticket.py:61 tickets/api/ticket.py:70
-msgid "Ticket already closed"
-msgstr "工单已经关闭"
-
#: tickets/const.py:8
msgid "General"
msgstr "一般"
@@ -3740,133 +4180,129 @@ msgstr "申请资产"
msgid "Apply for application"
msgstr "申请应用"
-#: tickets/const.py:17 tickets/const.py:24
+#: tickets/const.py:17 tickets/const.py:30 tickets/const.py:35
msgid "Open"
msgstr "打开"
-#: tickets/const.py:18
+#: tickets/const.py:18 tickets/const.py:25
+msgid "Approved"
+msgstr "已同意"
+
+#: tickets/const.py:19 tickets/const.py:26
+msgid "Rejected"
+msgstr "已拒绝"
+
+#: tickets/const.py:20 tickets/const.py:31
+msgid "Closed"
+msgstr "关闭的"
+
+#: tickets/const.py:24
+msgid "Notified"
+msgstr "已通知"
+
+#: tickets/const.py:37
msgid "Approve"
msgstr "同意"
-#: tickets/const.py:25
-msgid "Closed"
-msgstr "关闭"
+#: tickets/const.py:42
+msgid "One level"
+msgstr "1 级"
-#: tickets/handler/apply_application.py:55
+#: tickets/const.py:43
+msgid "Two level"
+msgstr "2 级"
+
+#: tickets/const.py:47
+msgid "Super admin"
+msgstr "超级管理员"
+
+#: tickets/const.py:48
+msgid "Org admin"
+msgstr "组织管理员"
+
+#: tickets/const.py:49
+msgid "Super admin and org admin"
+msgstr "组织管理员或超级管理员"
+
+#: tickets/const.py:50
+msgid "Custom user"
+msgstr "自定义用户"
+
+#: tickets/errors.py:9
+msgid "Ticket already closed"
+msgstr "工单已经关闭"
+
+#: tickets/handler/apply_application.py:51
msgid "Applied category"
msgstr "申请的类别"
-#: tickets/handler/apply_application.py:56
+#: tickets/handler/apply_application.py:52
msgid "Applied type"
msgstr "申请的类型"
-#: tickets/handler/apply_application.py:57
+#: tickets/handler/apply_application.py:53
msgid "Applied application group"
msgstr "申请的应用组"
-#: tickets/handler/apply_application.py:58 tickets/handler/apply_asset.py:59
+#: tickets/handler/apply_application.py:54 tickets/handler/apply_asset.py:47
msgid "Applied system user group"
msgstr "申请的系统用户组"
-#: tickets/handler/apply_application.py:59 tickets/handler/apply_asset.py:61
+#: tickets/handler/apply_application.py:55 tickets/handler/apply_asset.py:49
msgid "Applied date start"
msgstr "申请的开始日期"
-#: tickets/handler/apply_application.py:60 tickets/handler/apply_asset.py:62
+#: tickets/handler/apply_application.py:56 tickets/handler/apply_asset.py:50
msgid "Applied date expired"
msgstr "申请的失效日期"
-#: tickets/handler/apply_application.py:75
-msgid "Approved applications"
-msgstr "批准的应用"
-
-#: tickets/handler/apply_application.py:76 tickets/handler/apply_asset.py:79
-msgid "Approved system users"
-msgstr "批准的系统用户"
-
-#: tickets/handler/apply_application.py:77 tickets/handler/apply_asset.py:81
-msgid "Approved date start"
-msgstr "批准的开始日期"
-
-#: tickets/handler/apply_application.py:78 tickets/handler/apply_asset.py:82
-msgid "Approved date expired"
-msgstr "批准的失效日期"
-
-#: tickets/handler/apply_application.py:100 tickets/handler/apply_asset.py:103
+#: tickets/handler/apply_application.py:78 tickets/handler/apply_asset.py:71
msgid ""
"Created by the ticket, ticket title: {}, ticket applicant: {}, ticket "
"processor: {}, ticket ID: {}"
msgstr ""
"通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}"
-#: tickets/handler/apply_asset.py:57
-msgid "Applied IP group"
-msgstr "申请的IP组"
-
-#: tickets/handler/apply_asset.py:58
+#: tickets/handler/apply_asset.py:46
msgid "Applied hostname group"
msgstr "申请的主机名组"
-#: tickets/handler/apply_asset.py:60
+#: tickets/handler/apply_asset.py:48
msgid "Applied actions"
msgstr "申请的动作"
-#: tickets/handler/apply_asset.py:78
-msgid "Approved assets"
-msgstr "批准的资产"
-
-#: tickets/handler/apply_asset.py:80
-msgid "Approved actions"
-msgstr "批准的动作"
-
-#: tickets/handler/base.py:62
+#: tickets/handler/base.py:86
msgid "{} {} the ticket"
msgstr "{} {}工单"
-#: tickets/handler/base.py:91
+#: tickets/handler/base.py:113
msgid "Ticket title"
msgstr "工单标题"
-#: tickets/handler/base.py:92
+#: tickets/handler/base.py:114
msgid "Ticket type"
msgstr "工单类型"
-#: tickets/handler/base.py:93
+#: tickets/handler/base.py:115
msgid "Ticket status"
msgstr "工单状态"
-#: tickets/handler/base.py:94
-msgid "Ticket action"
-msgstr "工单动作"
-
-#: tickets/handler/base.py:95
+#: tickets/handler/base.py:116
msgid "Ticket applicant"
msgstr "工单申请人"
-#: tickets/handler/base.py:96
-msgid "Ticket assignees"
-msgstr "工单受理人"
-
-#: tickets/handler/base.py:99
-msgid "Ticket processor"
-msgstr "工单处理人"
-
-#: tickets/handler/base.py:100
+#: tickets/handler/base.py:118
msgid "Ticket basic info"
msgstr "工单基本信息"
-#: tickets/handler/base.py:114 tickets/handler/base.py:121
+#: tickets/handler/base.py:129
msgid "No content"
msgstr "无内容"
-#: tickets/handler/base.py:116
+#: tickets/handler/base.py:131
msgid "Ticket applied info"
msgstr "工单申请信息"
-#: tickets/handler/base.py:123
-msgid "Ticket approved info"
-msgstr "工单批准信息"
-
#: tickets/handler/command_confirm.py:24
msgid "Applied run user"
msgstr "申请运行的用户"
@@ -3927,105 +4363,106 @@ msgstr "用户显示名称"
msgid "Body"
msgstr "内容"
-#: tickets/models/ticket.py:28
-msgid "ugettext_lazy"
-msgstr ""
+#: tickets/models/flow.py:19 tickets/models/flow.py:55
+#: tickets/models/ticket.py:25
+msgid "Approve level"
+msgstr "审批级别"
-#: tickets/models/ticket.py:35
-msgid "Title"
-msgstr "标题"
+#: tickets/models/flow.py:24 tickets/serializers/ticket/ticket.py:140
+msgid "Approve strategy"
+msgstr "审批策略"
-#: tickets/models/ticket.py:52
-msgid "Applicant"
-msgstr "申请人"
-
-#: tickets/models/ticket.py:55
-msgid "Applicant display"
-msgstr "申请人名称"
-
-#: tickets/models/ticket.py:60
-msgid "Processor"
-msgstr "处理人"
-
-#: tickets/models/ticket.py:63
-msgid "Processor display"
-msgstr "处理人名称"
-
-#: tickets/models/ticket.py:67
+#: tickets/models/flow.py:29 tickets/serializers/ticket/ticket.py:141
msgid "Assignees"
msgstr "受理人"
-#: tickets/models/ticket.py:70
+#: tickets/models/flow.py:33
msgid "Assignees display"
msgstr "受理人名称"
+#: tickets/models/flow.py:37
+msgid "Ticket flow approval rule"
+msgstr "工单批准信息"
+
+#: tickets/models/flow.py:60
+msgid "Ticket flow"
+msgstr "工单流程"
+
+#: tickets/models/ticket.py:38
+msgid "Ticket assignee"
+msgstr "工单受理人"
+
+#: tickets/models/ticket.py:45
+msgid "Title"
+msgstr "标题"
+
+#: tickets/models/ticket.py:53
+msgid "State"
+msgstr "状态"
+
+#: tickets/models/ticket.py:61
+msgid "Approval step"
+msgstr "审批步骤"
+
+#: tickets/models/ticket.py:66
+msgid "Applicant"
+msgstr "申请人"
+
+#: tickets/models/ticket.py:68
+msgid "Applicant display"
+msgstr "申请人名称"
+
+#: tickets/models/ticket.py:69
+msgid "Process"
+msgstr "流程"
+
+#: tickets/models/ticket.py:74
+msgid "TicketFlow"
+msgstr "工单流程"
+
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:16
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:16
+msgid "Apply name"
+msgstr "应用名称"
+
#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:35
-msgid "Application group"
-msgstr "应用组"
+msgid "Apply applications"
+msgstr "申请应用"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:39
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:28
-msgid "System user group"
-msgstr "系统用户组"
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:40
+msgid "Apply applications display"
+msgstr "应用名称名称"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:53
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:50
-msgid "Permission name"
-msgstr "授权名称"
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:44
+msgid "Apply system users"
+msgstr "系统用户"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:56
-msgid "Approve applications"
-msgstr "批准的应用"
-
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:61
-msgid "Approve applications display"
-msgstr "批准的应用名称"
-
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:65
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:62
-msgid "Approve system users"
-msgstr "批准的系统用户"
-
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:70
-msgid "Approve system user display"
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:49
+msgid "Apply system user display"
msgstr "批准的系统用户名称"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:90
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:94
+#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:69
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:61
+#: tickets/serializers/ticket/ticket.py:127
msgid "Permission named `{}` already exists"
msgstr "授权名称 `{}` 已存在"
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:107
-msgid "No `Application` are found under Organization `{}`"
-msgstr "在组织 `{}` 下没有发现 `应用`"
-
-#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:125
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:124
-msgid "No `SystemUser` are found under Organization `{}`"
-msgstr "在组织 `{}` 下没有发现 `系统用户`"
-
#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:20
-msgid "IP group"
-msgstr "IP组"
+msgid "Apply assets"
+msgstr "申请资产"
#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:24
-msgid "Hostname group"
-msgstr "主机名组"
-
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:36
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:57
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:66
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:74
msgid "Approve assets display"
msgstr "批准的资产名称"
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:53
-msgid "Approve assets"
-msgstr "批准的资产"
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:29
+msgid "Approve system users"
+msgstr "批准的系统用户"
-#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:108
-msgid "No `Asset` are found under Organization `{}`"
-msgstr "在组织 `{}` 下没有发现 `资产`"
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:33
+#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:41
+msgid "Apply assets display"
+msgstr "批准的资产名称"
#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:12
msgid "Run user"
@@ -4056,6 +4493,7 @@ msgid "From cmd filter"
msgstr "来自命令过滤规则"
#: tickets/serializers/ticket/meta/ticket_type/common.py:11
+#: tickets/serializers/ticket/ticket.py:122
msgid "Created by ticket ({}-{})"
msgstr "通过工单创建 ({}-{})"
@@ -4075,49 +4513,49 @@ msgstr "登录系统用户"
msgid "Login datetime"
msgstr "登录日期"
-#: tickets/serializers/ticket/ticket.py:21
-msgid "Action display"
-msgstr "动作名称"
-
-#: tickets/serializers/ticket/ticket.py:101
+#: tickets/serializers/ticket/ticket.py:95
msgid ""
"The `type` in the submission data (`{}`) is different from the type in the "
"request url (`{}`)"
msgstr "提交数据中的类型 (`{}`) 与请求URL地址中的类型 (`{}`) 不一致"
-#: tickets/serializers/ticket/ticket.py:122
-msgid "None of the assignees belong to Organization `{}` admins"
-msgstr "所有受理人都不属于组织 `{}` 下的管理员"
+#: tickets/serializers/ticket/ticket.py:115
+msgid "The ticket flow `{}` does not exist"
+msgstr "工单流程 `{}` 不存在"
-#: tickets/utils.py:36
+#: tickets/serializers/ticket/ticket.py:182
+msgid "The current organization type already exists"
+msgstr "当前组织已存在该类型"
+
+#: tickets/utils.py:37
msgid "New Ticket - {} ({})"
msgstr "新工单 - {} ({})"
-#: tickets/utils.py:38
+#: tickets/utils.py:39
msgid "Your has a new ticket, applicant - {}"
msgstr "你有一个新的工单, 申请人 - {}"
-#: tickets/utils.py:40 tickets/utils.py:59
+#: tickets/utils.py:41 tickets/utils.py:60
msgid "click here to review"
msgstr "点击查看"
-#: tickets/utils.py:55
+#: tickets/utils.py:56
msgid "Ticket has processed - {} ({})"
msgstr "工单已处理 - {} ({})"
-#: tickets/utils.py:57
+#: tickets/utils.py:58
msgid "Your ticket has been processed, processor - {}"
msgstr "你的工单已被处理, 处理人 - {}"
-#: users/api/user.py:214
+#: users/api/user.py:208
msgid "Could not reset self otp, use profile reset instead"
msgstr "不能在该页面重置多因子认证, 请去个人信息页面重置"
-#: users/const.py:10 users/models/user.py:174
+#: users/const.py:10 users/models/user.py:179
msgid "System administrator"
msgstr "系统管理员"
-#: users/const.py:11 users/models/user.py:175
+#: users/const.py:11 users/models/user.py:180
msgid "System auditor"
msgstr "系统审计员"
@@ -4133,6 +4571,14 @@ msgstr "设置密码"
msgid "MFA not enabled"
msgstr "MFA没有开启"
+#: users/exceptions.py:15
+msgid "Phone not set"
+msgstr "手机号没有设置"
+
+#: users/exceptions.py:20
+msgid "MFA method not support"
+msgstr "MFA 方法不支持"
+
#: users/forms/profile.py:49
msgid ""
"When enabled, you will enter the MFA binding process the next time you log "
@@ -4200,55 +4646,510 @@ msgid "Public key should not be the same as your old one."
msgstr "不能和原来的密钥相同"
#: users/forms/profile.py:149 users/serializers/profile.py:74
-#: users/serializers/profile.py:149 users/serializers/profile.py:162
+#: users/serializers/profile.py:150 users/serializers/profile.py:163
msgid "Not a valid ssh public key"
msgstr "SSH密钥不合法"
-#: users/forms/profile.py:160 users/models/user.py:591
+#: users/forms/profile.py:160 users/models/user.py:634
#: users/templates/users/user_password_update.html:48
msgid "Public key"
msgstr "SSH公钥"
-#: users/models/user.py:470
+#: users/models/user.py:38
+msgid "SMS verify code"
+msgstr "短信验证码"
+
+#: users/models/user.py:472
msgid "Force enable"
msgstr "强制启用"
-#: users/models/user.py:540
+#: users/models/user.py:583
msgid "Local"
msgstr "数据库"
-#: users/models/user.py:574
+#: users/models/user.py:617
msgid "Avatar"
msgstr "头像"
-#: users/models/user.py:577
+#: users/models/user.py:620
msgid "Wechat"
msgstr "微信"
-#: users/models/user.py:588
+#: users/models/user.py:631
msgid "Private key"
msgstr "ssh私钥"
-#: users/models/user.py:607
+#: users/models/user.py:650
msgid "Source"
msgstr "来源"
-#: users/models/user.py:611
+#: users/models/user.py:654
msgid "Date password last updated"
msgstr "最后更新密码日期"
-#: users/models/user.py:614
+#: users/models/user.py:657
msgid "Need update password"
msgstr "需要更新密码"
-#: users/models/user.py:770
+#: users/models/user.py:817
msgid "Administrator"
msgstr "管理员"
-#: users/models/user.py:773
+#: users/models/user.py:820
msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员"
+#: users/notifications.py:43 users/notifications.py:76
+#: users/templates/users/reset_password.html:5
+#: users/templates/users/reset_password.html:6
+msgid "Reset password"
+msgstr "重置密码"
+
+#: users/notifications.py:44
+msgid ""
+"\n"
+"Hello %(name)s:\n"
+"Please click the link below to reset your password, if not your request, "
+"concern your account security\n"
+"\n"
+"Click here reset password 👇\n"
+"%(rest_password_url)s?token=%(rest_password_token)s\n"
+"\n"
+"This link is valid for 1 hour. After it expires, \n"
+"\n"
+"request new one 👇\n"
+"%(forget_password_url)s?email=%(email)s\n"
+"\n"
+"-------------------\n"
+"\n"
+"Login direct 👇\n"
+"%(login_url)s\n"
+"\n"
+msgstr ""
+"\n"
+"您好 %(name)s:\n"
+"请点击下面链接重置密码, 如果不是您申请的,请关注账号安全\n "
+"\n"
+"请点击这里设置密码 👇\n"
+"%(rest_password_url)s?token=%(rest_password_token)s\n"
+"\n"
+"这个链接有效期1小时, 超过时间您可以, \n"
+"\n"
+"重新申请 👇\n"
+"%(forget_password_url)s?email=%(email)s\n"
+"\n"
+"-------------------\n"
+"\n"
+"直接登录 👇\n"
+"%(login_url)s\n"
+"\n"
+
+#: users/notifications.py:77
+msgid ""
+"\n"
+" Hello %(name)s:\n"
+"
\n"
+" Please click the link below to reset your password, if not your "
+"request, concern your account security\n"
+"
\n"
+" Click here reset password\n"
+"
\n"
+" This link is valid for 1 hour. After it expires, request new one\n"
+" \n"
+"
\n"
+" ---\n"
+" \n"
+"
\n"
+" Login direct\n"
+" \n"
+"
\n"
+" "
+msgstr ""
+"\n"
+" 您好 %(name)s:\n"
+"
\n"
+" 请点击下面链接重置密码, 如果不是您申请的,请关注账号安全\n"
+"
\n"
+" 请点击这"
+"里设置密码 \n"
+"
\n"
+" 这个链接有效期1小时, 超过时间您可以重新申请\n"
+"\n"
+"
\n"
+" ---\n"
+"\n"
+"
\n"
+" 直接登录\n"
+"\n"
+"
\n"
+" "
+
+#: users/notifications.py:116 users/notifications.py:150
+#: users/views/profile/reset.py:127
+msgid "Reset password success"
+msgstr "重置密码成功"
+
+#: users/notifications.py:117
+msgid ""
+"\n"
+" \n"
+"Hi %(name)s:\n"
+"\n"
+"Your JumpServer password has just been successfully updated.\n"
+"\n"
+"If the password update was not initiated by you, your account may have "
+"security issues. \n"
+"It is recommended that you log on to the JumpServer immediately and change "
+"your password.\n"
+"\n"
+"If you have any questions, you can contact the administrator.\n"
+"\n"
+"-------------------\n"
+"\n"
+"\n"
+"IP Address: %(ip_address)s\n"
+"
\n"
+"
\n"
+"Browser: %(browser)s\n"
+"
\n"
+" \n"
+" "
+msgstr ""
+"\n"
+" \n"
+"Hi %(name)s:\n"
+"\n"
+"你的 JumpServer 密码刚刚已经成功更新。\n"
+"\n"
+"如果这次密码更新不是由你发起的,那么你的账号可能存在安全问题。 "
+"\n"
+"建议你立刻登录 JumpServer 更改密码。 \n"
+"\n"
+"如果你有任何疑问,可以联系管理员。\n"
+"\n"
+"-------------------\n"
+"\n"
+"\n"
+"IP 地址: %(ip_address)s\n"
+"
\n"
+"
\n"
+"浏览器: %(browser)s\n"
+"
\n"
+" \n"
+" "
+
+#: users/notifications.py:151
+msgid ""
+"\n"
+" \n"
+" Hi %(name)s:\n"
+"
\n"
+" \n"
+" \n"
+"
\n"
+" Your JumpServer password has just been successfully updated.\n"
+"
\n"
+" \n"
+"
\n"
+" If the password update was not initiated by you, your account may "
+"have security issues. \n"
+" It is recommended that you log on to the JumpServer immediately and "
+"change your password.\n"
+"
\n"
+"\n"
+"
\n"
+" If you have any questions, you can contact the administrator.\n"
+"
\n"
+"
\n"
+" ---\n"
+"
\n"
+"
\n"
+" IP Address: %(ip_address)s\n"
+"
\n"
+"
\n"
+" Browser: %(browser)s\n"
+"
\n"
+" \n"
+" "
+msgstr ""
+"\n"
+" \n"
+" Hi %(name)s:\n"
+"
\n"
+" \n"
+" \n"
+"
\n"
+" 你的 JumpServer 密码刚刚已经成功更新。
\n"
+" \n"
+"
\n"
+" 如果这次密码更新不是由你发起的,那么你的账号可能存在安全问题。\n"
+" 建议你立刻登录 JumpServer 更改密码。\n"
+"
\n"
+" \n"
+"
\n"
+" 如果你有任何疑问,可以联系管理员。
\n"
+"
\n"
+" ---\n"
+"
\n"
+"
\n"
+" IP 地址: %(ip_address)s\n"
+"
\n"
+"
\n"
+" 浏览器: %(browser)s\n"
+"
\n"
+" \n"
+" "
+
+#: users/notifications.py:194 users/notifications.py:230
+msgid "Security notice"
+msgstr "安全通知"
+
+#: users/notifications.py:195
+msgid ""
+"\n"
+"Hello %(name)s:\n"
+"\n"
+"Your password will expire in %(date_password_expired)s,\n"
+"\n"
+"For your account security, please click on the link below to update your "
+"password in time\n"
+"\n"
+"Click here update password 👇\n"
+"%(update_password_url)s\n"
+"\n"
+"If your password has expired, please click 👇 to apply for a password reset "
+"email.\n"
+"%(forget_password_url)s?email=%(email)s\n"
+"\n"
+"-------------------\n"
+"\n"
+"Login direct 👇\n"
+"%(login_url)s\n"
+"\n"
+" "
+msgstr ""
+"\n"
+"您好 %(name)s:\n"
+"\n"
+"您的密码会在 %(date_password_expired)s 过期,\n"
+"\n"
+"为了您的账号安全,请点击下面的链接及时更新密码 \n"
+"\n"
+"请点击这里更新密码 👇\n"
+"%(update_password_url)s"
+"\n"
+"如果您的密码已经过期,请点击 👇 申请一份重置密码邮件。 "
+"\n"
+"%(forget_password_url)s?email=%(email)s\n"
+"\n"
+"-------------------\n"
+"\n"
+"直接登录 👇\n"
+"%(login_url)s\n"
+"\n"
+" "
+
+#: users/notifications.py:231
+msgid ""
+"\n"
+" Hello %(name)s:\n"
+"
\n"
+" Your password will expire in %(date_password_expired)s,\n"
+"
\n"
+" For your account security, please click on the link below to update "
+"your password in time\n"
+"
\n"
+" Click here update password\n"
+"
\n"
+" If your password has expired, please click \n"
+" Password "
+"expired \n"
+" to apply for a password reset email.\n"
+" \n"
+"
\n"
+" ---\n"
+" \n"
+"
\n"
+" Login direct\n"
+" \n"
+"
\n"
+" "
+msgstr ""
+"\n"
+" 您好 %(name)s:\n"
+"
\n"
+" 您的密码会在 %(date_password_expired)s 过期,\n"
+"
\n"
+" 为了您的账号安全,请点击下面的链接及时更新密码\n"
+"
\n"
+" 请点击这里更新密码\n"
+"
\n"
+" 如果您的密码已经过期,请点击 \n"
+" 密码过期 \n"
+" 申请一份重置密码邮件。\n"
+"\n"
+"
\n"
+" ---\n"
+"\n"
+"
\n"
+" 直接登录\n"
+"\n"
+"
\n"
+" "
+
+#: users/notifications.py:268 users/notifications.py:287
+msgid "Expiration notice"
+msgstr "过期通知"
+
+#: users/notifications.py:269
+msgid ""
+"\n"
+"Hello %(name)s:\n"
+"\n"
+"Your account will expire in %(date_expired)s,\n"
+"\n"
+"In order not to affect your normal work, please contact the administrator "
+"for confirmation.\n"
+"\n"
+" "
+msgstr ""
+"\n"
+"您好 %(name)s:\n"
+"\n"
+"您的账户会在 %(date_expired)s 过期,\n"
+"\n"
+"为了不影响您正常工作,请联系管理员确认。"
+"\n"
+" "
+
+#: users/notifications.py:288
+msgid ""
+"\n"
+" Hello %(name)s:\n"
+"
\n"
+" Your account will expire in %(date_expired)s,\n"
+"
\n"
+" In order not to affect your normal work, please contact the "
+"administrator for confirmation.\n"
+"
\n"
+" "
+msgstr ""
+"\n"
+" 您好 %(name)s:\n"
+"
\n"
+" 您的账户会在 %(date_expired)s 过期,\n"
+"
\n"
+" 为了不影响您正常工作,请联系管理员确认。\n"
+"
\n"
+" "
+
+#: users/notifications.py:308 users/notifications.py:329
+msgid "SSH Key Reset"
+msgstr "重置SSH密钥"
+
+#: users/notifications.py:309
+msgid ""
+"\n"
+"Hello %(name)s:\n"
+"\n"
+"Your ssh public key has been reset by site administrator.\n"
+"Please login and reset your ssh public key.\n"
+"\n"
+"Login direct 👇\n"
+"%(login_url)s\n"
+"\n"
+" "
+msgstr ""
+"\n"
+"你好 %(name)s:\n"
+"\n"
+"您的密钥已被管理员重置,\n"
+"请登录并重新设置您的密钥.\n"
+"\n"
+"直接登录 👇\n"
+"%(login_url)s\n"
+"\n"
+" "
+
+#: users/notifications.py:330
+msgid ""
+"\n"
+" Hello %(name)s:\n"
+"
\n"
+" Your ssh public key has been reset by site administrator.\n"
+" Please login and reset your ssh public key.\n"
+"
\n"
+" Login direct\n"
+" \n"
+"
\n"
+" "
+msgstr ""
+"\n"
+" 你好 %(name)s:\n"
+"
\n"
+" 您的密钥已被管理员重置,\n"
+" 请登录并重新设置您的密钥.\n"
+"
\n"
+" 直接登录\n"
+"\n"
+"
\n"
+" "
+
+#: users/notifications.py:352 users/notifications.py:372
+msgid "MFA Reset"
+msgstr "重置 MFA"
+
+#: users/notifications.py:353
+msgid ""
+"\n"
+"Hello %(name)s:\n"
+"\n"
+"Your MFA has been reset by site administrator.\n"
+"Please login and reset your MFA.\n"
+"\n"
+"Login direct 👇 \n"
+"%(login_url)s\n"
+"\n"
+" "
+msgstr ""
+"\n"
+"你好 %(name)s:\n"
+"\n"
+"您的 MFA 已被管理员重置,\n"
+"请登录并重新设置您的 MFA.\n"
+"\n"
+"直接登录 👇 \n"
+"%(login_url)s\n"
+"\n"
+" "
+
+#: users/notifications.py:373
+msgid ""
+"\n"
+" Hello %(name)s:\n"
+"
\n"
+" Your MFA has been reset by site administrator.\n"
+" Please login and reset your MFA.\n"
+"
\n"
+" Login direct\n"
+" \n"
+"
\n"
+" "
+msgstr ""
+"\n"
+" 你好 %(name)s:\n"
+"
\n"
+" 您的 MFA 已被管理员重置,\n"
+" 请登录并重新设置您的 MFA.\n"
+"
\n"
+" 直接登录\n"
+"\n"
+"
\n"
+" "
+
#: users/serializers/profile.py:29
msgid "The old password is incorrect"
msgstr "旧密码错误"
@@ -4265,12 +5166,12 @@ msgstr "新密码不能是最近 {} 次的密码"
msgid "The newly set password is inconsistent"
msgstr "两次密码不一致"
-#: users/serializers/profile.py:120 users/serializers/user.py:75
+#: users/serializers/profile.py:121 users/serializers/user.py:75
msgid "Is first login"
msgstr "首次登录"
-#: users/serializers/user.py:22 xpack/plugins/change_auth_plan/models.py:65
-#: xpack/plugins/change_auth_plan/serializers.py:33
+#: users/serializers/user.py:22 xpack/plugins/change_auth_plan/models.py:61
+#: xpack/plugins/change_auth_plan/serializers.py:30
msgid "Password strategy"
msgstr "密码策略"
@@ -4424,11 +5325,6 @@ msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中"
msgid "Submit"
msgstr "提交"
-#: users/templates/users/reset_password.html:5
-#: users/templates/users/reset_password.html:6 users/utils.py:83
-msgid "Reset password"
-msgstr "重置密码"
-
#: users/templates/users/reset_password.html:23
#: users/templates/users/user_password_update.html:64
msgid "Your password must satisfy"
@@ -4542,9 +5438,13 @@ msgid ""
"operations according to the prompts"
msgstr "账号保护已开启,请根据提示完成以下操作"
+#: users/templates/users/user_verify_mfa.html:13
+msgid "Open MFA Authenticator and enter the 6-bit dynamic code"
+msgstr "请打开MFA验证器,输入6位动态码"
+
# msgid "Update user"
# msgstr "更新用户"
-#: users/utils.py:24
+#: users/utils.py:23
#, python-format
msgid ""
"\n"
@@ -4583,263 +5483,15 @@ msgstr ""
" \n"
" "
-#: users/utils.py:58
+#: users/utils.py:57
msgid "Create account successfully"
msgstr "创建账户成功"
-#: users/utils.py:62
+#: users/utils.py:61
#, python-format
msgid "Hello %(name)s"
msgstr "您好 %(name)s"
-#: users/utils.py:85
-#, python-format
-msgid ""
-"\n"
-" Hello %(name)s:\n"
-"
\n"
-" Please click the link below to reset your password, if not your request, "
-"concern your account security\n"
-"
\n"
-" Click "
-"here reset password\n"
-"
\n"
-" This link is valid for 1 hour. After it expires, request new one\n"
-"\n"
-"
\n"
-" ---\n"
-"\n"
-"
\n"
-" Login direct\n"
-"\n"
-"
\n"
-" "
-msgstr ""
-"\n"
-" 您好 %(name)s:\n"
-"
\n"
-" 请点击下面链接重置密码, 如果不是您申请的,请关注账号安全\n"
-"
\n"
-" 请点击这"
-"里设置密码 \n"
-"
\n"
-" 这个链接有效期1小时, 超过时间您可以重新申请\n"
-"\n"
-"
\n"
-" ---\n"
-"\n"
-"
\n"
-" 直接登录\n"
-"\n"
-"
\n"
-" "
-
-#: users/utils.py:116 users/views/profile/reset.py:125
-msgid "Reset password success"
-msgstr "重置密码成功"
-
-#: users/utils.py:118
-#, python-format
-msgid ""
-"\n"
-" \n"
-" Hi %(name)s:\n"
-"
\n"
-" \n"
-" \n"
-"
\n"
-" Your JumpServer password has just been successfully updated.\n"
-"
\n"
-" \n"
-"
\n"
-" If the password update was not initiated by you, your account may have "
-"security issues. \n"
-" It is recommended that you log on to the JumpServer immediately and "
-"change your password.\n"
-"
\n"
-" \n"
-"
\n"
-" If you have any questions, you can contact the administrator.\n"
-"
\n"
-"
\n"
-" ---\n"
-"
\n"
-"
\n"
-" IP Address: %(ip_address)s\n"
-"
\n"
-"
\n"
-" Browser: %(browser)s\n"
-"
\n"
-" \n"
-" "
-msgstr ""
-"\n"
-" \n"
-" Hi %(name)s:\n"
-"
\n"
-" \n"
-" \n"
-"
\n"
-" 你的 JumpServer 密码刚刚已经成功更新。
\n"
-" \n"
-"
\n"
-" 如果这次密码更新不是由你发起的,那么你的账号可能存在安全问题。\n"
-" 建议你立刻登录 JumpServer 更改密码。\n"
-"
\n"
-" \n"
-"
\n"
-" 如果你有任何疑问,可以联系管理员。
\n"
-"
\n"
-" ---\n"
-"
\n"
-"
\n"
-" IP 地址: %(ip_address)s\n"
-"
\n"
-"
\n"
-" 浏览器: %(browser)s\n"
-"
\n"
-" \n"
-" "
-
-#: users/utils.py:158
-msgid "Security notice"
-msgstr "安全通知"
-
-#: users/utils.py:160
-#, python-format
-msgid ""
-"\n"
-" Hello %(name)s:\n"
-"
\n"
-" Your password will expire in %(date_password_expired)s,\n"
-"
\n"
-" For your account security, please click on the link below to update your "
-"password in time\n"
-"
\n"
-" Click here update password\n"
-"
\n"
-" If your password has expired, please click \n"
-" Password expired"
-"a> \n"
-" to apply for a password reset email.\n"
-"\n"
-"
\n"
-" ---\n"
-"\n"
-"
\n"
-" Login direct\n"
-"\n"
-"
\n"
-" "
-msgstr ""
-"\n"
-" 您好 %(name)s:\n"
-"
\n"
-" 您的密码会在 %(date_password_expired)s 过期,\n"
-"
\n"
-" 为了您的账号安全,请点击下面的链接及时更新密码\n"
-"
\n"
-" 请点击这里更新密码\n"
-"
\n"
-" 如果您的密码已经过期,请点击 \n"
-" 密码过期 \n"
-" 申请一份重置密码邮件。\n"
-"\n"
-"
\n"
-" ---\n"
-"\n"
-"
\n"
-" 直接登录\n"
-"\n"
-"
\n"
-" "
-
-#: users/utils.py:196
-msgid "Expiration notice"
-msgstr "过期通知"
-
-#: users/utils.py:198
-#, python-format
-msgid ""
-"\n"
-" Hello %(name)s:\n"
-"
\n"
-" Your account will expire in %(date_expired)s,\n"
-"
\n"
-" In order not to affect your normal work, please contact the "
-"administrator for confirmation.\n"
-"
\n"
-" "
-msgstr ""
-"\n"
-" 您好 %(name)s:\n"
-"
\n"
-" 您的账户会在 %(date_expired)s 过期,\n"
-"
\n"
-" 为了不影响您正常工作,请联系管理员确认。\n"
-"
\n"
-" "
-
-#: users/utils.py:217
-msgid "SSH Key Reset"
-msgstr "重置SSH密钥"
-
-#: users/utils.py:219
-#, python-format
-msgid ""
-"\n"
-" Hello %(name)s:\n"
-"
\n"
-" Your ssh public key has been reset by site administrator.\n"
-" Please login and reset your ssh public key.\n"
-"
\n"
-" Login direct\n"
-"\n"
-"
\n"
-" "
-msgstr ""
-"\n"
-" 你好 %(name)s:\n"
-"
\n"
-" 您的密钥已被管理员重置,\n"
-" 请登录并重新设置您的密钥.\n"
-"
\n"
-" Login direct\n"
-"\n"
-"
\n"
-" "
-
-#: users/utils.py:239
-msgid "MFA Reset"
-msgstr "重置 MFA"
-
-#: users/utils.py:241
-#, python-format
-msgid ""
-"\n"
-" Hello %(name)s:\n"
-"
\n"
-" Your MFA has been reset by site administrator.\n"
-" Please login and reset your MFA.\n"
-"
\n"
-" Login direct\n"
-"\n"
-"
\n"
-" "
-msgstr ""
-"\n"
-" 你好 %(name)s:\n"
-"
\n"
-" 您的 MFA 已被管理员重置,\n"
-" 请登录并重新设置您的 MFA.\n"
-"
\n"
-" 登录\n"
-"\n"
-"
\n"
-" "
-
#: users/views/profile/otp.py:122 users/views/profile/otp.py:161
#: users/views/profile/otp.py:181
msgid "MFA code invalid, or ntp sync server time"
@@ -4861,7 +5513,7 @@ msgstr "多因子认证禁用成功"
msgid "MFA disable success, return login page"
msgstr "多因子认证禁用成功,返回登录页面"
-#: users/views/profile/password.py:30
+#: users/views/profile/password.py:32 users/views/profile/password.py:36
msgid "Password invalid"
msgstr "用户名或密码无效"
@@ -4888,163 +5540,110 @@ msgid ""
"password"
msgstr "用户来自 {} 请去相应系统修改密码"
-#: users/views/profile/reset.py:83 users/views/profile/reset.py:94
+#: users/views/profile/reset.py:84 users/views/profile/reset.py:95
msgid "Token invalid or expired"
msgstr "Token错误或失效"
-#: users/views/profile/reset.py:99
+#: users/views/profile/reset.py:100
msgid "User auth from {}, go there change password"
msgstr "用户认证源来自 {}, 请去相应系统修改密码"
-#: users/views/profile/reset.py:106
+#: users/views/profile/reset.py:107
msgid "* Your password does not meet the requirements"
msgstr "* 您的密码不符合要求"
-#: users/views/profile/reset.py:112
+#: users/views/profile/reset.py:113
msgid "* The new password cannot be the last {} passwords"
msgstr "* 新密码不能是最近 {} 次的密码"
-#: users/views/profile/reset.py:126
+#: users/views/profile/reset.py:128
msgid "Reset password success, return to login page"
msgstr "重置密码成功,返回到登录页面"
#: xpack/plugins/change_auth_plan/meta.py:9
-#: xpack/plugins/change_auth_plan/models.py:100
-#: xpack/plugins/change_auth_plan/models.py:201
+#: xpack/plugins/change_auth_plan/models.py:89
+#: xpack/plugins/change_auth_plan/models.py:184
msgid "Change auth plan"
msgstr "改密计划"
-#: xpack/plugins/change_auth_plan/models.py:40
+#: xpack/plugins/change_auth_plan/models.py:41
msgid "Custom password"
msgstr "自定义密码"
-#: xpack/plugins/change_auth_plan/models.py:41
-msgid "All assets use the same random password"
-msgstr "所有资产使用相同的随机密码"
-
#: xpack/plugins/change_auth_plan/models.py:42
+msgid "All assets use the same random password"
+msgstr "使用相同的随机密码"
+
+#: xpack/plugins/change_auth_plan/models.py:43
msgid "All assets use different random password"
-msgstr "所有资产使用不同的随机密码"
+msgstr "使用不同的随机密码"
-#: xpack/plugins/change_auth_plan/models.py:46
-msgid "Append SSH KEY"
-msgstr "追加新密钥"
-
-#: xpack/plugins/change_auth_plan/models.py:47
-msgid "Empty and append SSH KEY"
-msgstr "清空所有密钥再追加新密钥"
-
-#: xpack/plugins/change_auth_plan/models.py:48
-msgid "Empty current user and append SSH KEY"
-msgstr "清空当前账号密钥再追加新密钥"
-
-#: xpack/plugins/change_auth_plan/models.py:69
+#: xpack/plugins/change_auth_plan/models.py:65
msgid "Password rules"
msgstr "密码规则"
-#: xpack/plugins/change_auth_plan/models.py:189
-msgid "Manual trigger"
-msgstr "手动触发"
-
-#: xpack/plugins/change_auth_plan/models.py:190
-msgid "Timing trigger"
-msgstr "定时触发"
-
-#: xpack/plugins/change_auth_plan/models.py:204
+#: xpack/plugins/change_auth_plan/models.py:187
msgid "Change auth plan snapshot"
msgstr "改密计划快照"
-#: xpack/plugins/change_auth_plan/models.py:218
-#: xpack/plugins/change_auth_plan/serializers.py:166
-msgid "Trigger mode"
-msgstr "触发模式"
-
-#: xpack/plugins/change_auth_plan/models.py:223
-#: xpack/plugins/change_auth_plan/models.py:329
+#: xpack/plugins/change_auth_plan/models.py:202
+#: xpack/plugins/change_auth_plan/models.py:296
msgid "Change auth plan execution"
msgstr "改密计划执行"
-#: xpack/plugins/change_auth_plan/models.py:302
+#: xpack/plugins/change_auth_plan/models.py:269
msgid "Ready"
msgstr "准备"
-#: xpack/plugins/change_auth_plan/models.py:303
+#: xpack/plugins/change_auth_plan/models.py:270
msgid "Preflight check"
msgstr "改密前的校验"
-#: xpack/plugins/change_auth_plan/models.py:304
+#: xpack/plugins/change_auth_plan/models.py:271
msgid "Change auth"
msgstr "执行改密"
-#: xpack/plugins/change_auth_plan/models.py:305
+#: xpack/plugins/change_auth_plan/models.py:272
msgid "Verify auth"
msgstr "验证密码/密钥"
-#: xpack/plugins/change_auth_plan/models.py:306
+#: xpack/plugins/change_auth_plan/models.py:273
msgid "Keep auth"
msgstr "保存密码/密钥"
-#: xpack/plugins/change_auth_plan/models.py:307
-msgid "Finished"
-msgstr "结束"
-
-#: xpack/plugins/change_auth_plan/models.py:333
+#: xpack/plugins/change_auth_plan/models.py:300
msgid "Step"
msgstr "步骤"
-#: xpack/plugins/change_auth_plan/models.py:350
+#: xpack/plugins/change_auth_plan/models.py:317
msgid "Change auth plan task"
msgstr "改密计划任务"
-#: xpack/plugins/change_auth_plan/serializers.py:29
-msgid "Change Password"
-msgstr "修改密码"
-
-#: xpack/plugins/change_auth_plan/serializers.py:30
-msgid "Change SSH Key"
-msgstr "修改密钥"
-
-#: xpack/plugins/change_auth_plan/serializers.py:35
-msgid "SSH Key strategy"
-msgstr "SSH Key 策略"
-
-#: xpack/plugins/change_auth_plan/serializers.py:61
+#: xpack/plugins/change_auth_plan/serializers.py:56
msgid "Run times"
msgstr "执行次数"
-#: xpack/plugins/change_auth_plan/serializers.py:79
-msgid "Require password strategy perform setting"
-msgstr "需要密码策略执行设置"
+#: xpack/plugins/change_auth_plan/serializers.py:72
+msgid "* Please enter custom password"
+msgstr "* 请输入自定义密码"
#: xpack/plugins/change_auth_plan/serializers.py:82
-msgid "Require password perform setting"
-msgstr "需要密码执行设置"
-
-#: xpack/plugins/change_auth_plan/serializers.py:85
-msgid "Require password rule perform setting"
-msgstr "需要密码规则执行设置"
-
-#: xpack/plugins/change_auth_plan/serializers.py:97
msgid "* Please enter the correct password length"
msgstr "* 请输入正确的密码长度"
-#: xpack/plugins/change_auth_plan/serializers.py:100
+#: xpack/plugins/change_auth_plan/serializers.py:85
msgid "* Password length range 6-30 bits"
msgstr "* 密码长度范围 6-30 位"
-#: xpack/plugins/change_auth_plan/serializers.py:118
-msgid "Require ssh key strategy or ssh key perform setting"
-msgstr "需要ssh密钥策略或ssh密钥执行设置"
-
-#: xpack/plugins/change_auth_plan/utils.py:485
+#: xpack/plugins/change_auth_plan/utils.py:442
msgid "Invalid/incorrect password"
msgstr "无效/错误 密码"
-#: xpack/plugins/change_auth_plan/utils.py:487
+#: xpack/plugins/change_auth_plan/utils.py:444
msgid "Failed to connect to the host"
msgstr "连接主机失败"
-#: xpack/plugins/change_auth_plan/utils.py:489
+#: xpack/plugins/change_auth_plan/utils.py:446
msgid "Data could not be sent to remote"
msgstr "无法将数据发送到远程"
@@ -5140,10 +5739,6 @@ msgstr "云服务商"
msgid "Cloud account"
msgstr "云账号"
-#: xpack/plugins/cloud/models.py:82 xpack/plugins/cloud/serializers.py:207
-msgid "Account"
-msgstr "账户"
-
#: xpack/plugins/cloud/models.py:85 xpack/plugins/cloud/serializers.py:179
msgid "Regions"
msgstr "地域"
@@ -5152,43 +5747,35 @@ msgstr "地域"
msgid "Hostname strategy"
msgstr "主机名策略"
-#: xpack/plugins/cloud/models.py:97 xpack/plugins/cloud/serializers.py:208
-msgid "Unix admin user"
-msgstr "Unix 特权用户"
-
-#: xpack/plugins/cloud/models.py:101 xpack/plugins/cloud/serializers.py:209
-msgid "Windows admin user"
-msgstr "Windows 特权用户"
-
-#: xpack/plugins/cloud/models.py:107 xpack/plugins/cloud/serializers.py:187
+#: xpack/plugins/cloud/models.py:102 xpack/plugins/cloud/serializers.py:186
msgid "IP network segment group"
msgstr "IP网段组"
-#: xpack/plugins/cloud/models.py:110 xpack/plugins/cloud/serializers.py:212
+#: xpack/plugins/cloud/models.py:105 xpack/plugins/cloud/serializers.py:208
msgid "Always update"
msgstr "总是更新"
-#: xpack/plugins/cloud/models.py:116
+#: xpack/plugins/cloud/models.py:111
msgid "Date last sync"
msgstr "最后同步日期"
-#: xpack/plugins/cloud/models.py:127 xpack/plugins/cloud/models.py:168
+#: xpack/plugins/cloud/models.py:122 xpack/plugins/cloud/models.py:163
msgid "Sync instance task"
msgstr "同步实例任务"
-#: xpack/plugins/cloud/models.py:179 xpack/plugins/cloud/models.py:224
+#: xpack/plugins/cloud/models.py:174 xpack/plugins/cloud/models.py:219
msgid "Date sync"
msgstr "同步日期"
-#: xpack/plugins/cloud/models.py:204
+#: xpack/plugins/cloud/models.py:199
msgid "Sync task"
msgstr "同步任务"
-#: xpack/plugins/cloud/models.py:208
+#: xpack/plugins/cloud/models.py:203
msgid "Sync instance task history"
msgstr "同步实例任务历史"
-#: xpack/plugins/cloud/models.py:211
+#: xpack/plugins/cloud/models.py:206
msgid "Instance"
msgstr "实例"
@@ -5344,10 +5931,6 @@ msgstr ""
msgid "Client ID"
msgstr "客户端 ID"
-#: xpack/plugins/cloud/serializers.py:33
-msgid "Client Secret"
-msgstr "客户端密钥"
-
#: xpack/plugins/cloud/serializers.py:36
msgid "Tenant ID"
msgstr "租户 ID"
@@ -5356,6 +5939,10 @@ msgstr "租户 ID"
msgid "Subscription ID"
msgstr "订阅 ID"
+#: xpack/plugins/cloud/serializers.py:51
+msgid "This field is required"
+msgstr "该字段是必填项。"
+
#: xpack/plugins/cloud/serializers.py:85 xpack/plugins/cloud/serializers.py:89
msgid "API Endpoint"
msgstr "API 端点"
@@ -5377,11 +5964,6 @@ msgstr "执行次数"
msgid "Instance count"
msgstr "实例个数"
-#: xpack/plugins/cloud/serializers.py:211
-#: xpack/plugins/gathered_user/serializers.py:20
-msgid "Periodic display"
-msgstr "定时执行"
-
#: xpack/plugins/cloud/utils.py:65
msgid "Account unavailable"
msgstr "账户无效"
@@ -5469,48 +6051,3 @@ msgstr "旗舰版"
#: xpack/plugins/license/models.py:77
msgid "Community edition"
msgstr "社区版"
-
-#~ msgid "* Please enter custom password"
-#~ msgstr "* 请输入自定义密码"
-
-#~ msgid "FeiShu Error, Please contact your system administrator"
-#~ msgstr "飞书错误,请联系系统管理员"
-
-#~ msgid "Category(Display)"
-#~ msgstr "类别 (显示名称)"
-
-#~ msgid "Type(Dispaly)"
-#~ msgstr "类型 (显示名称)"
-
-#~ msgid "Users name"
-#~ msgstr "用户名"
-
-#~ msgid "User groups name"
-#~ msgstr "用户组名称"
-
-#~ msgid "Assets name"
-#~ msgstr "资产名称"
-
-#~ msgid "System users name"
-#~ msgstr "系统用户名称"
-
-#~ msgid "Admin user MFA auth"
-#~ msgstr "所有管理员启用 MFA"
-
-#~ msgid "Admin user enable MFA"
-#~ msgstr "强制管理员启用 MFA"
-
-#~ msgid "Password update"
-#~ msgstr "密码更新"
-
-#~ msgid "All user enable MFA"
-#~ msgstr "强制所有用户启用 MFA"
-
-#~ msgid "Application category"
-#~ msgstr "应用类别"
-
-#~ msgid "Application type"
-#~ msgstr "应用类型"
-
-#~ msgid "Trigger"
-#~ msgstr "触发"
diff --git a/apps/notifications/api/notifications.py b/apps/notifications/api/notifications.py
index 5c726e201..3b58d3edc 100644
--- a/apps/notifications/api/notifications.py
+++ b/apps/notifications/api/notifications.py
@@ -1,18 +1,18 @@
-from django.http import Http404
-from rest_framework.mixins import ListModelMixin, UpdateModelMixin
+from rest_framework.mixins import ListModelMixin, UpdateModelMixin, RetrieveModelMixin
from rest_framework.views import APIView
from rest_framework.response import Response
-from rest_framework import status
from common.drf.api import JMSGenericViewSet
+from common.permissions import IsObjectOwner, IsSuperUser, OnlySuperUserCanList
from notifications.notifications import system_msgs
-from notifications.models import SystemMsgSubscription
+from notifications.models import SystemMsgSubscription, UserMsgSubscription
from notifications.backends import BACKEND
from notifications.serializers import (
- SystemMsgSubscriptionSerializer, SystemMsgSubscriptionByCategorySerializer
+ SystemMsgSubscriptionSerializer, SystemMsgSubscriptionByCategorySerializer,
+ UserMsgSubscriptionSerializer,
)
-__all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet')
+__all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet', 'UserMsgSubscriptionViewSet')
class BackendListView(APIView):
@@ -70,3 +70,13 @@ class SystemMsgSubscriptionViewSet(ListModelMixin,
serializer = self.get_serializer(data, many=True)
return Response(data=serializer.data)
+
+
+class UserMsgSubscriptionViewSet(ListModelMixin,
+ RetrieveModelMixin,
+ UpdateModelMixin,
+ JMSGenericViewSet):
+ lookup_field = 'user_id'
+ queryset = UserMsgSubscription.objects.all()
+ serializer_class = UserMsgSubscriptionSerializer
+ permission_classes = (IsObjectOwner | IsSuperUser, OnlySuperUserCanList)
diff --git a/apps/notifications/backends/__init__.py b/apps/notifications/backends/__init__.py
index 11a95cf40..a991b9567 100644
--- a/apps/notifications/backends/__init__.py
+++ b/apps/notifications/backends/__init__.py
@@ -1,11 +1,9 @@
+import importlib
+
from django.utils.translation import gettext_lazy as _
from django.db import models
-from .dingtalk import DingTalk
-from .email import Email
-from .site_msg import SiteMessage
-from .wecom import WeCom
-from .feishu import FeiShu
+client_name_mapper = {}
class BACKEND(models.TextChoices):
@@ -14,17 +12,11 @@ class BACKEND(models.TextChoices):
DINGTALK = 'dingtalk', _('DingTalk')
SITE_MSG = 'site_msg', _('Site message')
FEISHU = 'feishu', _('FeiShu')
+ SMS = 'sms', _('SMS')
@property
def client(self):
- client = {
- self.EMAIL: Email,
- self.WECOM: WeCom,
- self.DINGTALK: DingTalk,
- self.SITE_MSG: SiteMessage,
- self.FEISHU: FeiShu,
- }[self]
- return client
+ return client_name_mapper[self]
def get_account(self, user):
return self.client.get_account(user)
@@ -37,3 +29,8 @@ class BACKEND(models.TextChoices):
def filter_enable_backends(cls, backends):
enable_backends = [b for b in backends if cls(b).is_enable]
return enable_backends
+
+
+for b in BACKEND:
+ m = importlib.import_module(f'.{b}', __package__)
+ client_name_mapper[b] = m.backend
diff --git a/apps/notifications/backends/dingtalk.py b/apps/notifications/backends/dingtalk.py
index 83add673e..ba72091a4 100644
--- a/apps/notifications/backends/dingtalk.py
+++ b/apps/notifications/backends/dingtalk.py
@@ -14,6 +14,9 @@ class DingTalk(BackendBase):
agentid=settings.DINGTALK_AGENTID
)
- def send_msg(self, users, msg):
+ def send_msg(self, users, message, subject=None):
accounts, __, __ = self.get_accounts(users)
- return self.dingtalk.send_text(accounts, msg)
+ return self.dingtalk.send_text(accounts, message)
+
+
+backend = DingTalk
diff --git a/apps/notifications/backends/email.py b/apps/notifications/backends/email.py
index 4e1c27322..390da151a 100644
--- a/apps/notifications/backends/email.py
+++ b/apps/notifications/backends/email.py
@@ -8,7 +8,10 @@ class Email(BackendBase):
account_field = 'email'
is_enable_field_in_settings = 'EMAIL_HOST_USER'
- def send_msg(self, users, subject, message):
+ def send_msg(self, users, message, subject):
from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
accounts, __, __ = self.get_accounts(users)
send_mail(subject, message, from_email, accounts, html_message=message)
+
+
+backend = Email
diff --git a/apps/notifications/backends/feishu.py b/apps/notifications/backends/feishu.py
index 90547299c..434898c8a 100644
--- a/apps/notifications/backends/feishu.py
+++ b/apps/notifications/backends/feishu.py
@@ -14,6 +14,9 @@ class FeiShu(BackendBase):
app_secret=settings.FEISHU_APP_SECRET
)
- def send_msg(self, users, msg):
+ def send_msg(self, users, message, subject=None):
accounts, __, __ = self.get_accounts(users)
- return self.client.send_text(accounts, msg)
+ return self.client.send_text(accounts, message)
+
+
+backend = FeiShu
diff --git a/apps/notifications/backends/site_msg.py b/apps/notifications/backends/site_msg.py
index 0f7468f48..faf539d17 100644
--- a/apps/notifications/backends/site_msg.py
+++ b/apps/notifications/backends/site_msg.py
@@ -5,10 +5,13 @@ from .base import BackendBase
class SiteMessage(BackendBase):
account_field = 'id'
- def send_msg(self, users, subject, message):
+ def send_msg(self, users, message, subject):
accounts, __, __ = self.get_accounts(users)
Client.send_msg(subject, message, user_ids=accounts)
@classmethod
def is_enable(cls):
return True
+
+
+backend = SiteMessage
diff --git a/apps/notifications/backends/sms.py b/apps/notifications/backends/sms.py
new file mode 100644
index 000000000..77a720a04
--- /dev/null
+++ b/apps/notifications/backends/sms.py
@@ -0,0 +1,25 @@
+from django.conf import settings
+
+from common.message.backends.sms.alibaba import AlibabaSMS as Client
+from .base import BackendBase
+
+
+class SMS(BackendBase):
+ account_field = 'phone'
+ is_enable_field_in_settings = 'SMS_ENABLED'
+
+ def __init__(self):
+ """
+ 暂时只对接阿里,之后再扩展
+ """
+ self.client = Client(
+ access_key_id=settings.ALIBABA_ACCESS_KEY_ID,
+ access_key_secret=settings.ALIBABA_ACCESS_KEY_SECRET
+ )
+
+ def send_msg(self, users, sign_name: str, template_code: str, template_param: dict):
+ accounts, __, __ = self.get_accounts(users)
+ return self.client.send_sms(accounts, sign_name, template_code, template_param)
+
+
+backend = SMS
diff --git a/apps/notifications/backends/wecom.py b/apps/notifications/backends/wecom.py
index 80b6f1a22..988c904c2 100644
--- a/apps/notifications/backends/wecom.py
+++ b/apps/notifications/backends/wecom.py
@@ -15,6 +15,9 @@ class WeCom(BackendBase):
agentid=settings.WECOM_AGENTID
)
- def send_msg(self, users, msg):
+ def send_msg(self, users, message, subject=None):
accounts, __, __ = self.get_accounts(users)
- return self.wecom.send_text(accounts, msg)
+ return self.wecom.send_text(accounts, message)
+
+
+backend = WeCom
diff --git a/apps/notifications/migrations/0001_initial.py b/apps/notifications/migrations/0001_initial.py
index ebe79f304..8b52fb84f 100644
--- a/apps/notifications/migrations/0001_initial.py
+++ b/apps/notifications/migrations/0001_initial.py
@@ -44,7 +44,7 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('message_type', models.CharField(max_length=128)),
('receive_backends', models.JSONField(default=list)),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscriptions', to=settings.AUTH_USER_MODEL)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscription', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
diff --git a/apps/notifications/migrations/0002_auto_20210909_1946.py b/apps/notifications/migrations/0002_auto_20210909_1946.py
new file mode 100644
index 000000000..75f71fad7
--- /dev/null
+++ b/apps/notifications/migrations/0002_auto_20210909_1946.py
@@ -0,0 +1,55 @@
+# Generated by Django 3.1.12 on 2021-09-09 11:46
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+def init_user_msg_subscription(apps, schema_editor):
+ UserMsgSubscription = apps.get_model('notifications', 'UserMsgSubscription')
+ User = apps.get_model('users', 'User')
+
+ to_create = []
+ users = User.objects.all()
+ for user in users:
+ receive_backends = []
+
+ receive_backends.append('site_msg')
+
+ if user.email:
+ receive_backends.append('email')
+
+ if user.wecom_id:
+ receive_backends.append('wecom')
+
+ if user.dingtalk_id:
+ receive_backends.append('dingtalk')
+
+ if user.feishu_id:
+ receive_backends.append('feishu')
+
+ to_create.append(UserMsgSubscription(user=user, receive_backends=receive_backends))
+ UserMsgSubscription.objects.bulk_create(to_create)
+ print(f'\n Init user message subscription: {len(to_create)}')
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('notifications', '0001_initial'),
+ ('users', '0036_user_feishu_id'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='usermsgsubscription',
+ name='message_type',
+ ),
+ migrations.AlterField(
+ model_name='usermsgsubscription',
+ name='user',
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscription', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.RunPython(init_user_msg_subscription)
+ ]
diff --git a/apps/notifications/models/notification.py b/apps/notifications/models/notification.py
index 94bd1ad7d..d50168576 100644
--- a/apps/notifications/models/notification.py
+++ b/apps/notifications/models/notification.py
@@ -6,12 +6,11 @@ __all__ = ('SystemMsgSubscription', 'UserMsgSubscription')
class UserMsgSubscription(JMSModel):
- message_type = models.CharField(max_length=128)
- user = models.ForeignKey('users.User', related_name='user_msg_subscriptions', on_delete=models.CASCADE)
+ user = models.OneToOneField('users.User', related_name='user_msg_subscription', on_delete=models.CASCADE)
receive_backends = models.JSONField(default=list)
def __str__(self):
- return f'{self.message_type}'
+ return f'{self.user} subscription: {self.receive_backends}'
class SystemMsgSubscription(JMSModel):
diff --git a/apps/notifications/notifications.py b/apps/notifications/notifications.py
index cac467734..1141c104f 100644
--- a/apps/notifications/notifications.py
+++ b/apps/notifications/notifications.py
@@ -1,14 +1,16 @@
from typing import Iterable
import traceback
from itertools import chain
+from collections import defaultdict
-from django.db.utils import ProgrammingError
from celery import shared_task
+from common.utils import lazyproperty
+from users.models import User
from notifications.backends import BACKEND
-from .models import SystemMsgSubscription
+from .models import SystemMsgSubscription, UserMsgSubscription
-__all__ = ('SystemMessage', 'UserMessage')
+__all__ = ('SystemMessage', 'UserMessage', 'system_msgs')
system_msgs = []
@@ -66,40 +68,55 @@ class Message(metaclass=MessageType):
raise NotImplementedError
def send_msg(self, users: Iterable, backends: Iterable = BACKEND):
+ backends = set(backends)
+ backends.add(BACKEND.SITE_MSG) # 站内信必须发
+
for backend in backends:
try:
backend = BACKEND(backend)
+ if not backend.is_enable:
+ continue
get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg)
- msg = get_msg_method()
+
+ try:
+ msg = get_msg_method()
+ except NotImplementedError:
+ continue
+
client = backend.client()
- if isinstance(msg, dict):
- client.send_msg(users, **msg)
- else:
- client.send_msg(users, msg)
+ client.send_msg(users, **msg)
except:
traceback.print_exc()
- def get_common_msg(self) -> str:
+ def get_common_msg(self) -> dict:
raise NotImplementedError
- def get_dingtalk_msg(self) -> str:
+ @lazyproperty
+ def common_msg(self) -> dict:
return self.get_common_msg()
- def get_wecom_msg(self) -> str:
- return self.get_common_msg()
+ # --------------------------------------------------------------
+ # 支持不同发送消息的方式定义自己的消息内容,比如有些支持 html 标签
+ def get_dingtalk_msg(self) -> dict:
+ return self.common_msg
+
+ def get_wecom_msg(self) -> dict:
+ return self.common_msg
+
+ def get_feishu_msg(self) -> dict:
+ return self.common_msg
def get_email_msg(self) -> dict:
- msg = self.get_common_msg()
- subject = f'{msg[:80]} ...' if len(msg) >= 80 else msg
- return {
- 'subject': subject,
- 'message': msg
- }
+ return self.common_msg
def get_site_msg_msg(self) -> dict:
- return self.get_email_msg()
+ return self.common_msg
+
+ def get_sms_msg(self) -> dict:
+ raise NotImplementedError
+ # --------------------------------------------------------------
class SystemMessage(Message):
@@ -125,4 +142,16 @@ class SystemMessage(Message):
class UserMessage(Message):
- pass
+ user: User
+
+ def __init__(self, user):
+ self.user = user
+
+ def publish(self):
+ """
+ 发送消息到每个用户配置的接收方式上
+ """
+
+ sub = UserMsgSubscription.objects.get(user=self.user)
+
+ self.send_msg([self.user], sub.receive_backends)
diff --git a/apps/notifications/serializers/notifications.py b/apps/notifications/serializers/notifications.py
index 7415d46f7..191d90a77 100644
--- a/apps/notifications/serializers/notifications.py
+++ b/apps/notifications/serializers/notifications.py
@@ -1,7 +1,7 @@
from rest_framework import serializers
from common.drf.serializers import BulkModelSerializer
-from notifications.models import SystemMsgSubscription
+from notifications.models import SystemMsgSubscription, UserMsgSubscription
class SystemMsgSubscriptionSerializer(BulkModelSerializer):
@@ -27,3 +27,11 @@ class SystemMsgSubscriptionByCategorySerializer(serializers.Serializer):
category = serializers.CharField()
category_label = serializers.CharField()
children = SystemMsgSubscriptionSerializer(many=True)
+
+
+class UserMsgSubscriptionSerializer(BulkModelSerializer):
+ receive_backends = serializers.ListField(child=serializers.CharField(), read_only=False)
+
+ class Meta:
+ model = UserMsgSubscription
+ fields = ('user_id', 'receive_backends',)
diff --git a/apps/notifications/signals_handler.py b/apps/notifications/signals_handler.py
index 451377557..019d2a3da 100644
--- a/apps/notifications/signals_handler.py
+++ b/apps/notifications/signals_handler.py
@@ -6,14 +6,14 @@ from django.utils.functional import LazyObject
from django.db.models.signals import post_save
from django.db.models.signals import post_migrate
from django.dispatch import receiver
-from django.db.utils import DEFAULT_DB_ALIAS
-from django.apps import apps as global_apps
from django.apps import AppConfig
+from notifications.backends import BACKEND
+from users.models import User
from common.utils.connection import RedisPubSub
from common.utils import get_logger
from common.decorator import on_transaction_commit
-from .models import SiteMessage, SystemMsgSubscription
+from .models import SiteMessage, SystemMsgSubscription, UserMsgSubscription
from .notifications import SystemMessage
@@ -82,3 +82,13 @@ def create_system_messages(app_config: AppConfig, **kwargs):
logger.info(f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}')
except ModuleNotFoundError:
pass
+
+
+@receiver(post_save, sender=User)
+def on_user_post_save(sender, instance, created, **kwargs):
+ if created:
+ receive_backends = []
+ for backend in BACKEND:
+ if backend.get_account(instance):
+ receive_backends.append(backend)
+ UserMsgSubscription.objects.create(user=instance, receive_backends=receive_backends)
diff --git a/apps/notifications/site_msg.py b/apps/notifications/site_msg.py
index 1a5c9dc23..6e3f45f9d 100644
--- a/apps/notifications/site_msg.py
+++ b/apps/notifications/site_msg.py
@@ -2,9 +2,12 @@ from django.db.models import F
from django.db import transaction
from common.utils.timezone import now
+from common.utils import get_logger
from users.models import User
from .models import SiteMessage as SiteMessageModel, SiteMessageUsers
+logger = get_logger(__file__)
+
class SiteMessageUtil:
@@ -14,6 +17,11 @@ class SiteMessageUtil:
if not any((user_ids, group_ids, is_broadcast)):
raise ValueError('No recipient is specified')
+ logger.info(f'Site message send: '
+ f'user_ids={user_ids} '
+ f'group_ids={group_ids} '
+ f'subject={subject} '
+ f'message={message}')
with transaction.atomic():
site_msg = SiteMessageModel.objects.create(
subject=subject, message=message,
diff --git a/apps/notifications/urls/api_urls.py b/apps/notifications/urls/api_urls.py
index 60aaee873..14ed78e52 100644
--- a/apps/notifications/urls/api_urls.py
+++ b/apps/notifications/urls/api_urls.py
@@ -8,6 +8,7 @@ app_name = 'notifications'
router = BulkRouter()
router.register('system-msg-subscription', api.SystemMsgSubscriptionViewSet, 'system-msg-subscription')
+router.register('user-msg-subscription', api.UserMsgSubscriptionViewSet, 'user-msg-subscription')
router.register('site-message', api.SiteMessageViewSet, 'site-message')
urlpatterns = [
diff --git a/apps/notifications/ws.py b/apps/notifications/ws.py
index 45cbb6d00..4b9d9e4bd 100644
--- a/apps/notifications/ws.py
+++ b/apps/notifications/ws.py
@@ -4,7 +4,6 @@ from redis.exceptions import ConnectionError
from channels.generic.websocket import JsonWebsocketConsumer
from common.utils import get_logger
-from .models import SiteMessage
from .site_msg import SiteMessageUtil
from .signals_handler import new_site_msg_chan
diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py
index 707855cc3..df20b06d6 100644
--- a/apps/ops/ansible/inventory.py
+++ b/apps/ops/ansible/inventory.py
@@ -46,7 +46,7 @@ class BaseHost(Host):
if host_data.get('username'):
self.set_variable('ansible_user', host_data['username'])
- # 添加密码和秘钥
+ # 添加密码和密钥
if host_data.get('password'):
self.set_variable('ansible_ssh_pass', host_data['password'])
if host_data.get('private_key'):
diff --git a/apps/ops/mixin.py b/apps/ops/mixin.py
index c6f1ce184..e64a763fc 100644
--- a/apps/ops/mixin.py
+++ b/apps/ops/mixin.py
@@ -159,7 +159,7 @@ class PeriodTaskFormMixin(forms.Form):
)
interval = forms.IntegerField(
required=False, initial=24,
- help_text=_('Tips: (Units: hour)'), label=_("Cycle perform"),
+ help_text=_('Unit: hour'), label=_("Cycle perform"),
)
def get_initial_for_field(self, field, field_name):
diff --git a/apps/ops/notifications.py b/apps/ops/notifications.py
index d39805b8e..a8c659108 100644
--- a/apps/ops/notifications.py
+++ b/apps/ops/notifications.py
@@ -18,7 +18,10 @@ class ServerPerformanceMessage(SystemMessage):
self._msg = msg
def get_common_msg(self):
- return self._msg
+ return {
+ 'subject': self._msg[:80],
+ 'message': self._msg
+ }
@classmethod
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py
index 498d90a5a..8176cb439 100644
--- a/apps/orgs/mixins/models.py
+++ b/apps/orgs/mixins/models.py
@@ -19,6 +19,7 @@ __all__ = [
class OrgManager(models.Manager):
+
def all_group_by_org(self):
from ..models import Organization
orgs = list(Organization.objects.all())
diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py
index 28b59a705..24ef3e8be 100644
--- a/apps/orgs/serializers.py
+++ b/apps/orgs/serializers.py
@@ -25,7 +25,7 @@ class ResourceStatisticsSerializer(serializers.Serializer):
app_perms_amount = serializers.IntegerField(required=False)
-class OrgSerializer(ModelSerializer):
+class OrgSerializer(BulkModelSerializer):
users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False)
admins = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False)
auditors = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False)
diff --git a/apps/perms/api/application/user_group_permission.py b/apps/perms/api/application/user_group_permission.py
index 32d116b2f..34c3eefaa 100644
--- a/apps/perms/api/application/user_group_permission.py
+++ b/apps/perms/api/application/user_group_permission.py
@@ -19,8 +19,8 @@ class UserGroupGrantedApplicationsApi(CommonApiMixin, ListAPIView):
获取用户组直接授权的应用
"""
permission_classes = (IsOrgAdminOrAppUser,)
- serializer_class = serializers.ApplicationGrantedSerializer
- only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields
+ serializer_class = serializers.AppGrantedSerializer
+ only_fields = serializers.AppGrantedSerializer.Meta.only_fields
filterset_fields = ['id', 'name', 'category', 'type', 'comment']
search_fields = ['name', 'comment']
diff --git a/apps/perms/api/application/user_permission/user_permission_applications.py b/apps/perms/api/application/user_permission/user_permission_applications.py
index a88217e27..be5fc6745 100644
--- a/apps/perms/api/application/user_permission/user_permission_applications.py
+++ b/apps/perms/api/application/user_permission/user_permission_applications.py
@@ -24,8 +24,8 @@ __all__ = [
class AllGrantedApplicationsMixin(CommonApiMixin, ListAPIView):
- only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields
- serializer_class = serializers.ApplicationGrantedSerializer
+ only_fields = serializers.AppGrantedSerializer.Meta.only_fields
+ serializer_class = serializers.AppGrantedSerializer
filterset_fields = {
'id': ['exact'],
'name': ['exact'],
diff --git a/apps/perms/const.py b/apps/perms/const.py
new file mode 100644
index 000000000..5b8e1baca
--- /dev/null
+++ b/apps/perms/const.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+#
+from django.db.models import TextChoices
+from django.utils.translation import ugettext_lazy as _
diff --git a/apps/perms/migrations/0019_auto_20210906_1044.py b/apps/perms/migrations/0019_auto_20210906_1044.py
new file mode 100644
index 000000000..42e480669
--- /dev/null
+++ b/apps/perms/migrations/0019_auto_20210906_1044.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.12 on 2021-09-06 02:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('perms', '0018_auto_20210208_1515'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='applicationpermission',
+ name='from_ticket',
+ field=models.BooleanField(default=False, verbose_name='From ticket'),
+ ),
+ migrations.AddField(
+ model_name='assetpermission',
+ name='from_ticket',
+ field=models.BooleanField(default=False, verbose_name='From ticket'),
+ ),
+ ]
diff --git a/apps/perms/models/base.py b/apps/perms/models/base.py
index 05f780e8f..2e62a84bf 100644
--- a/apps/perms/models/base.py
+++ b/apps/perms/models/base.py
@@ -12,7 +12,6 @@ from common.db.models import UnionQuerySet
from common.utils import date_expired_default, lazyproperty
from orgs.mixins.models import OrgManager
-
__all__ = [
'BasePermission', 'BasePermissionQuerySet'
]
@@ -31,11 +30,7 @@ class BasePermissionQuerySet(models.QuerySet):
def invalid(self):
now = timezone.now()
- q = (
- Q(is_active=False) |
- Q(date_start__gt=now) |
- Q(date_expired__lt=now)
- )
+ q = (Q(is_active=False) | Q(date_start__gt=now) | Q(date_expired__lt=now))
return self.filter(q)
@@ -48,13 +43,15 @@ class BasePermission(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("User"), related_name='%(class)ss')
- user_groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User group"), related_name='%(class)ss')
+ user_groups = models.ManyToManyField(
+ 'users.UserGroup', blank=True, verbose_name=_("User group"), related_name='%(class)ss')
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start"))
date_expired = models.DateTimeField(default=date_expired_default, db_index=True, verbose_name=_('Date expired'))
created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
comment = models.TextField(verbose_name=_('Comment'), blank=True)
+ from_ticket = models.BooleanField(default=False, verbose_name=_('From ticket'))
objects = BasePermissionManager.from_queryset(BasePermissionQuerySet)()
diff --git a/apps/perms/serializers/application/permission.py b/apps/perms/serializers/application/permission.py
index ecf6ae3f4..e636eec8c 100644
--- a/apps/perms/serializers/application/permission.py
+++ b/apps/perms/serializers/application/permission.py
@@ -24,7 +24,7 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer):
fields_small = fields_mini + [
'category', 'category_display', 'type', 'type_display',
'is_active', 'is_expired', 'is_valid',
- 'created_by', 'date_created', 'date_expired', 'date_start', 'comment'
+ 'created_by', 'date_created', 'date_expired', 'date_start', 'comment', 'from_ticket'
]
fields_m2m = [
'users', 'user_groups', 'applications', 'system_users',
@@ -32,7 +32,7 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer):
'system_users_amount',
]
fields = fields_small + fields_m2m
- read_only_fields = ['created_by', 'date_created']
+ read_only_fields = ['created_by', 'date_created', 'from_ticket']
extra_kwargs = {
'is_expired': {'label': _('Is expired')},
'is_valid': {'label': _('Is valid')},
diff --git a/apps/perms/serializers/application/user_permission.py b/apps/perms/serializers/application/user_permission.py
index 5c722852b..63b681d5a 100644
--- a/apps/perms/serializers/application/user_permission.py
+++ b/apps/perms/serializers/application/user_permission.py
@@ -6,10 +6,10 @@ from django.utils.translation import ugettext_lazy as _
from assets.models import SystemUser
from applications.models import Application
-from applications.serializers import ApplicationSerializerMixin
+from applications.serializers import AppSerializerMixin
__all__ = [
- 'ApplicationGrantedSerializer', 'ApplicationSystemUserSerializer'
+ 'AppGrantedSerializer', 'ApplicationSystemUserSerializer'
]
@@ -26,7 +26,7 @@ class ApplicationSystemUserSerializer(serializers.ModelSerializer):
read_only_fields = fields
-class ApplicationGrantedSerializer(ApplicationSerializerMixin, serializers.ModelSerializer):
+class AppGrantedSerializer(AppSerializerMixin, serializers.ModelSerializer):
"""
被授权应用的数据结构
"""
diff --git a/apps/perms/serializers/asset/permission.py b/apps/perms/serializers/asset/permission.py
index 824a25292..3df2c1173 100644
--- a/apps/perms/serializers/asset/permission.py
+++ b/apps/perms/serializers/asset/permission.py
@@ -53,7 +53,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
fields_small = fields_mini + [
'is_active', 'is_expired', 'is_valid', 'actions',
'created_by', 'date_created', 'date_expired',
- 'date_start', 'comment'
+ 'date_start', 'comment', 'from_ticket'
]
fields_m2m = [
'users', 'users_display', 'user_groups', 'user_groups_display', 'assets',
@@ -62,7 +62,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
'nodes_amount', 'system_users_amount',
]
fields = fields_small + fields_m2m
- read_only_fields = ['created_by', 'date_created']
+ read_only_fields = ['created_by', 'date_created', 'from_ticket']
extra_kwargs = {
'is_expired': {'label': _('Is expired')},
'is_valid': {'label': _('Is valid')},
diff --git a/apps/perms/signals_handler/__init__.py b/apps/perms/signals_handler/__init__.py
index e0b84afea..68a531887 100644
--- a/apps/perms/signals_handler/__init__.py
+++ b/apps/perms/signals_handler/__init__.py
@@ -1,2 +1,3 @@
-from . import common
+from . import asset_permission
+from . import app_permission
from . import refresh_perms
diff --git a/apps/perms/signals_handler/app_permission.py b/apps/perms/signals_handler/app_permission.py
new file mode 100644
index 000000000..84ee58807
--- /dev/null
+++ b/apps/perms/signals_handler/app_permission.py
@@ -0,0 +1,104 @@
+import itertools
+
+from django.db.models.signals import m2m_changed
+from django.dispatch import receiver
+
+from users.models import User, UserGroup
+from assets.models import SystemUser
+from applications.models import Application
+from common.utils import get_logger
+from common.exceptions import M2MReverseNotAllowed
+from common.decorator import on_transaction_commit
+from common.const.signals import POST_ADD
+from perms.models import ApplicationPermission
+from applications.models import Account as AppAccount
+
+
+logger = get_logger(__file__)
+
+
+@receiver(m2m_changed, sender=ApplicationPermission.applications.through)
+@on_transaction_commit
+def on_app_permission_applications_changed(sender, instance, action, reverse, pk_set, **kwargs):
+ if reverse:
+ raise M2MReverseNotAllowed
+ if action != POST_ADD:
+ return
+
+ logger.debug("Application permission applications change signal received")
+ system_users = instance.system_users.all()
+ set_remote_app_asset_system_users_if_need(instance, system_users=system_users)
+
+ apps = Application.objects.filter(pk__in=pk_set)
+ set_app_accounts(apps, system_users)
+
+
+def set_app_accounts(apps, system_users):
+ for app, system_user in itertools.product(apps, system_users):
+ AppAccount.objects.get_or_create(
+ defaults={'app': app, 'systemuser': system_user},
+ app=app, systemuser=system_user
+ )
+
+
+def set_remote_app_asset_system_users_if_need(instance: ApplicationPermission, system_users=None,
+ users=None, groups=None):
+ if not instance.category_remote_app:
+ return
+
+ attrs = instance.applications.all().values_list('attrs', flat=True)
+ asset_ids = [attr['asset'] for attr in attrs if attr.get('asset')]
+ if not asset_ids:
+ return
+
+ system_users = system_users or instance.system_users.all()
+ for system_user in system_users:
+ system_user.assets.add(*asset_ids)
+
+ if system_user.username_same_with_user:
+ users = users or instance.users.all()
+ groups = groups or instance.user_groups.all()
+ system_user.groups.add(*groups)
+ system_user.users.add(*users)
+
+
+@receiver(m2m_changed, sender=ApplicationPermission.system_users.through)
+@on_transaction_commit
+def on_app_permission_system_users_changed(sender, instance, action, reverse, pk_set, **kwargs):
+ if reverse:
+ raise M2MReverseNotAllowed
+ if action != POST_ADD:
+ return
+
+ logger.debug("Application permission system_users change signal received")
+ system_users = SystemUser.objects.filter(pk__in=pk_set)
+
+ set_remote_app_asset_system_users_if_need(instance, system_users=system_users)
+ apps = instance.applications.all()
+ set_app_accounts(apps, system_users)
+
+
+@receiver(m2m_changed, sender=ApplicationPermission.users.through)
+@on_transaction_commit
+def on_app_permission_users_changed(sender, instance, action, reverse, pk_set, **kwargs):
+ if reverse:
+ raise M2MReverseNotAllowed
+ if action != POST_ADD:
+ return
+
+ logger.debug("Application permission users change signal received")
+ users = User.objects.filter(pk__in=pk_set)
+ set_remote_app_asset_system_users_if_need(instance, users=users)
+
+
+@receiver(m2m_changed, sender=ApplicationPermission.user_groups.through)
+@on_transaction_commit
+def on_app_permission_user_groups_changed(sender, instance, action, reverse, pk_set, **kwargs):
+ if reverse:
+ raise M2MReverseNotAllowed
+ if action != POST_ADD:
+ return
+
+ logger.debug("Application permission user groups change signal received")
+ groups = UserGroup.objects.filter(pk__in=pk_set)
+ set_remote_app_asset_system_users_if_need(instance, groups=groups)
diff --git a/apps/perms/signals_handler/common.py b/apps/perms/signals_handler/asset_permission.py
similarity index 54%
rename from apps/perms/signals_handler/common.py
rename to apps/perms/signals_handler/asset_permission.py
index 7399346db..0b2c1aeee 100644
--- a/apps/perms/signals_handler/common.py
+++ b/apps/perms/signals_handler/asset_permission.py
@@ -3,19 +3,20 @@
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
-from users.models import User, UserGroup
+from users.models import User
from assets.models import SystemUser
-from applications.models import Application
from common.utils import get_logger
+from common.decorator import on_transaction_commit
from common.exceptions import M2MReverseNotAllowed
from common.const.signals import POST_ADD
-from perms.models import AssetPermission, ApplicationPermission
+from perms.models import AssetPermission
logger = get_logger(__file__)
@receiver(m2m_changed, sender=User.groups.through)
+@on_transaction_commit
def on_user_groups_change(sender, instance, action, reverse, pk_set, **kwargs):
"""
UserGroup 增加 User 时,增加的 User 需要与 UserGroup 关联的动态系统用户相关联
@@ -39,12 +40,13 @@ def on_user_groups_change(sender, instance, action, reverse, pk_set, **kwargs):
@receiver(m2m_changed, sender=AssetPermission.nodes.through)
+@on_transaction_commit
def on_permission_nodes_changed(instance, action, reverse, pk_set, model, **kwargs):
if reverse:
raise M2MReverseNotAllowed
-
if action != POST_ADD:
return
+
logger.debug("Asset permission nodes change signal received")
nodes = model.objects.filter(pk__in=pk_set)
system_users = instance.system_users.all()
@@ -55,12 +57,13 @@ def on_permission_nodes_changed(instance, action, reverse, pk_set, model, **kwar
@receiver(m2m_changed, sender=AssetPermission.assets.through)
+@on_transaction_commit
def on_permission_assets_changed(instance, action, reverse, pk_set, model, **kwargs):
if reverse:
raise M2MReverseNotAllowed
-
if action != POST_ADD:
return
+
logger.debug("Asset permission assets change signal received")
assets = model.objects.filter(pk__in=pk_set)
@@ -71,33 +74,38 @@ def on_permission_assets_changed(instance, action, reverse, pk_set, model, **kwa
@receiver(m2m_changed, sender=AssetPermission.system_users.through)
+@on_transaction_commit
def on_asset_permission_system_users_changed(instance, action, reverse, **kwargs):
if reverse:
raise M2MReverseNotAllowed
-
if action != POST_ADD:
return
+
logger.debug("Asset permission system_users change signal received")
system_users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
assets = instance.assets.all().values_list('id', flat=True)
nodes = instance.nodes.all().values_list('id', flat=True)
- users = instance.users.all().values_list('id', flat=True)
- groups = instance.user_groups.all().values_list('id', flat=True)
+
for system_user in system_users:
system_user.nodes.add(*tuple(nodes))
system_user.assets.add(*tuple(assets))
+
+ # 动态系统用户,需要关联用户和用户组了
if system_user.username_same_with_user:
+ users = instance.users.all().values_list('id', flat=True)
+ groups = instance.user_groups.all().values_list('id', flat=True)
system_user.groups.add(*tuple(groups))
system_user.users.add(*tuple(users))
@receiver(m2m_changed, sender=AssetPermission.users.through)
+@on_transaction_commit
def on_asset_permission_users_changed(instance, action, reverse, pk_set, model, **kwargs):
if reverse:
raise M2MReverseNotAllowed
-
if action != POST_ADD:
return
+
logger.debug("Asset permission users change signal received")
users = model.objects.filter(pk__in=pk_set)
system_users = instance.system_users.all()
@@ -109,13 +117,13 @@ def on_asset_permission_users_changed(instance, action, reverse, pk_set, model,
@receiver(m2m_changed, sender=AssetPermission.user_groups.through)
-def on_asset_permission_user_groups_changed(instance, action, pk_set, model,
- reverse, **kwargs):
+@on_transaction_commit
+def on_asset_permission_user_groups_changed(instance, action, pk_set, model, reverse, **kwargs):
if reverse:
raise M2MReverseNotAllowed
-
if action != POST_ADD:
return
+
logger.debug("Asset permission user groups change signal received")
groups = model.objects.filter(pk__in=pk_set)
system_users = instance.system_users.all()
@@ -126,87 +134,6 @@ def on_asset_permission_user_groups_changed(instance, action, pk_set, model,
system_user.groups.add(*tuple(groups))
-@receiver(m2m_changed, sender=ApplicationPermission.system_users.through)
-def on_application_permission_system_users_changed(sender, instance: ApplicationPermission, action, reverse, pk_set, **kwargs):
- if reverse:
- raise M2MReverseNotAllowed
- if not instance.category_remote_app:
- return
- if action != POST_ADD:
- return
-
- system_users = SystemUser.objects.filter(pk__in=pk_set)
- logger.debug("Application permission system_users change signal received")
- attrs = instance.applications.all().values_list('attrs', flat=True)
-
- asset_ids = [attr['asset'] for attr in attrs if attr.get('asset')]
- if not asset_ids:
- return
-
- for system_user in system_users:
- system_user.assets.add(*asset_ids)
- if system_user.username_same_with_user:
- user_ids = instance.users.all().values_list('id', flat=True)
- group_ids = instance.user_groups.all().values_list('id', flat=True)
- system_user.groups.add(*group_ids)
- system_user.users.add(*user_ids)
-@receiver(m2m_changed, sender=ApplicationPermission.users.through)
-def on_application_permission_users_changed(sender, instance, action, reverse, pk_set, **kwargs):
- if reverse:
- raise M2MReverseNotAllowed
- if not instance.category_remote_app:
- return
-
- if action != POST_ADD:
- return
-
- logger.debug("Application permission users change signal received")
- user_ids = User.objects.filter(pk__in=pk_set).values_list('id', flat=True)
- system_users = instance.system_users.all()
-
- for system_user in system_users:
- if system_user.username_same_with_user:
- system_user.users.add(*user_ids)
-
-
-@receiver(m2m_changed, sender=ApplicationPermission.user_groups.through)
-def on_application_permission_user_groups_changed(sender, instance, action, reverse, pk_set, **kwargs):
- if reverse:
- raise M2MReverseNotAllowed
- if not instance.category_remote_app:
- return
- if action != POST_ADD:
- return
-
- logger.debug("Application permission user groups change signal received")
- group_ids = UserGroup.objects.filter(pk__in=pk_set).values_list('id', flat=True)
- system_users = instance.system_users.all()
-
- for system_user in system_users:
- if system_user.username_same_with_user:
- system_user.groups.add(*group_ids)
-
-
-@receiver(m2m_changed, sender=ApplicationPermission.applications.through)
-def on_application_permission_applications_changed(sender, instance, action, reverse, pk_set, **kwargs):
- if reverse:
- raise M2MReverseNotAllowed
-
- if not instance.category_remote_app:
- return
-
- if action != POST_ADD:
- return
-
- attrs = Application.objects.filter(id__in=pk_set).values_list('attrs', flat=True)
- asset_ids = [attr['asset'] for attr in attrs if attr.get('asset')]
- if not asset_ids:
- return
-
- system_users = instance.system_users.all()
-
- for system_user in system_users:
- system_user.assets.add(*asset_ids)
diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py
index d7cfa4cec..65438dda1 100644
--- a/apps/settings/api/__init__.py
+++ b/apps/settings/api/__init__.py
@@ -1,5 +1,10 @@
-from .common import *
+from .settings import *
from .ldap import *
from .wecom import *
from .dingtalk import *
from .feishu import *
+from .public import *
+from .email import *
+from .alibaba_sms import *
+from .tencent_sms import *
+from .sms import *
diff --git a/apps/settings/api/alibaba_sms.py b/apps/settings/api/alibaba_sms.py
new file mode 100644
index 000000000..c7f42f110
--- /dev/null
+++ b/apps/settings/api/alibaba_sms.py
@@ -0,0 +1,58 @@
+from rest_framework.views import Response
+from rest_framework.generics import GenericAPIView
+from rest_framework.exceptions import APIException
+from rest_framework import status
+from django.utils.translation import gettext_lazy as _
+
+from common.message.backends.sms import SMS_MESSAGE
+from common.message.backends.sms.alibaba import AlibabaSMS
+from settings.models import Setting
+from common.permissions import IsSuperUser
+from common.exceptions import JMSException
+
+from .. import serializers
+
+
+class AlibabaSMSTestingAPI(GenericAPIView):
+ permission_classes = (IsSuperUser,)
+ serializer_class = serializers.AlibabaSMSSettingSerializer
+
+ def post(self, request):
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ alibaba_access_key_id = serializer.validated_data['ALIBABA_ACCESS_KEY_ID']
+ alibaba_access_key_secret = serializer.validated_data.get('ALIBABA_ACCESS_KEY_SECRET')
+ alibaba_sms_sign_and_tmpl = serializer.validated_data['ALIBABA_SMS_SIGN_AND_TEMPLATES']
+ test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
+
+ if not test_phone:
+ raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
+
+ if not alibaba_access_key_secret:
+ secret = Setting.objects.filter(name='ALIBABA_ACCESS_KEY_SECRET').first()
+ if secret:
+ alibaba_access_key_secret = secret.cleaned_value
+
+ alibaba_access_key_secret = alibaba_access_key_secret or ''
+
+ try:
+ client = AlibabaSMS(
+ access_key_id=alibaba_access_key_id,
+ access_key_secret=alibaba_access_key_secret
+ )
+ sign, tmpl = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(alibaba_sms_sign_and_tmpl)
+
+ client.send_sms(
+ phone_numbers=[test_phone],
+ sign_name=sign,
+ template_code=tmpl,
+ template_param={'code': 'test'}
+ )
+ return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
+ except APIException as e:
+ try:
+ error = e.detail['errmsg']
+ except:
+ error = e.detail
+ return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
diff --git a/apps/settings/api/common.py b/apps/settings/api/common.py
deleted file mode 100644
index 5f0e6f89c..000000000
--- a/apps/settings/api/common.py
+++ /dev/null
@@ -1,192 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-
-from smtplib import SMTPSenderRefused
-from rest_framework import generics
-from rest_framework.views import Response, APIView
-from rest_framework.permissions import AllowAny
-from django.conf import settings
-from django.core.mail import send_mail, get_connection
-from django.utils.translation import ugettext_lazy as _
-from django.templatetags.static import static
-
-from jumpserver.utils import has_valid_xpack_license
-from common.permissions import IsSuperUser
-from common.utils import get_logger
-from .. import serializers
-from ..models import Setting
-
-logger = get_logger(__file__)
-
-
-class MailTestingAPI(APIView):
- permission_classes = (IsSuperUser,)
- serializer_class = serializers.MailTestSerializer
- success_message = _("Test mail sent to {}, please check")
-
- def post(self, request):
- serializer = self.serializer_class(data=request.data)
- serializer.is_valid(raise_exception=True)
-
- email_host = serializer.validated_data['EMAIL_HOST']
- email_port = serializer.validated_data['EMAIL_PORT']
- email_host_user = serializer.validated_data["EMAIL_HOST_USER"]
- email_host_password = serializer.validated_data['EMAIL_HOST_PASSWORD']
- email_from = serializer.validated_data["EMAIL_FROM"]
- email_recipient = serializer.validated_data["EMAIL_RECIPIENT"]
- email_use_ssl = serializer.validated_data['EMAIL_USE_SSL']
- email_use_tls = serializer.validated_data['EMAIL_USE_TLS']
-
- # 设置 settings 的值,会导致动态配置在当前进程失效
- # for k, v in serializer.validated_data.items():
- # if k.startswith('EMAIL'):
- # setattr(settings, k, v)
- try:
- subject = "Test"
- message = "Test smtp setting"
- email_from = email_from or email_host_user
- email_recipient = email_recipient or email_from
- connection = get_connection(
- host=email_host, port=email_port,
- username=email_host_user, password=email_host_password,
- use_tls=email_use_tls, use_ssl=email_use_ssl,
- )
- send_mail(
- subject, message, email_from, [email_recipient],
- connection=connection
- )
- except SMTPSenderRefused as e:
- error = e.smtp_error
- if isinstance(error, bytes):
- for coding in ('gbk', 'utf8'):
- try:
- error = error.decode(coding)
- except UnicodeDecodeError:
- continue
- else:
- break
- return Response({"error": str(error)}, status=400)
- except Exception as e:
- logger.error(e)
- return Response({"error": str(e)}, status=400)
- return Response({"msg": self.success_message.format(email_recipient)})
-
-
-class PublicSettingApi(generics.RetrieveAPIView):
- permission_classes = (AllowAny,)
- serializer_class = serializers.PublicSettingSerializer
-
- @staticmethod
- def get_logo_urls():
- logo_urls = {
- 'logo_logout': static('img/logo.png'),
- 'logo_index': static('img/logo_text.png'),
- 'login_image': static('img/login_image.jpg'),
- 'favicon': static('img/facio.ico')
- }
- if not settings.XPACK_ENABLED:
- return logo_urls
- from xpack.plugins.interface.models import Interface
- obj = Interface.interface()
- if not obj:
- return logo_urls
- for attr in ['logo_logout', 'logo_index', 'login_image', 'favicon']:
- if getattr(obj, attr, '') and getattr(obj, attr).url:
- logo_urls.update({attr: getattr(obj, attr).url})
- return logo_urls
-
- @staticmethod
- def get_login_title():
- default_title = _('Welcome to the JumpServer open source Bastion Host')
- if not settings.XPACK_ENABLED:
- return default_title
- from xpack.plugins.interface.models import Interface
- return Interface.get_login_title()
-
- def get_object(self):
- instance = {
- "data": {
- "WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD,
- "SECURITY_MAX_IDLE_TIME": settings.SECURITY_MAX_IDLE_TIME,
- "XPACK_ENABLED": settings.XPACK_ENABLED,
- "LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE,
- "SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
- "SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL,
- "OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT,
- "SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
- "SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
- "SECURITY_LUNA_REMEMBER_AUTH": settings.SECURITY_LUNA_REMEMBER_AUTH,
- "XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
- "LOGIN_TITLE": self.get_login_title(),
- "LOGO_URLS": self.get_logo_urls(),
- "TICKETS_ENABLED": settings.TICKETS_ENABLED,
- "PASSWORD_RULE": {
- 'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH,
- 'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH,
- 'SECURITY_PASSWORD_UPPER_CASE': settings.SECURITY_PASSWORD_UPPER_CASE,
- 'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE,
- 'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
- 'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
- },
- "AUTH_WECOM": settings.AUTH_WECOM,
- "AUTH_DINGTALK": settings.AUTH_DINGTALK,
- "AUTH_FEISHU": settings.AUTH_FEISHU,
- 'SECURITY_WATERMARK_ENABLED': settings.SECURITY_WATERMARK_ENABLED
- }
- }
- return instance
-
-
-class SettingsApi(generics.RetrieveUpdateAPIView):
- permission_classes = (IsSuperUser,)
- serializer_class_mapper = {
- 'all': serializers.SettingsSerializer,
- 'basic': serializers.BasicSettingSerializer,
- 'terminal': serializers.TerminalSettingSerializer,
- 'security': serializers.SecuritySettingSerializer,
- 'ldap': serializers.LDAPSettingSerializer,
- 'email': serializers.EmailSettingSerializer,
- 'email_content': serializers.EmailContentSettingSerializer,
- 'wecom': serializers.WeComSettingSerializer,
- 'dingtalk': serializers.DingTalkSettingSerializer,
- 'feishu': serializers.FeiShuSettingSerializer,
- }
-
- def get_serializer_class(self):
- category = self.request.query_params.get('category', serializers.BasicSettingSerializer)
- return self.serializer_class_mapper.get(category, serializers.BasicSettingSerializer)
-
- def get_fields(self):
- serializer = self.get_serializer_class()()
- fields = serializer.get_fields()
- return fields
-
- def get_object(self):
- items = self.get_fields().keys()
- obj = {item: getattr(settings, item) for item in items}
- return obj
-
- def parse_serializer_data(self, serializer):
- data = []
- fields = self.get_fields()
- encrypted_items = [name for name, field in fields.items() if field.write_only]
- category = self.request.query_params.get('category', '')
- for name, value in serializer.validated_data.items():
- encrypted = name in encrypted_items
- if encrypted and value in ['', None]:
- continue
- data.append({
- 'name': name, 'value': value,
- 'encrypted': encrypted, 'category': category
- })
- return data
-
- def perform_update(self, serializer):
- settings_items = self.parse_serializer_data(serializer)
- serializer_data = getattr(serializer, 'data', {})
- for item in settings_items:
- changed, setting = Setting.update_or_create(**item)
- if not changed:
- continue
- serializer_data[setting.name] = setting.cleaned_value
- setattr(serializer, '_data', serializer_data)
diff --git a/apps/settings/api/email.py b/apps/settings/api/email.py
new file mode 100644
index 000000000..91163213a
--- /dev/null
+++ b/apps/settings/api/email.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+#
+
+from smtplib import SMTPSenderRefused
+from rest_framework.views import Response, APIView
+from django.core.mail import send_mail, get_connection
+from django.utils.translation import ugettext_lazy as _
+
+from common.permissions import IsSuperUser
+from common.utils import get_logger
+from .. import serializers
+
+logger = get_logger(__file__)
+
+__all__ = ['MailTestingAPI']
+
+
+class MailTestingAPI(APIView):
+ permission_classes = (IsSuperUser,)
+ serializer_class = serializers.MailTestSerializer
+ success_message = _("Test mail sent to {}, please check")
+
+ def post(self, request):
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ email_host = serializer.validated_data['EMAIL_HOST']
+ email_port = serializer.validated_data['EMAIL_PORT']
+ email_host_user = serializer.validated_data["EMAIL_HOST_USER"]
+ email_host_password = serializer.validated_data['EMAIL_HOST_PASSWORD']
+ email_from = serializer.validated_data["EMAIL_FROM"]
+ email_recipient = serializer.validated_data["EMAIL_RECIPIENT"]
+ email_use_ssl = serializer.validated_data['EMAIL_USE_SSL']
+ email_use_tls = serializer.validated_data['EMAIL_USE_TLS']
+
+ # 设置 settings 的值,会导致动态配置在当前进程失效
+ # for k, v in serializer.validated_data.items():
+ # if k.startswith('EMAIL'):
+ # setattr(settings, k, v)
+ try:
+ subject = "Test"
+ message = "Test smtp setting"
+ email_from = email_from or email_host_user
+ email_recipient = email_recipient or email_from
+ connection = get_connection(
+ host=email_host, port=email_port,
+ username=email_host_user, password=email_host_password,
+ use_tls=email_use_tls, use_ssl=email_use_ssl,
+ )
+ send_mail(
+ subject, message, email_from, [email_recipient],
+ connection=connection
+ )
+ except SMTPSenderRefused as e:
+ error = e.smtp_error
+ if isinstance(error, bytes):
+ for coding in ('gbk', 'utf8'):
+ try:
+ error = error.decode(coding)
+ except UnicodeDecodeError:
+ continue
+ else:
+ break
+ return Response({"error": str(error)}, status=400)
+ except Exception as e:
+ logger.error(e)
+ return Response({"error": str(e)}, status=400)
+ return Response({"msg": self.success_message.format(email_recipient)})
\ No newline at end of file
diff --git a/apps/settings/api/public.py b/apps/settings/api/public.py
new file mode 100644
index 000000000..b15f0c6f7
--- /dev/null
+++ b/apps/settings/api/public.py
@@ -0,0 +1,80 @@
+from rest_framework import generics
+from rest_framework.permissions import AllowAny
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
+from django.templatetags.static import static
+
+from jumpserver.utils import has_valid_xpack_license
+from common.utils import get_logger
+from .. import serializers
+
+logger = get_logger(__name__)
+
+__all__ = ['PublicSettingApi']
+
+
+class PublicSettingApi(generics.RetrieveAPIView):
+ permission_classes = (AllowAny,)
+ serializer_class = serializers.PublicSettingSerializer
+
+ @staticmethod
+ def get_logo_urls():
+ logo_urls = {
+ 'logo_logout': static('img/logo.png'),
+ 'logo_index': static('img/logo_text.png'),
+ 'login_image': static('img/login_image.jpg'),
+ 'favicon': static('img/facio.ico')
+ }
+ if not settings.XPACK_ENABLED:
+ return logo_urls
+ from xpack.plugins.interface.models import Interface
+ obj = Interface.interface()
+ if not obj:
+ return logo_urls
+ for attr in ['logo_logout', 'logo_index', 'login_image', 'favicon']:
+ if getattr(obj, attr, '') and getattr(obj, attr).url:
+ logo_urls.update({attr: getattr(obj, attr).url})
+ return logo_urls
+
+ @staticmethod
+ def get_login_title():
+ default_title = _('Welcome to the JumpServer open source Bastion Host')
+ if not settings.XPACK_ENABLED:
+ return default_title
+ from xpack.plugins.interface.models import Interface
+ return Interface.get_login_title()
+
+ def get_object(self):
+ instance = {
+ "data": {
+ "WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD,
+ "SECURITY_MAX_IDLE_TIME": settings.SECURITY_MAX_IDLE_TIME,
+ "XPACK_ENABLED": settings.XPACK_ENABLED,
+ "LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE,
+ "SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
+ "SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL,
+ "OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT,
+ "SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
+ "SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
+ "SECURITY_LUNA_REMEMBER_AUTH": settings.SECURITY_LUNA_REMEMBER_AUTH,
+ "XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
+ "LOGIN_TITLE": self.get_login_title(),
+ "LOGO_URLS": self.get_logo_urls(),
+ "TICKETS_ENABLED": settings.TICKETS_ENABLED,
+ "PASSWORD_RULE": {
+ 'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH,
+ 'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH,
+ 'SECURITY_PASSWORD_UPPER_CASE': settings.SECURITY_PASSWORD_UPPER_CASE,
+ 'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE,
+ 'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
+ 'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
+ },
+ "AUTH_WECOM": settings.AUTH_WECOM,
+ "AUTH_DINGTALK": settings.AUTH_DINGTALK,
+ "AUTH_FEISHU": settings.AUTH_FEISHU,
+ 'SECURITY_WATERMARK_ENABLED': settings.SECURITY_WATERMARK_ENABLED,
+ 'SECURITY_SESSION_SHARE': settings.SECURITY_SESSION_SHARE,
+ "XRDP_ENABLED": settings.XRDP_ENABLED,
+ }
+ }
+ return instance
diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py
new file mode 100644
index 000000000..4dda51408
--- /dev/null
+++ b/apps/settings/api/settings.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+#
+
+from rest_framework import generics
+from django.conf import settings
+
+from jumpserver.conf import Config
+from common.permissions import IsSuperUser
+from common.utils import get_logger
+from .. import serializers
+from ..models import Setting
+
+logger = get_logger(__file__)
+
+
+class SettingsApi(generics.RetrieveUpdateAPIView):
+ permission_classes = (IsSuperUser,)
+ serializer_class_mapper = {
+ 'all': serializers.SettingsSerializer,
+ 'basic': serializers.BasicSettingSerializer,
+ 'terminal': serializers.TerminalSettingSerializer,
+ 'security': serializers.SecuritySettingSerializer,
+ 'ldap': serializers.LDAPSettingSerializer,
+ 'email': serializers.EmailSettingSerializer,
+ 'email_content': serializers.EmailContentSettingSerializer,
+ 'wecom': serializers.WeComSettingSerializer,
+ 'dingtalk': serializers.DingTalkSettingSerializer,
+ 'feishu': serializers.FeiShuSettingSerializer,
+ 'auth': serializers.AuthSettingSerializer,
+ 'oidc': serializers.OIDCSettingSerializer,
+ 'keycloak': serializers.KeycloakSettingSerializer,
+ 'radius': serializers.RadiusSettingSerializer,
+ 'cas': serializers.CASSettingSerializer,
+ 'sso': serializers.SSOSettingSerializer,
+ 'clean': serializers.CleaningSerializer,
+ 'other': serializers.OtherSettingSerializer,
+ 'alibaba': serializers.AlibabaSMSSettingSerializer,
+ 'tencent': serializers.TencentSMSSettingSerializer,
+ }
+
+ def get_serializer_class(self):
+ category = self.request.query_params.get('category', 'basic')
+ default = serializers.BasicSettingSerializer
+ cls = self.serializer_class_mapper.get(category, default)
+ return cls
+
+ def get_fields(self):
+ serializer = self.get_serializer_class()()
+ fields = serializer.get_fields()
+ return fields
+
+ def get_object(self):
+ items = self.get_fields().keys()
+ obj = {}
+ for item in items:
+ if hasattr(settings, item):
+ obj[item] = getattr(settings, item)
+ else:
+ obj[item] = Config.defaults[item]
+ return obj
+
+ def parse_serializer_data(self, serializer):
+ data = []
+ fields = self.get_fields()
+ encrypted_items = [name for name, field in fields.items() if field.write_only]
+ category = self.request.query_params.get('category', '')
+ for name, value in serializer.validated_data.items():
+ encrypted = name in encrypted_items
+ if encrypted and value in ['', None]:
+ continue
+ data.append({
+ 'name': name, 'value': value,
+ 'encrypted': encrypted, 'category': category
+ })
+ return data
+
+ def perform_update(self, serializer):
+ settings_items = self.parse_serializer_data(serializer)
+ serializer_data = getattr(serializer, 'data', {})
+ for item in settings_items:
+ changed, setting = Setting.update_or_create(**item)
+ if not changed:
+ continue
+ serializer_data[setting.name] = setting.cleaned_value
+ setattr(serializer, '_data', serializer_data)
+ if hasattr(serializer, 'post_save'):
+ serializer.post_save()
diff --git a/apps/settings/api/sms.py b/apps/settings/api/sms.py
new file mode 100644
index 000000000..194dbc608
--- /dev/null
+++ b/apps/settings/api/sms.py
@@ -0,0 +1,22 @@
+from rest_framework.generics import ListAPIView
+from rest_framework.response import Response
+
+from common.permissions import IsSuperUser
+from common.message.backends.sms import BACKENDS
+from settings.serializers.sms import SMSBackendSerializer
+
+
+class SMSBackendAPI(ListAPIView):
+ permission_classes = (IsSuperUser,)
+ serializer_class = SMSBackendSerializer
+
+ def list(self, request, *args, **kwargs):
+ data = [
+ {
+ 'name': b,
+ 'label': b.label
+ }
+ for b in BACKENDS
+ ]
+
+ return Response(data)
diff --git a/apps/settings/api/tencent_sms.py b/apps/settings/api/tencent_sms.py
new file mode 100644
index 000000000..27ad07327
--- /dev/null
+++ b/apps/settings/api/tencent_sms.py
@@ -0,0 +1,63 @@
+from collections import OrderedDict
+
+from rest_framework.views import Response
+from rest_framework.generics import GenericAPIView
+from rest_framework.exceptions import APIException
+from rest_framework import status
+from django.utils.translation import gettext_lazy as _
+
+from common.message.backends.sms import SMS_MESSAGE
+from common.message.backends.sms.tencent import TencentSMS
+from settings.models import Setting
+from common.permissions import IsSuperUser
+from common.exceptions import JMSException
+
+from .. import serializers
+
+
+class TencentSMSTestingAPI(GenericAPIView):
+ permission_classes = (IsSuperUser,)
+ serializer_class = serializers.TencentSMSSettingSerializer
+
+ def post(self, request):
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ tencent_secret_id = serializer.validated_data['TENCENT_SECRET_ID']
+ tencent_secret_key = serializer.validated_data.get('TENCENT_SECRET_KEY')
+ tencent_sms_sign_and_tmpl = serializer.validated_data['TENCENT_SMS_SIGN_AND_TEMPLATES']
+ tencent_sdkappid = serializer.validated_data.get('TENCENT_SDKAPPID')
+
+ test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
+
+ if not test_phone:
+ raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
+
+ if not tencent_secret_key:
+ secret = Setting.objects.filter(name='TENCENT_SECRET_KEY').first()
+ if secret:
+ tencent_secret_key = secret.cleaned_value
+
+ tencent_secret_key = tencent_secret_key or ''
+
+ try:
+ client = TencentSMS(
+ secret_id=tencent_secret_id,
+ secret_key=tencent_secret_key,
+ sdkappid=tencent_sdkappid
+ )
+ sign, tmpl = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(tencent_sms_sign_and_tmpl)
+
+ client.send_sms(
+ phone_numbers=[test_phone],
+ sign_name=sign,
+ template_code=tmpl,
+ template_param=OrderedDict(code='test')
+ )
+ return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
+ except APIException as e:
+ try:
+ error = e.detail['errmsg']
+ except:
+ error = e.detail
+ return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
diff --git a/apps/settings/migrations/0003_auto_20210901_1035.py b/apps/settings/migrations/0003_auto_20210901_1035.py
new file mode 100644
index 000000000..c6f37625a
--- /dev/null
+++ b/apps/settings/migrations/0003_auto_20210901_1035.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.12 on 2021-09-01 02:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('settings', '0002_auto_20210729_1546'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='setting',
+ name='value',
+ field=models.TextField(blank=True, null=True, verbose_name='Value'),
+ ),
+ ]
diff --git a/apps/settings/models.py b/apps/settings/models.py
index 0a986efcb..1660e318d 100644
--- a/apps/settings/models.py
+++ b/apps/settings/models.py
@@ -27,7 +27,7 @@ class SettingManager(models.Manager):
class Setting(models.Model):
name = models.CharField(max_length=128, unique=True, verbose_name=_("Name"))
- value = models.TextField(verbose_name=_("Value"))
+ value = models.TextField(verbose_name=_("Value"), null=True, blank=True)
category = models.CharField(max_length=128, default="default")
encrypted = models.BooleanField(default=False)
enabled = models.BooleanField(verbose_name=_("Enabled"), default=True)
diff --git a/apps/settings/serializers/__init__.py b/apps/settings/serializers/__init__.py
index 5868a76df..0a55f645d 100644
--- a/apps/settings/serializers/__init__.py
+++ b/apps/settings/serializers/__init__.py
@@ -1,7 +1,13 @@
# coding: utf-8
#
+from .basic import *
+from .auth import *
from .email import *
-from .ldap import *
from .public import *
from .settings import *
+from .security import *
+from .terminal import *
+from .cleaning import *
+from .other import *
+
diff --git a/apps/settings/serializers/auth/__init__.py b/apps/settings/serializers/auth/__init__.py
new file mode 100644
index 000000000..4a2f77ebe
--- /dev/null
+++ b/apps/settings/serializers/auth/__init__.py
@@ -0,0 +1,10 @@
+from .cas import *
+from .ldap import *
+from .oidc import *
+from .radius import *
+from .dingtalk import *
+from .feishu import *
+from .wecom import *
+from .sso import *
+from .base import *
+from .sms import *
diff --git a/apps/settings/serializers/auth/base.py b/apps/settings/serializers/auth/base.py
new file mode 100644
index 000000000..d98816108
--- /dev/null
+++ b/apps/settings/serializers/auth/base.py
@@ -0,0 +1,25 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+__all__ = [
+ 'AuthSettingSerializer',
+]
+
+
+class AuthSettingSerializer(serializers.Serializer):
+ AUTH_CAS = serializers.BooleanField(required=False, label=_('CAS Auth'))
+ AUTH_OPENID = serializers.BooleanField(required=False, label=_('OPENID Auth'))
+ AUTH_RADIUS = serializers.BooleanField(required=False, label=_('RADIUS Auth'))
+ AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('DingTalk Auth'))
+ AUTH_FEISHU = serializers.BooleanField(default=False, label=_('FeiShu Auth'))
+ AUTH_WECOM = serializers.BooleanField(default=False, label=_('WeCom Auth'))
+ AUTH_SSO = serializers.BooleanField(default=False, label=_("SSO Auth"))
+ FORGOT_PASSWORD_URL = serializers.CharField(
+ required=False, max_length=1024, label=_("Forgot password url")
+ )
+ HEALTH_CHECK_TOKEN = serializers.CharField(
+ required=False, max_length=1024, label=_("Health check token")
+ )
+ LOGIN_REDIRECT_MSG_ENABLED = serializers.BooleanField(
+ required=False, label=_("Enable login redirect msg")
+ )
diff --git a/apps/settings/serializers/auth/cas.py b/apps/settings/serializers/auth/cas.py
new file mode 100644
index 000000000..49a02505f
--- /dev/null
+++ b/apps/settings/serializers/auth/cas.py
@@ -0,0 +1,18 @@
+
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+__all__ = [
+ 'CASSettingSerializer',
+]
+
+
+class CASSettingSerializer(serializers.Serializer):
+ AUTH_CAS = serializers.BooleanField(required=False, label=_('Enable CAS Auth'))
+ CAS_SERVER_URL = serializers.CharField(required=False, max_length=1024, label=_('Server url'))
+ CAS_LOGOUT_COMPLETELY = serializers.BooleanField(required=False, label=_('Logout completely'))
+ CAS_VERSION = serializers.IntegerField(required=False, label=_('Version'))
+ CAS_USERNAME_ATTRIBUTE = serializers.CharField(required=False, max_length=1024, label=_('Username attr'))
+ CAS_APPLY_ATTRIBUTES_TO_USER = serializers.BooleanField(required=False, label=_('Enable attributes map'))
+ CAS_RENAME_ATTRIBUTES = serializers.DictField(required=False, label=_('Rename attr'))
+ CAS_CREATE_USER = serializers.BooleanField(required=False, label=_('Create user if not'))
diff --git a/apps/settings/serializers/auth/dingtalk.py b/apps/settings/serializers/auth/dingtalk.py
new file mode 100644
index 000000000..062f19f26
--- /dev/null
+++ b/apps/settings/serializers/auth/dingtalk.py
@@ -0,0 +1,11 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+__all__ = ['DingTalkSettingSerializer']
+
+
+class DingTalkSettingSerializer(serializers.Serializer):
+ DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label='AgentId')
+ DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label='AppKey')
+ DINGTALK_APPSECRET = serializers.CharField(max_length=256, required=False, label='AppSecret', write_only=True)
+ AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth'))
diff --git a/apps/settings/serializers/auth/feishu.py b/apps/settings/serializers/auth/feishu.py
new file mode 100644
index 000000000..68b7ee2b1
--- /dev/null
+++ b/apps/settings/serializers/auth/feishu.py
@@ -0,0 +1,11 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+__all__ = ['FeiShuSettingSerializer']
+
+
+class FeiShuSettingSerializer(serializers.Serializer):
+ FEISHU_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID')
+ FEISHU_APP_SECRET = serializers.CharField(max_length=256, required=False, label='App Secret', write_only=True)
+ AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth'))
+
diff --git a/apps/settings/serializers/auth/ldap.py b/apps/settings/serializers/auth/ldap.py
new file mode 100644
index 000000000..d4eba4089
--- /dev/null
+++ b/apps/settings/serializers/auth/ldap.py
@@ -0,0 +1,74 @@
+
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+__all__ = [
+ 'LDAPTestConfigSerializer', 'LDAPUserSerializer', 'LDAPTestLoginSerializer',
+ 'LDAPSettingSerializer',
+]
+
+
+class LDAPTestConfigSerializer(serializers.Serializer):
+ AUTH_LDAP_SERVER_URI = serializers.CharField(max_length=1024)
+ AUTH_LDAP_BIND_DN = serializers.CharField(max_length=1024, required=False, allow_blank=True)
+ AUTH_LDAP_BIND_PASSWORD = serializers.CharField(required=False, allow_blank=True)
+ AUTH_LDAP_SEARCH_OU = serializers.CharField()
+ AUTH_LDAP_SEARCH_FILTER = serializers.CharField()
+ AUTH_LDAP_USER_ATTR_MAP = serializers.CharField()
+ AUTH_LDAP_START_TLS = serializers.BooleanField(required=False)
+ AUTH_LDAP = serializers.BooleanField(required=False)
+
+
+class LDAPTestLoginSerializer(serializers.Serializer):
+ username = serializers.CharField(max_length=1024, required=True)
+ password = serializers.CharField(max_length=2014, required=True)
+
+
+class LDAPUserSerializer(serializers.Serializer):
+ id = serializers.CharField()
+ username = serializers.CharField()
+ name = serializers.CharField()
+ email = serializers.CharField()
+ existing = serializers.BooleanField(read_only=True)
+
+
+class LDAPSettingSerializer(serializers.Serializer):
+ # encrypt_fields 现在使用 write_only 来判断了
+
+ AUTH_LDAP_SERVER_URI = serializers.CharField(
+ required=True, max_length=1024, label=_('LDAP server'),
+ help_text=_('eg: ldap://localhost:389')
+ )
+ AUTH_LDAP_BIND_DN = serializers.CharField(required=False, max_length=1024, label=_('Bind DN'))
+ AUTH_LDAP_BIND_PASSWORD = serializers.CharField(max_length=1024, write_only=True, required=False,
+ label=_('Password'))
+ AUTH_LDAP_SEARCH_OU = serializers.CharField(
+ max_length=1024, allow_blank=True, required=False, label=_('User OU'),
+ help_text=_('Use | split multi OUs')
+ )
+ AUTH_LDAP_SEARCH_FILTER = serializers.CharField(
+ max_length=1024, required=True, label=_('User search filter'),
+ help_text=_('Choice may be (cn|uid|sAMAccountName)=%(user)s)')
+ )
+ AUTH_LDAP_USER_ATTR_MAP = serializers.DictField(
+ required=True, label=_('User attr map'),
+ help_text=_('User attr map present how to map LDAP user attr to '
+ 'jumpserver, username,name,email is jumpserver attr')
+ )
+ AUTH_LDAP_SYNC_IS_PERIODIC = serializers.BooleanField(required=False, label=_('Periodic display'))
+ AUTH_LDAP_SYNC_INTERVAL = serializers.CharField(
+ required=False, max_length=1024, allow_null=True,
+ label=_('Interval'), help_text=_('Unit: hour')
+ )
+ AUTH_LDAP_SYNC_CRONTAB = serializers.CharField(
+ required=False, max_length=1024, allow_null=True, label=_('Regularly perform')
+ )
+ AUTH_LDAP_CONNECT_TIMEOUT = serializers.IntegerField(required=False, label=_('Connect timeout'))
+ AUTH_LDAP_SEARCH_PAGED_SIZE = serializers.IntegerField(required=False, label=_('Search paged size'))
+
+ AUTH_LDAP = serializers.BooleanField(required=False, label=_('Enable LDAP auth'))
+
+ @staticmethod
+ def post_save():
+ from users.tasks import import_ldap_user_periodic
+ import_ldap_user_periodic()
diff --git a/apps/settings/serializers/auth/oidc.py b/apps/settings/serializers/auth/oidc.py
new file mode 100644
index 000000000..21f0f989e
--- /dev/null
+++ b/apps/settings/serializers/auth/oidc.py
@@ -0,0 +1,78 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+__all__ = [
+ 'OIDCSettingSerializer', 'KeycloakSettingSerializer',
+]
+
+
+class CommonSettingSerializer(serializers.Serializer):
+ # OpenID 公有配置参数 (version <= 1.5.8 或 version >= 1.5.8)
+ BASE_SITE_URL = serializers.CharField(
+ required=False, allow_null=True, max_length=1024, label=_('Base site url')
+ )
+ AUTH_OPENID_CLIENT_ID = serializers.CharField(
+ required=False, max_length=1024, label=_('Client Id')
+ )
+ AUTH_OPENID_CLIENT_SECRET = serializers.CharField(
+ required=False, max_length=1024, write_only=True, label=_('Client Secret')
+ )
+ AUTH_OPENID_SHARE_SESSION = serializers.BooleanField(required=False, label=_('Share session'))
+ AUTH_OPENID_IGNORE_SSL_VERIFICATION = serializers.BooleanField(
+ required=False, label=_('Ignore ssl verification')
+ )
+
+
+class KeycloakSettingSerializer(CommonSettingSerializer):
+ # OpenID 旧配置参数 (version <= 1.5.8 (discarded))
+ AUTH_OPENID_KEYCLOAK = serializers.BooleanField(
+ label=_("Use Keycloak"), required=False, default=False
+ )
+ AUTH_OPENID_SERVER_URL = serializers.CharField(
+ required=False, max_length=1024, label=_('Server url')
+ )
+ AUTH_OPENID_REALM_NAME = serializers.CharField(
+ required=False, max_length=1024, allow_null=True, label=_('Realm name')
+ )
+
+
+class OIDCSettingSerializer(KeycloakSettingSerializer):
+ # OpenID 新配置参数 (version >= 1.5.9)
+ AUTH_OPENID = serializers.BooleanField(required=False, label=_('Enable OPENID Auth'))
+ AUTH_OPENID_PROVIDER_ENDPOINT = serializers.CharField(
+ required=False, max_length=1024, label=_('Provider endpoint')
+ )
+ AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT = serializers.CharField(
+ required=False, max_length=1024, label=_('Provider auth endpoint')
+ )
+ AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT = serializers.CharField(
+ required=False, max_length=1024, label=_('Provider token endpoint')
+ )
+ AUTH_OPENID_PROVIDER_JWKS_ENDPOINT = serializers.CharField(
+ required=False, max_length=1024, label=_('Provider jwks endpoint')
+ )
+ AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT = serializers.CharField(
+ required=False, max_length=1024, label=_('Provider userinfo endpoint')
+ )
+ AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT = serializers.CharField(
+ required=False, max_length=1024, label=_('Provider end session endpoint')
+ )
+ AUTH_OPENID_PROVIDER_SIGNATURE_ALG = serializers.CharField(
+ required=False, max_length=1024, label=_('Provider sign alg')
+ )
+ AUTH_OPENID_PROVIDER_SIGNATURE_KEY = serializers.CharField(
+ required=False, max_length=1024, allow_null=True, label=_('Provider sign key')
+ )
+ AUTH_OPENID_SCOPES = serializers.CharField(required=False, max_length=1024, label=_('Scopes'))
+ AUTH_OPENID_ID_TOKEN_MAX_AGE = serializers.IntegerField(
+ required=False, label=_('Id token max age')
+ )
+ AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS = serializers.BooleanField(
+ required=False, label=_('Id token include claims')
+ )
+ AUTH_OPENID_USE_STATE = serializers.BooleanField(required=False, label=_('Use state'))
+ AUTH_OPENID_USE_NONCE = serializers.BooleanField(required=False, label=_('Use nonce'))
+ AUTH_OPENID_ALWAYS_UPDATE_USER = serializers.BooleanField(
+ required=False, label=_('Always update user')
+ )
+
diff --git a/apps/settings/serializers/auth/radius.py b/apps/settings/serializers/auth/radius.py
new file mode 100644
index 000000000..0a956d18a
--- /dev/null
+++ b/apps/settings/serializers/auth/radius.py
@@ -0,0 +1,19 @@
+# coding: utf-8
+#
+
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+__all__ = [
+ 'RadiusSettingSerializer',
+]
+
+
+class RadiusSettingSerializer(serializers.Serializer):
+ AUTH_RADIUS = serializers.BooleanField(required=False, label=_('Enable RADIUS Auth'))
+ RADIUS_SERVER = serializers.CharField(required=False, max_length=1024, label=_('Host'))
+ RADIUS_PORT = serializers.IntegerField(required=False, label=_('Port'))
+ RADIUS_SECRET = serializers.CharField(
+ required=False, max_length=1024, allow_null=True, label=_('Secret'), write_only=True
+ )
+ OTP_IN_RADIUS = serializers.BooleanField(required=False, label=_('OTP in radius'))
diff --git a/apps/settings/serializers/auth/sms.py b/apps/settings/serializers/auth/sms.py
new file mode 100644
index 000000000..275a8436e
--- /dev/null
+++ b/apps/settings/serializers/auth/sms.py
@@ -0,0 +1,51 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+from common.message.backends.sms import BACKENDS
+
+__all__ = ['AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer']
+
+
+class BaseSMSSettingSerializer(serializers.Serializer):
+ SMS_ENABLED = serializers.BooleanField(default=False, label=_('Enable SMS'))
+ SMS_TEST_PHONE = serializers.CharField(max_length=256, required=False, label=_('Test phone'))
+
+ def to_representation(self, instance):
+ data = super().to_representation(instance)
+ data['SMS_BACKEND'] = self.fields['SMS_BACKEND'].default
+ return data
+
+
+class AlibabaSMSSettingSerializer(BaseSMSSettingSerializer):
+ SMS_BACKEND = serializers.ChoiceField(choices=BACKENDS.choices, default=BACKENDS.ALIBABA)
+ ALIBABA_ACCESS_KEY_ID = serializers.CharField(max_length=256, required=True, label='AccessKeyId')
+ ALIBABA_ACCESS_KEY_SECRET = serializers.CharField(
+ max_length=256, required=False, label='AccessKeySecret', write_only=True)
+ ALIBABA_SMS_SIGN_AND_TEMPLATES = serializers.DictField(
+ label=_('Signatures and Templates'), required=True, help_text=_('''
+ Filling in JSON Data:
+ {
+ "verification_code": {
+ "sign_name": "",
+ "template_code": ""
+ }
+ }
+ ''')
+ )
+
+
+class TencentSMSSettingSerializer(BaseSMSSettingSerializer):
+ SMS_BACKEND = serializers.ChoiceField(choices=BACKENDS.choices, default=BACKENDS.TENCENT)
+ TENCENT_SECRET_ID = serializers.CharField(max_length=256, required=True, label='Secret id')
+ TENCENT_SECRET_KEY = serializers.CharField(max_length=256, required=False, label='Secret key', write_only=True)
+ TENCENT_SDKAPPID = serializers.CharField(max_length=256, required=True, label='SDK app id')
+ TENCENT_SMS_SIGN_AND_TEMPLATES = serializers.DictField(
+ label=_('Signatures and Templates'), required=True, help_text=_('''
+ Filling in JSON Data:
+ {
+ "verification_code": {
+ "sign_name": "",
+ "template_code": ""
+ }
+ }
+ '''))
diff --git a/apps/settings/serializers/auth/sso.py b/apps/settings/serializers/auth/sso.py
new file mode 100644
index 000000000..38481cf2a
--- /dev/null
+++ b/apps/settings/serializers/auth/sso.py
@@ -0,0 +1,17 @@
+
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+__all__ = [
+ 'SSOSettingSerializer',
+]
+
+
+class SSOSettingSerializer(serializers.Serializer):
+ AUTH_SSO = serializers.BooleanField(
+ required=False, label=_('Enable SSO auth'),
+ help_text=_("Other service can using SSO token login to JumpServer without password")
+ )
+ AUTH_SSO_AUTHKEY_TTL = serializers.IntegerField(
+ required=False, label=_('SSO auth key TTL'), help_text=_("Unit: second")
+ )
diff --git a/apps/settings/serializers/auth/wecom.py b/apps/settings/serializers/auth/wecom.py
new file mode 100644
index 000000000..ceb83aa85
--- /dev/null
+++ b/apps/settings/serializers/auth/wecom.py
@@ -0,0 +1,11 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+__all__ = ['WeComSettingSerializer']
+
+
+class WeComSettingSerializer(serializers.Serializer):
+ WECOM_CORPID = serializers.CharField(max_length=256, required=True, label='corpid')
+ WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label='agentid')
+ WECOM_SECRET = serializers.CharField(max_length=256, required=False, label='secret', write_only=True)
+ AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth'))
\ No newline at end of file
diff --git a/apps/settings/serializers/basic.py b/apps/settings/serializers/basic.py
new file mode 100644
index 000000000..82b7f83dd
--- /dev/null
+++ b/apps/settings/serializers/basic.py
@@ -0,0 +1,22 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+
+class BasicSettingSerializer(serializers.Serializer):
+ SITE_URL = serializers.URLField(
+ required=True, label=_("Site url"),
+ help_text=_('eg: http://dev.jumpserver.org:8080')
+ )
+ USER_GUIDE_URL = serializers.URLField(
+ required=False, allow_blank=True, allow_null=True, label=_("User guide url"),
+ help_text=_('User first login update profile done redirect to it')
+ )
+ FORGOT_PASSWORD_URL = serializers.URLField(
+ required=False, allow_blank=True, allow_null=True, label=_("Forgot password url"),
+ help_text=_('The forgot password url on login page, If you use '
+ 'ldap or cas external authentication, you can set it')
+ )
+ GLOBAL_ORG_DISPLAY_NAME = serializers.CharField(
+ required=False, max_length=1024, allow_blank=True, allow_null=True, label=_("Global organization name"),
+ help_text=_('The name of global organization to display')
+ )
diff --git a/apps/settings/serializers/cleaning.py b/apps/settings/serializers/cleaning.py
new file mode 100644
index 000000000..182a97eaa
--- /dev/null
+++ b/apps/settings/serializers/cleaning.py
@@ -0,0 +1,22 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+__all__ = ['CleaningSerializer']
+
+
+class CleaningSerializer(serializers.Serializer):
+ LOGIN_LOG_KEEP_DAYS = serializers.IntegerField(
+ label=_("Login log keep days"), help_text=_("Unit: day")
+ )
+ TASK_LOG_KEEP_DAYS = serializers.IntegerField(
+ label=_("Task log keep days"), help_text=_("Unit: day")
+ )
+ OPERATE_LOG_KEEP_DAYS = serializers.IntegerField(
+ label=_("Operate log keep days"), help_text=_("Unit: day")
+ )
+ FTP_LOG_KEEP_DAYS = serializers.IntegerField(
+ label=_("FTP log keep days"), help_text=_("Unit: day")
+ )
+ CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = serializers.IntegerField(
+ label=_("Cloud sync record keep days"), help_text=_("Unit: day")
+ )
diff --git a/apps/settings/serializers/email.py b/apps/settings/serializers/email.py
index 6d033f6aa..2ad7f84e6 100644
--- a/apps/settings/serializers/email.py
+++ b/apps/settings/serializers/email.py
@@ -1,9 +1,10 @@
# coding: utf-8
#
+from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
-__all__ = ['MailTestSerializer']
+__all__ = ['MailTestSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer']
class MailTestSerializer(serializers.Serializer):
@@ -15,3 +16,56 @@ class MailTestSerializer(serializers.Serializer):
EMAIL_RECIPIENT = serializers.CharField(required=False, allow_blank=True)
EMAIL_USE_SSL = serializers.BooleanField(default=False)
EMAIL_USE_TLS = serializers.BooleanField(default=False)
+
+
+class EmailSettingSerializer(serializers.Serializer):
+ # encrypt_fields 现在使用 write_only 来判断了
+
+ EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("SMTP host"))
+ EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("SMTP port"))
+ EMAIL_HOST_USER = serializers.CharField(max_length=128, required=True, label=_("SMTP account"))
+ EMAIL_HOST_PASSWORD = serializers.CharField(
+ max_length=1024, write_only=True, required=False, label=_("SMTP password"),
+ help_text=_("Tips: Some provider use token except password")
+ )
+ EMAIL_FROM = serializers.CharField(
+ max_length=128, allow_blank=True, required=False, label=_('Send user'),
+ help_text=_('Tips: Send mail account, default SMTP account as the send account')
+ )
+ EMAIL_RECIPIENT = serializers.CharField(
+ max_length=128, allow_blank=True, required=False, label=_('Test recipient'),
+ help_text=_('Tips: Used only as a test mail recipient')
+ )
+ EMAIL_USE_SSL = serializers.BooleanField(
+ required=False, label=_('Use SSL'),
+ help_text=_('If SMTP port is 465, may be select')
+ )
+ EMAIL_USE_TLS = serializers.BooleanField(
+ required=False, label=_("Use TLS"),
+ help_text=_('If SMTP port is 587, may be select')
+ )
+ EMAIL_SUBJECT_PREFIX = serializers.CharField(
+ max_length=1024, required=True, label=_('Subject prefix')
+ )
+
+
+class EmailContentSettingSerializer(serializers.Serializer):
+ EMAIL_CUSTOM_USER_CREATED_SUBJECT = serializers.CharField(
+ max_length=1024, allow_blank=True, required=False,
+ label=_('Create user email subject'),
+ help_text=_('Tips: When creating a user, send the subject of the email (eg:Create account successfully)')
+ )
+ EMAIL_CUSTOM_USER_CREATED_HONORIFIC = serializers.CharField(
+ max_length=1024, allow_blank=True, required=False,
+ label=_('Create user honorific'),
+ help_text=_('Tips: When creating a user, send the honorific of the email (eg:Hello)')
+ )
+ EMAIL_CUSTOM_USER_CREATED_BODY = serializers.CharField(
+ max_length=4096, allow_blank=True, required=False,
+ label=_('Create user email content'),
+ help_text=_('Tips:When creating a user, send the content of the email')
+ )
+ EMAIL_CUSTOM_USER_CREATED_SIGNATURE = serializers.CharField(
+ max_length=512, allow_blank=True, required=False, label=_('Signature'),
+ help_text=_('Tips: Email signature (eg:jumpserver)')
+ )
diff --git a/apps/settings/serializers/ldap.py b/apps/settings/serializers/ldap.py
deleted file mode 100644
index 1ccc02c26..000000000
--- a/apps/settings/serializers/ldap.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# coding: utf-8
-#
-
-from django.utils.translation import ugettext_lazy as _
-from rest_framework import serializers
-
-__all__ = [
- 'LDAPTestConfigSerializer', 'LDAPUserSerializer', 'LDAPTestLoginSerializer'
-]
-
-
-class LDAPTestConfigSerializer(serializers.Serializer):
- AUTH_LDAP_SERVER_URI = serializers.CharField(max_length=1024)
- AUTH_LDAP_BIND_DN = serializers.CharField(max_length=1024, required=False, allow_blank=True)
- AUTH_LDAP_BIND_PASSWORD = serializers.CharField(required=False, allow_blank=True)
- AUTH_LDAP_SEARCH_OU = serializers.CharField()
- AUTH_LDAP_SEARCH_FILTER = serializers.CharField()
- AUTH_LDAP_USER_ATTR_MAP = serializers.CharField()
- AUTH_LDAP_START_TLS = serializers.BooleanField(required=False)
- AUTH_LDAP = serializers.BooleanField(required=False)
-
-
-class LDAPTestLoginSerializer(serializers.Serializer):
- username = serializers.CharField(max_length=1024, required=True)
- password = serializers.CharField(max_length=2014, required=True)
-
-
-class LDAPUserSerializer(serializers.Serializer):
- id = serializers.CharField()
- username = serializers.CharField()
- name = serializers.CharField()
- email = serializers.CharField()
- existing = serializers.BooleanField(read_only=True)
-
diff --git a/apps/settings/serializers/other.py b/apps/settings/serializers/other.py
new file mode 100644
index 000000000..f8ebaeceb
--- /dev/null
+++ b/apps/settings/serializers/other.py
@@ -0,0 +1,28 @@
+from abc import ABCMeta
+
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+
+class OtherSettingSerializer(serializers.Serializer):
+ EMAIL_SUFFIX = serializers.CharField(
+ required=False, max_length=1024, label=_("Email suffix"),
+ help_text=_('This is used by default if no email is returned during SSO authentication')
+ )
+ TICKETS_ENABLED = serializers.BooleanField(required=False, default=True, label=_("Enable tickets"))
+
+ OTP_ISSUER_NAME = serializers.CharField(
+ required=False, max_length=1024, label=_('OTP issuer name'),
+ )
+ OTP_VALID_WINDOW = serializers.IntegerField(label=_("OTP valid window"))
+
+ PERIOD_TASK_ENABLED = serializers.BooleanField(required=False, label=_("Enable period task"))
+ WINDOWS_SSH_DEFAULT_SHELL = serializers.CharField(
+ required=False, max_length=1024, label=_('Ansible windows default shell')
+ )
+
+ PERM_SINGLE_ASSET_TO_UNGROUP_NODE = serializers.BooleanField(
+ required=False, label=_("Perm single to ungroup node")
+ )
+
+
diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py
new file mode 100644
index 000000000..5749a5c8f
--- /dev/null
+++ b/apps/settings/serializers/security.py
@@ -0,0 +1,115 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+
+class SecurityPasswordRuleSerializer(serializers.Serializer):
+ SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(
+ min_value=6, max_value=30, required=True,
+ label=_('Password minimum length')
+ )
+ SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH = serializers.IntegerField(
+ min_value=6, max_value=30, required=True,
+ label=_('Admin user password minimum length')
+ )
+ SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField(
+ required=False, label=_('Must contain capital')
+ )
+ SECURITY_PASSWORD_LOWER_CASE = serializers.BooleanField(required=False, label=_('Must contain lowercase'))
+ SECURITY_PASSWORD_NUMBER = serializers.BooleanField(required=False, label=_('Must contain numeric'))
+ SECURITY_PASSWORD_SPECIAL_CHAR = serializers.BooleanField(required=False, label=_('Must contain special'))
+
+
+class SecurityAuthSerializer(serializers.Serializer):
+ SECURITY_MFA_AUTH = serializers.ChoiceField(
+ choices=(
+ [0, _('Disable')],
+ [1, _('All users')],
+ [2, _('Only admin users')],
+ ),
+ required=False, label=_("Global MFA auth")
+ )
+ SECURITY_LOGIN_LIMIT_COUNT = serializers.IntegerField(
+ min_value=3, max_value=99999,
+ label=_('Limit the number of login failures')
+ )
+ SECURITY_LOGIN_LIMIT_TIME = serializers.IntegerField(
+ min_value=5, max_value=99999, required=True,
+ label=_('Block logon interval'),
+ help_text=_(
+ 'Unit: minute, If the user has failed to log in for a limited number of times, '
+ 'no login is allowed during this time interval.'
+ )
+ )
+ SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(
+ min_value=1, max_value=99999, required=True,
+ label=_('User password expiration'),
+ help_text=_(
+ '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'
+ )
+ )
+ OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField(
+ min_value=0, max_value=99999, required=True,
+ label=_('Number of repeated historical passwords'),
+ help_text=_(
+ 'Tip: When the user resets the password, it cannot be '
+ 'the previous n historical passwords of the user'
+ )
+ )
+ USER_LOGIN_SINGLE_MACHINE_ENABLED = serializers.BooleanField(
+ required=False, default=False, label=_("Only single device login"),
+ help_text=_("Next device login, pre login will be logout")
+ )
+ ONLY_ALLOW_EXIST_USER_AUTH = serializers.BooleanField(
+ required=False, default=False, label=_("Only exist user login"),
+ help_text=_("If enable, CAS、OIDC auth will be failed, if user not exist yet")
+ )
+ ONLY_ALLOW_AUTH_FROM_SOURCE = serializers.BooleanField(
+ required=False, default=False, label=_("Only from source login"),
+ help_text=_("If enable, CAS、OIDC auth will be failed, if user not exist yet")
+ )
+ SECURITY_MFA_VERIFY_TTL = serializers.IntegerField(label=_("MFA verify TTL"), help_text=_("Unit: second"))
+ SECURITY_LOGIN_CAPTCHA_ENABLED = serializers.BooleanField(
+ required=False, default=True,
+ label=_("Enable Login captcha")
+ )
+
+
+class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSerializer):
+ SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField(
+ required=True, label=_('Enable terminal register'),
+ help_text=_("Allow terminal register, after all terminal setup, you should disable this for security")
+ )
+ SECURITY_WATERMARK_ENABLED = serializers.BooleanField(
+ required=True, label=_('Replay watermark'),
+ help_text=_('Enabled, the session replay contains watermark information')
+ )
+ SECURITY_MAX_IDLE_TIME = serializers.IntegerField(
+ min_value=1, max_value=99999, required=False,
+ label=_('Connection max idle time'),
+ help_text=_('If idle time more than it, disconnect connection Unit: minute')
+ )
+ SECURITY_LUNA_REMEMBER_AUTH = serializers.BooleanField(
+ label=_("Remember manual auth")
+ )
+ CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = serializers.BooleanField(
+ label=_("Enable change auth secure mode")
+ )
+ SECURITY_INSECURE_COMMAND = serializers.BooleanField(
+ required=False, label=_('Insecure command alert')
+ )
+ SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER = serializers.CharField(
+ max_length=8192, required=False, allow_blank=True, label=_('Email recipient'),
+ help_text=_('Multiple user using , split')
+ )
+ SECURITY_COMMAND_EXECUTION = serializers.BooleanField(
+ required=False, label=_('Batch command execution'),
+ help_text=_('Allow user run batch command or not using ansible')
+ )
+ SECURITY_SESSION_SHARE = serializers.BooleanField(
+ required=True, label=_('Session share'),
+ help_text=_("Enabled, Allows user active session to be shared with other users")
+ )
+
+
diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py
index 6c0dadb14..7baa19196 100644
--- a/apps/settings/serializers/settings.py
+++ b/apps/settings/serializers/settings.py
@@ -1,246 +1,42 @@
# coding: utf-8
-from django.utils.translation import ugettext_lazy as _
-from rest_framework import serializers
+from .basic import BasicSettingSerializer
+from .other import OtherSettingSerializer
+from .email import EmailSettingSerializer, EmailContentSettingSerializer
+from .auth import (
+ LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer,
+ CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer,
+ WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer,
+ TencentSMSSettingSerializer,
+)
+from .terminal import TerminalSettingSerializer
+from .security import SecuritySettingSerializer
+from .cleaning import CleaningSerializer
+
__all__ = [
- 'BasicSettingSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer',
- 'LDAPSettingSerializer', 'TerminalSettingSerializer', 'SecuritySettingSerializer',
- 'SettingsSerializer', 'WeComSettingSerializer', 'DingTalkSettingSerializer',
- 'FeiShuSettingSerializer',
+ 'SettingsSerializer',
]
-class BasicSettingSerializer(serializers.Serializer):
- SITE_URL = serializers.URLField(
- required=True, label=_("Site url"),
- help_text=_('eg: http://dev.jumpserver.org:8080')
- )
-
- USER_GUIDE_URL = serializers.URLField(
- required=False, allow_blank=True, allow_null=True, label=_("User guide url"),
- help_text=_('User first login update profile done redirect to it')
- )
- FORGOT_PASSWORD_URL = serializers.URLField(
- required=False, allow_blank=True, allow_null=True, label=_("Forgot password url"),
- help_text=_('The forgot password url on login page, If you use '
- 'ldap or cas external authentication, you can set it')
- )
- GLOBAL_ORG_DISPLAY_NAME = serializers.CharField(
- required=False, max_length=1024, allow_blank=True, allow_null=True, label=_("Global organization name"),
- help_text=_('The name of global organization to display')
- )
-
-
-class EmailSettingSerializer(serializers.Serializer):
- # encrypt_fields 现在使用 write_only 来判断了
-
- EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("SMTP host"))
- EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("SMTP port"))
- EMAIL_HOST_USER = serializers.CharField(max_length=128, required=True, label=_("SMTP account"))
- EMAIL_HOST_PASSWORD = serializers.CharField(
- max_length=1024, write_only=True, required=False, label=_("SMTP password"),
- help_text=_("Tips: Some provider use token except password")
- )
- EMAIL_FROM = serializers.CharField(
- max_length=128, allow_blank=True, required=False, label=_('Send user'),
- help_text=_('Tips: Send mail account, default SMTP account as the send account')
- )
- EMAIL_RECIPIENT = serializers.CharField(
- max_length=128, allow_blank=True, required=False, label=_('Test recipient'),
- help_text=_('Tips: Used only as a test mail recipient')
- )
- EMAIL_USE_SSL = serializers.BooleanField(
- required=False, label=_('Use SSL'),
- help_text=_('If SMTP port is 465, may be select')
- )
- EMAIL_USE_TLS = serializers.BooleanField(
- required=False, label=_("Use TLS"),
- help_text=_('If SMTP port is 587, may be select')
- )
- EMAIL_SUBJECT_PREFIX = serializers.CharField(
- max_length=1024, required=True, label=_('Subject prefix')
- )
-
-
-class EmailContentSettingSerializer(serializers.Serializer):
- EMAIL_CUSTOM_USER_CREATED_SUBJECT = serializers.CharField(
- max_length=1024, allow_blank=True, required=False,
- label=_('Create user email subject'),
- help_text=_('Tips: When creating a user, send the subject of the email (eg:Create account successfully)')
- )
- EMAIL_CUSTOM_USER_CREATED_HONORIFIC = serializers.CharField(
- max_length=1024, allow_blank=True, required=False,
- label=_('Create user honorific'),
- help_text=_('Tips: When creating a user, send the honorific of the email (eg:Hello)')
- )
- EMAIL_CUSTOM_USER_CREATED_BODY = serializers.CharField(
- max_length=4096, allow_blank=True, required=False,
- label=_('Create user email content'),
- help_text=_('Tips:When creating a user, send the content of the email')
- )
- EMAIL_CUSTOM_USER_CREATED_SIGNATURE = serializers.CharField(
- max_length=512, allow_blank=True, required=False, label=_('Signature'),
- help_text=_('Tips: Email signature (eg:jumpserver)')
- )
-
-
-class LDAPSettingSerializer(serializers.Serializer):
- # encrypt_fields 现在使用 write_only 来判断了
-
- AUTH_LDAP_SERVER_URI = serializers.CharField(
- required=True, max_length=1024, label=_('LDAP server'), help_text=_('eg: ldap://localhost:389')
- )
- AUTH_LDAP_BIND_DN = serializers.CharField(required=False, max_length=1024, label=_('Bind DN'))
- AUTH_LDAP_BIND_PASSWORD = serializers.CharField(max_length=1024, write_only=True, required=False, label=_('Password'))
- AUTH_LDAP_SEARCH_OU = serializers.CharField(
- max_length=1024, allow_blank=True, required=False, label=_('User OU'),
- help_text=_('Use | split multi OUs')
- )
- AUTH_LDAP_SEARCH_FILTER = serializers.CharField(
- max_length=1024, required=True, label=_('User search filter'),
- help_text=_('Choice may be (cn|uid|sAMAccountName)=%(user)s)')
- )
- AUTH_LDAP_USER_ATTR_MAP = serializers.DictField(
- required=True, label=_('User attr map'),
- help_text=_('User attr map present how to map LDAP user attr to jumpserver, username,name,email is jumpserver attr')
- )
- AUTH_LDAP = serializers.BooleanField(required=False, label=_('Enable LDAP auth'))
-
-
-class TerminalSettingSerializer(serializers.Serializer):
- SORT_BY_CHOICES = (
- ('hostname', _('Hostname')),
- ('ip', _('IP'))
- )
-
- PAGE_SIZE_CHOICES = (
- ('all', _('All')),
- ('auto', _('Auto')),
- ('10', '10'),
- ('15', '15'),
- ('25', '25'),
- ('50', '50'),
- )
- TERMINAL_PASSWORD_AUTH = serializers.BooleanField(required=False, label=_('Password auth'))
- TERMINAL_PUBLIC_KEY_AUTH = serializers.BooleanField(
- required=False, label=_('Public key auth'),
- help_text=_('Tips: If use other auth method, like AD/LDAP, you should disable this to '
- 'avoid being able to log in after deleting')
- )
- TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False, label=_('List sort by'))
- TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False, label=_('List page size'))
- TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(
- min_value=1, max_value=99999, required=True, label=_('Session keep duration'),
- help_text=_('Units: days, Session, record, command will be delete if more than duration, only in database')
- )
- TERMINAL_TELNET_REGEX = serializers.CharField(allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex'))
- TERMINAL_RDP_ADDR = serializers.CharField(
- required=False, label=_("RDP address"),
- max_length=1024,
- allow_blank=True,
- help_text=_('RDP visit address, eg: dev.jumpserver.org:3389')
- )
-
-
-class SecuritySettingSerializer(serializers.Serializer):
- SECURITY_MFA_AUTH = serializers.ChoiceField(
- choices=(
- [0, _('Disable')],
- [1, _('All users')],
- [2, _('Only admin users')],
- ),
- required=False, label=_("Global MFA auth")
- )
- SECURITY_COMMAND_EXECUTION = serializers.BooleanField(
- required=False, label=_('Batch command execution'),
- help_text=_('Allow user run batch command or not using ansible')
- )
- SECURITY_SERVICE_ACCOUNT_REGISTRATION = serializers.BooleanField(
- required=True, label=_('Enable terminal register'),
- help_text=_("Allow terminal register, after all terminal setup, you should disable this for security")
- )
- SECURITY_WATERMARK_ENABLED = serializers.BooleanField(
- required=True, label=_('Replay watermark'),
- help_text=_('Enabled, the session replay contains watermark information')
- )
- SECURITY_LOGIN_LIMIT_COUNT = serializers.IntegerField(
- min_value=3, max_value=99999,
- label=_('Limit the number of login failures')
- )
- SECURITY_LOGIN_LIMIT_TIME = serializers.IntegerField(
- min_value=5, max_value=99999, required=True,
- label=_('Block logon interval'),
- help_text=_('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.')
- )
- SECURITY_MAX_IDLE_TIME = serializers.IntegerField(
- min_value=1, max_value=99999, required=False,
- label=_('Connection max idle time'),
- help_text=_('If idle time more than it, disconnect connection Unit: minute')
- )
- SECURITY_PASSWORD_EXPIRATION_TIME = serializers.IntegerField(
- min_value=1, max_value=99999, required=True,
- label=_('User password expiration'),
- 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')
- )
- OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField(
- min_value=0, max_value=99999, required=True,
- label=_('Number of repeated historical passwords'),
- help_text=_('Tip: When the user resets the password, it cannot be the previous n historical passwords of the user')
- )
- SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField(
- min_value=6, max_value=30, required=True,
- label=_('Password minimum length')
- )
- SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH = serializers.IntegerField(
- min_value=6, max_value=30, required=True,
- label=_('Admin user password minimum length')
- )
- SECURITY_PASSWORD_UPPER_CASE = serializers.BooleanField(
- required=False, label=_('Must contain capital')
- )
- SECURITY_PASSWORD_LOWER_CASE = serializers.BooleanField(required=False, label=_('Must contain lowercase'))
- SECURITY_PASSWORD_NUMBER = serializers.BooleanField(required=False, label=_('Must contain numeric'))
- SECURITY_PASSWORD_SPECIAL_CHAR = serializers.BooleanField(required=False, label=_('Must contain special'))
- SECURITY_INSECURE_COMMAND = serializers.BooleanField(required=False, label=_('Insecure command alert'))
- SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER = serializers.CharField(
- max_length=8192, required=False, allow_blank=True, label=_('Email recipient'),
- help_text=_('Multiple user using , split')
- )
-
-
-class WeComSettingSerializer(serializers.Serializer):
- WECOM_CORPID = serializers.CharField(max_length=256, required=True, label='corpid')
- WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label='agentid')
- WECOM_SECRET = serializers.CharField(max_length=256, required=False, label='secret', write_only=True)
- AUTH_WECOM = serializers.BooleanField(default=False, label=_('Enable WeCom Auth'))
-
-
-class DingTalkSettingSerializer(serializers.Serializer):
- DINGTALK_AGENTID = serializers.CharField(max_length=256, required=True, label='AgentId')
- DINGTALK_APPKEY = serializers.CharField(max_length=256, required=True, label='AppKey')
- DINGTALK_APPSECRET = serializers.CharField(max_length=256, required=False, label='AppSecret', write_only=True)
- AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('Enable DingTalk Auth'))
-
-
-class FeiShuSettingSerializer(serializers.Serializer):
- FEISHU_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID')
- FEISHU_APP_SECRET = serializers.CharField(max_length=256, required=False, label='App Secret', write_only=True)
- AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth'))
-
-
class SettingsSerializer(
BasicSettingSerializer,
- EmailSettingSerializer,
- EmailContentSettingSerializer,
LDAPSettingSerializer,
TerminalSettingSerializer,
SecuritySettingSerializer,
WeComSettingSerializer,
DingTalkSettingSerializer,
FeiShuSettingSerializer,
+ EmailSettingSerializer,
+ EmailContentSettingSerializer,
+ OtherSettingSerializer,
+ OIDCSettingSerializer,
+ KeycloakSettingSerializer,
+ CASSettingSerializer,
+ RadiusSettingSerializer,
+ CleaningSerializer,
+ AlibabaSMSSettingSerializer,
+ TencentSMSSettingSerializer,
):
-
# encrypt_fields 现在使用 write_only 来判断了
pass
-
diff --git a/apps/settings/serializers/sms.py b/apps/settings/serializers/sms.py
new file mode 100644
index 000000000..fa274a52a
--- /dev/null
+++ b/apps/settings/serializers/sms.py
@@ -0,0 +1,7 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+
+class SMSBackendSerializer(serializers.Serializer):
+ name = serializers.CharField(max_length=256, required=True, label=_('Name'))
+ label = serializers.CharField(max_length=256, required=True, label=_('Label'))
diff --git a/apps/settings/serializers/terminal.py b/apps/settings/serializers/terminal.py
new file mode 100644
index 000000000..0410a7517
--- /dev/null
+++ b/apps/settings/serializers/terminal.py
@@ -0,0 +1,42 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+
+class TerminalSettingSerializer(serializers.Serializer):
+ SORT_BY_CHOICES = (
+ ('hostname', _('Hostname')),
+ ('ip', _('IP'))
+ )
+
+ PAGE_SIZE_CHOICES = (
+ ('all', _('All')),
+ ('auto', _('Auto')),
+ ('10', '10'),
+ ('15', '15'),
+ ('25', '25'),
+ ('50', '50'),
+ )
+ TERMINAL_PASSWORD_AUTH = serializers.BooleanField(required=False, label=_('Password auth'))
+ TERMINAL_PUBLIC_KEY_AUTH = serializers.BooleanField(
+ required=False, label=_('Public key auth'),
+ help_text=_('Tips: If use other auth method, like AD/LDAP, you should disable this to '
+ 'avoid being able to log in after deleting')
+ )
+ TERMINAL_ASSET_LIST_SORT_BY = serializers.ChoiceField(SORT_BY_CHOICES, required=False, label=_('List sort by'))
+ TERMINAL_ASSET_LIST_PAGE_SIZE = serializers.ChoiceField(PAGE_SIZE_CHOICES, required=False,
+ label=_('List page size'))
+ TERMINAL_SESSION_KEEP_DURATION = serializers.IntegerField(
+ min_value=1, max_value=99999, required=True, label=_('Session keep duration'),
+ help_text=_('Unit: days, Session, record, command will be delete if more than duration, only in database')
+ )
+ TERMINAL_TELNET_REGEX = serializers.CharField(
+ allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex'),
+ help_text=_("The login success message varies with devices. "
+ "if you cannot log in to the device through Telnet, set this parameter")
+ )
+ TERMINAL_RDP_ADDR = serializers.CharField(
+ required=False, label=_("RDP address"), max_length=1024, allow_blank=True,
+ help_text=_('RDP visit address, eg: dev.jumpserver.org:3389')
+ )
+
+ XRDP_ENABLED = serializers.BooleanField(label=_("Enable XRDP"))
diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py
index bd423611f..22825f4e8 100644
--- a/apps/settings/urls/api_urls.py
+++ b/apps/settings/urls/api_urls.py
@@ -16,6 +16,9 @@ urlpatterns = [
path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'),
path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'),
path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-testing'),
+ path('alibaba/testing/', api.AlibabaSMSTestingAPI.as_view(), name='alibaba-sms-testing'),
+ path('tencent/testing/', api.TencentSMSTestingAPI.as_view(), name='tencent-sms-testing'),
+ path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'),
path('setting/', api.SettingsApi.as_view(), name='settings-setting'),
path('public/', api.PublicSettingApi.as_view(), name='public-setting'),
diff --git a/apps/templates/resource_download.html b/apps/templates/resource_download.html
new file mode 100644
index 000000000..da8242a12
--- /dev/null
+++ b/apps/templates/resource_download.html
@@ -0,0 +1,36 @@
+{% extends '_without_nav_base.html' %}
+{% block body %}
+
+
+
+{% endblock %}
diff --git a/apps/terminal/api/__init__.py b/apps/terminal/api/__init__.py
index e6a3b3885..fec6da11e 100644
--- a/apps/terminal/api/__init__.py
+++ b/apps/terminal/api/__init__.py
@@ -6,3 +6,4 @@ from .command import *
from .task import *
from .storage import *
from .status import *
+from .sharing import *
diff --git a/apps/terminal/api/sharing.py b/apps/terminal/api/sharing.py
new file mode 100644
index 000000000..3fe5ca45c
--- /dev/null
+++ b/apps/terminal/api/sharing.py
@@ -0,0 +1,79 @@
+from rest_framework.exceptions import MethodNotAllowed, ValidationError
+from rest_framework.decorators import action
+from rest_framework.response import Response
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
+from common.permissions import IsAppUser, IsSuperUser
+from common.const.http import PATCH
+from orgs.mixins.api import OrgModelViewSet
+from .. import serializers, models
+
+__all__ = ['SessionSharingViewSet', 'SessionJoinRecordsViewSet']
+
+
+class SessionSharingViewSet(OrgModelViewSet):
+ serializer_class = serializers.SessionSharingSerializer
+ permission_classes = (IsAppUser | IsSuperUser, )
+ search_fields = ('session', 'creator', 'is_active', 'expired_time')
+ filterset_fields = search_fields
+ model = models.SessionSharing
+
+ def get_permissions(self):
+ if self.request.method.lower() in ['post']:
+ self.permission_classes = (IsAppUser,)
+ return super().get_permissions()
+
+ def create(self, request, *args, **kwargs):
+ if not settings.SECURITY_SESSION_SHARE:
+ detail = _('Secure session sharing settings is disabled')
+ raise MethodNotAllowed(self.action, detail=detail)
+ return super().create(request, *args, **kwargs)
+
+ def destroy(self, request, *args, **kwargs):
+ raise MethodNotAllowed(self.action)
+
+
+class SessionJoinRecordsViewSet(OrgModelViewSet):
+ serializer_class = serializers.SessionJoinRecordSerializer
+ permission_classes = (IsAppUser | IsSuperUser, )
+ search_fields = (
+ 'sharing', 'session', 'joiner', 'date_joined', 'date_left',
+ 'login_from', 'is_success', 'is_finished'
+ )
+ filterset_fields = search_fields
+ model = models.SessionJoinRecord
+
+ def get_permissions(self):
+ if self.request.method.lower() in ['post']:
+ self.permission_classes = (IsAppUser,)
+ return super().get_permissions()
+
+ def create(self, request, *args, **kwargs):
+ try:
+ response = super().create(request, *args, **kwargs)
+ except ValidationError as e:
+ error = e.args[0] if e.args else ''
+ response = Response(
+ data={'error': str(error)}, status=e.status_code
+ )
+ return response
+
+ def perform_create(self, serializer):
+ instance = serializer.save()
+ self.can_join(instance)
+
+ @staticmethod
+ def can_join(instance):
+ can_join, reason = instance.can_join()
+ if not can_join:
+ instance.join_failed(reason=reason)
+ raise ValidationError(reason)
+
+ @action(methods=[PATCH], detail=True)
+ def finished(self, request, *args, **kwargs):
+ instance = self.get_object()
+ instance.finished()
+ return Response(data={'msg': 'ok'})
+
+ def destroy(self, request, *args, **kwargs):
+ raise MethodNotAllowed(self.action)
diff --git a/apps/terminal/migrations/0040_sessionjoinrecord_sessionsharing.py b/apps/terminal/migrations/0040_sessionjoinrecord_sessionsharing.py
new file mode 100644
index 000000000..a2e0933a8
--- /dev/null
+++ b/apps/terminal/migrations/0040_sessionjoinrecord_sessionsharing.py
@@ -0,0 +1,59 @@
+# Generated by Django 3.1.12 on 2021-09-07 09:20
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('terminal', '0039_auto_20210805_1552'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='SessionSharing',
+ fields=[
+ ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
+ ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
+ ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
+ ('verify_code', models.CharField(max_length=16, verbose_name='Verify code')),
+ ('is_active', models.BooleanField(db_index=True, default=True, verbose_name='Active')),
+ ('expired_time', models.IntegerField(db_index=True, default=0, verbose_name='Expired time (min)')),
+ ('creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Creator')),
+ ('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.session', verbose_name='Session')),
+ ],
+ options={
+ 'ordering': ('-date_created',),
+ },
+ ),
+ migrations.CreateModel(
+ name='SessionJoinRecord',
+ fields=[
+ ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
+ ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
+ ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
+ ('verify_code', models.CharField(max_length=16, verbose_name='Verify code')),
+ ('date_joined', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Date joined')),
+ ('date_left', models.DateTimeField(db_index=True, null=True, verbose_name='Date left')),
+ ('remote_addr', models.CharField(blank=True, db_index=True, max_length=128, null=True, verbose_name='Remote addr')),
+ ('login_from', models.CharField(choices=[('ST', 'SSH Terminal'), ('RT', 'RDP Terminal'), ('WT', 'Web Terminal')], default='WT', max_length=2, verbose_name='Login from')),
+ ('is_success', models.BooleanField(db_index=True, default=True, verbose_name='Success')),
+ ('reason', models.CharField(blank=True, default='-', max_length=1024, null=True, verbose_name='Reason')),
+ ('is_finished', models.BooleanField(db_index=True, default=False, verbose_name='Finished')),
+ ('joiner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Joiner')),
+ ('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.session', verbose_name='Session')),
+ ('sharing', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.sessionsharing', verbose_name='Session sharing')),
+ ],
+ options={
+ 'ordering': ('-date_joined',),
+ },
+ ),
+ ]
diff --git a/apps/terminal/models/__init__.py b/apps/terminal/models/__init__.py
index 1de5fd31e..ef5c759a6 100644
--- a/apps/terminal/models/__init__.py
+++ b/apps/terminal/models/__init__.py
@@ -4,3 +4,4 @@ from .status import *
from .storage import *
from .task import *
from .terminal import *
+from .sharing import *
diff --git a/apps/terminal/models/sharing.py b/apps/terminal/models/sharing.py
new file mode 100644
index 000000000..46b4eb1e8
--- /dev/null
+++ b/apps/terminal/models/sharing.py
@@ -0,0 +1,124 @@
+from django.db import models
+import datetime
+from common.mixins import CommonModelMixin
+from orgs.mixins.models import OrgModelMixin
+from django.utils.translation import ugettext_lazy as _
+from django.utils import timezone
+from .session import Session
+
+
+__all__ = ['SessionSharing', 'SessionJoinRecord']
+
+
+class SessionSharing(CommonModelMixin, OrgModelMixin):
+ session = models.ForeignKey(
+ 'terminal.Session', on_delete=models.CASCADE, verbose_name=_('Session')
+ )
+ # creator / created_by
+ creator = models.ForeignKey(
+ 'users.User', on_delete=models.CASCADE, blank=True, null=True,
+ verbose_name=_('Creator')
+ )
+ verify_code = models.CharField(max_length=16, verbose_name=_('Verify code'))
+ is_active = models.BooleanField(
+ default=True, verbose_name=_('Active'), db_index=True
+ )
+ expired_time = models.IntegerField(
+ default=0, verbose_name=_('Expired time (min)'), db_index=True
+ )
+
+ class Meta:
+ ordering = ('-date_created', )
+
+ def __str__(self):
+ return 'Creator: {}'.format(self.creator)
+
+ @property
+ def date_expired(self):
+ return self.date_created + datetime.timedelta(minutes=self.expired_time)
+
+ @property
+ def is_expired(self):
+ if timezone.now() > self.date_expired:
+ return False
+ return True
+
+ def can_join(self):
+ if not self.is_active:
+ return False, _('Link not active')
+ if not self.is_expired:
+ return False, _('Link expired')
+ return True, ''
+
+
+class SessionJoinRecord(CommonModelMixin, OrgModelMixin):
+ LOGIN_FROM = Session.LOGIN_FROM
+
+ session = models.ForeignKey(
+ 'terminal.Session', on_delete=models.CASCADE, verbose_name=_('Session')
+ )
+ verify_code = models.CharField(max_length=16, verbose_name=_('Verify code'))
+ sharing = models.ForeignKey(
+ SessionSharing, on_delete=models.CASCADE,
+ verbose_name=_('Session sharing')
+ )
+ joiner = models.ForeignKey(
+ 'users.User', on_delete=models.CASCADE, blank=True, null=True,
+ verbose_name=_('Joiner')
+ )
+ date_joined = models.DateTimeField(
+ auto_now_add=True, verbose_name=_("Date joined"), db_index=True,
+ )
+ date_left = models.DateTimeField(
+ verbose_name=_("Date left"), null=True, db_index=True
+ )
+ remote_addr = models.CharField(
+ max_length=128, verbose_name=_("Remote addr"), blank=True, null=True,
+ db_index=True
+ )
+ login_from = models.CharField(
+ max_length=2, choices=LOGIN_FROM.choices, default="WT",
+ verbose_name=_("Login from")
+ )
+ is_success = models.BooleanField(
+ default=True, db_index=True, verbose_name=_('Success')
+ )
+ reason = models.CharField(
+ max_length=1024, default='-', blank=True, null=True,
+ verbose_name=_('Reason')
+ )
+ is_finished = models.BooleanField(
+ default=False, db_index=True, verbose_name=_('Finished')
+ )
+
+ class Meta:
+ ordering = ('-date_joined', )
+
+ def __str__(self):
+ return 'Joiner: {}'.format(self.joiner)
+
+ @property
+ def joiner_display(self):
+ return str(self.joiner)
+
+ def can_join(self):
+ # sharing
+ sharing_can_join, reason = self.sharing.can_join()
+ if not sharing_can_join:
+ return False, reason
+ # self
+ if self.verify_code != self.sharing.verify_code:
+ return False, _('Invalid verification code')
+ return True, ''
+
+ def join_failed(self, reason):
+ self.is_success = False
+ self.reason = reason[:1024]
+ self.save()
+
+ def finished(self):
+ if self.is_finished:
+ return
+ self.date_left = timezone.now()
+ self.is_finished = True
+ self.save()
diff --git a/apps/terminal/models/terminal.py b/apps/terminal/models/terminal.py
index 4a69c1112..d15010fef 100644
--- a/apps/terminal/models/terminal.py
+++ b/apps/terminal/models/terminal.py
@@ -151,7 +151,8 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model):
configs.update(self.get_replay_storage_setting())
configs.update(self.get_login_title_setting())
configs.update({
- 'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME
+ 'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME,
+ 'SECURITY_SESSION_SHARE': settings.SECURITY_SESSION_SHARE
})
return configs
diff --git a/apps/terminal/notifications.py b/apps/terminal/notifications.py
index e9c83135e..46ac5b18d 100644
--- a/apps/terminal/notifications.py
+++ b/apps/terminal/notifications.py
@@ -81,7 +81,12 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage):
return message
def get_common_msg(self):
- return self._get_message()
+ msg = self._get_message()
+
+ return {
+ 'subject': msg[:80],
+ 'message': msg
+ }
def get_email_msg(self):
command = self.command
@@ -140,9 +145,6 @@ class CommandExecutionAlert(CommandAlertMixin, SystemMessage):
return message
def get_common_msg(self):
- return self._get_message()
-
- def get_email_msg(self):
command = self.command
subject = _("Insecure Web Command Execution Alert: [%(name)s]") % {
diff --git a/apps/terminal/serializers/__init__.py b/apps/terminal/serializers/__init__.py
index f1714dc21..a2a5bbf30 100644
--- a/apps/terminal/serializers/__init__.py
+++ b/apps/terminal/serializers/__init__.py
@@ -4,3 +4,4 @@ from .terminal import *
from .session import *
from .storage import *
from .command import *
+from .sharing import *
diff --git a/apps/terminal/serializers/sharing.py b/apps/terminal/serializers/sharing.py
new file mode 100644
index 000000000..5e8568cf5
--- /dev/null
+++ b/apps/terminal/serializers/sharing.py
@@ -0,0 +1,57 @@
+from rest_framework import serializers
+from django.utils.translation import ugettext_lazy as _
+from orgs.mixins.serializers import OrgResourceModelSerializerMixin
+from common.utils.random import random_string
+from ..models import SessionSharing, SessionJoinRecord
+
+__all__ = ['SessionSharingSerializer', 'SessionJoinRecordSerializer']
+
+
+class SessionSharingSerializer(OrgResourceModelSerializerMixin):
+ class Meta:
+ model = SessionSharing
+ fields_mini = ['id']
+ fields_small = fields_mini + [
+ 'verify_code', 'is_active', 'expired_time', 'created_by',
+ 'date_created', 'date_updated'
+ ]
+ fields_fk = ['session', 'creator']
+ fields = fields_small + fields_fk
+ read_only_fields = ['verify_code']
+
+ def create(self, validated_data):
+ validated_data['verify_code'] = random_string(4)
+ session = validated_data.get('session')
+ if session:
+ validated_data['creator_id'] = session.user_id
+ validated_data['created_by'] = str(session.user)
+ validated_data['org_id'] = session.org_id
+ return super().create(validated_data)
+
+
+class SessionJoinRecordSerializer(OrgResourceModelSerializerMixin):
+ class Meta:
+ model = SessionJoinRecord
+ fields_mini = ['id']
+ fields_small = fields_mini + [
+ 'joiner_display', 'verify_code', 'date_joined', 'date_left',
+ 'remote_addr', 'login_from', 'is_success', 'reason', 'is_finished',
+ 'created_by', 'date_created', 'date_updated'
+ ]
+ fields_fk = ['session', 'sharing', 'joiner']
+ fields = fields_small + fields_fk
+ extra_kwargs = {
+ 'session': {'required': False},
+ 'joiner': {'required': True},
+ 'sharing': {'required': True},
+ 'remote_addr': {'required': True},
+ 'verify_code': {'required': True},
+ 'joiner_display': {'label': _('Joiner')},
+ }
+
+ def create(self, validate_data):
+ sharing = validate_data.get('sharing')
+ if sharing:
+ validate_data['session'] = sharing.session
+ validate_data['org_id'] = sharing.org_id
+ return super().create(validate_data)
diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py
index 6f00c309a..073bceec0 100644
--- a/apps/terminal/serializers/terminal.py
+++ b/apps/terminal/serializers/terminal.py
@@ -99,15 +99,22 @@ class TerminalRegistrationSerializer(serializers.ModelSerializer):
class Meta:
model = Terminal
fields = ['name', 'type', 'comment', 'service_account', 'remote_addr']
- extra_fields = {
- 'remote_addr': {'readonly': True}
+ extra_kwargs = {
+ 'name': {'max_length': 1024},
+ 'remote_addr': {'read_only': True}
}
def is_valid(self, raise_exception=False):
valid = super().is_valid(raise_exception=raise_exception)
if not valid:
return valid
- data = {'name': self.validated_data.get('name')}
+ name = self.validated_data.get('name')
+ if len(name) > 128:
+ self.validated_data['comment'] = name
+ name = '{}...{}'.format(name[:32], name[-32:])
+ self.validated_data['name'] = name
+
+ data = {'name': name}
kwargs = {'data': data}
if self.instance and self.instance.user:
kwargs['instance'] = self.instance.user
diff --git a/apps/terminal/startup.py b/apps/terminal/startup.py
index 1e77c71a0..0626911ef 100644
--- a/apps/terminal/startup.py
+++ b/apps/terminal/startup.py
@@ -15,10 +15,15 @@ __all__ = ['CoreTerminal', 'CeleryTerminal']
class BaseTerminal(object):
def __init__(self, suffix_name, _type):
- self.server_hostname = os.environ.get('SERVER_HOSTNAME') or socket.gethostname()
- self.name = f'[{suffix_name}] {self.server_hostname}'
+ server_hostname = os.environ.get('SERVER_HOSTNAME') or ''
+ hostname = socket.gethostname()
+ if server_hostname:
+ name = f'[{suffix_name}]-{server_hostname}'
+ else:
+ name = f'[{suffix_name}]-{hostname}'
+ self.name = name
self.interval = 30
- self.remote_addr = socket.gethostbyname(socket.gethostname())
+ self.remote_addr = socket.gethostbyname(hostname)
self.type = _type
def start_heartbeat_thread(self):
diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py
index 57fb6eb73..edbf9db23 100644
--- a/apps/terminal/urls/api_urls.py
+++ b/apps/terminal/urls/api_urls.py
@@ -20,6 +20,8 @@ router.register(r'commands', api.CommandViewSet, 'command')
router.register(r'status', api.StatusViewSet, 'status')
router.register(r'replay-storages', api.ReplayStorageViewSet, 'replay-storage')
router.register(r'command-storages', api.CommandStorageViewSet, 'command-storage')
+router.register(r'session-sharings', api.SessionSharingViewSet, 'session-sharing')
+router.register(r'session-join-records', api.SessionJoinRecordsViewSet, 'session-sharing-record')
urlpatterns = [
path('terminal-registrations/', api.TerminalRegistrationApi.as_view(), name='terminal-registration'),
diff --git a/apps/tickets/api/__init__.py b/apps/tickets/api/__init__.py
index a6b5e39c6..a9d31b436 100644
--- a/apps/tickets/api/__init__.py
+++ b/apps/tickets/api/__init__.py
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
#
from .ticket import *
-from .assignee import *
from .comment import *
from .common import *
diff --git a/apps/tickets/api/assignee.py b/apps/tickets/api/assignee.py
deleted file mode 100644
index d95729085..000000000
--- a/apps/tickets/api/assignee.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-from rest_framework import viewsets
-
-from common.permissions import IsValidUser
-from common.exceptions import JMSException
-from users.models import User
-from orgs.models import Organization
-from .. import serializers
-
-
-class AssigneeViewSet(viewsets.ReadOnlyModelViewSet):
- permission_classes = (IsValidUser,)
- serializer_class = serializers.AssigneeSerializer
- filterset_fields = ('id', 'name', 'username', 'email', 'source')
- search_fields = filterset_fields
-
- def get_org(self):
- org_id = self.request.query_params.get('org_id')
- org = Organization.get_instance(org_id)
- if not org:
- error = ('The organization `{}` does not exist'.format(org_id))
- raise JMSException(error)
- return org
-
- def get_queryset(self):
- org = self.get_org()
- queryset = User.get_super_and_org_admins(org=org)
- return queryset
diff --git a/apps/tickets/api/common.py b/apps/tickets/api/common.py
index fe5a5d1e9..2838d23d0 100644
--- a/apps/tickets/api/common.py
+++ b/apps/tickets/api/common.py
@@ -15,16 +15,16 @@ class GenericTicketStatusRetrieveCloseAPI(RetrieveDestroyAPIView):
permission_classes = (IsAppUser, )
def retrieve(self, request, *args, **kwargs):
- if self.ticket.action_open:
+ if self.ticket.state_open:
status = 'await'
- elif self.ticket.action_approve:
- status = 'approve'
+ elif self.ticket.state_approve:
+ status = 'approved'
else:
- status = 'reject'
+ status = 'rejected'
data = {
'status': status,
- 'action': self.ticket.action,
- 'processor': self.ticket.processor_display
+ 'action': self.ticket.state,
+ 'processor': str(self.ticket.processor)
}
return Response(data=data, status=200)
@@ -32,9 +32,9 @@ class GenericTicketStatusRetrieveCloseAPI(RetrieveDestroyAPIView):
if self.ticket.status_open:
self.ticket.close(processor=self.ticket.applicant)
data = {
- 'action': self.ticket.action,
+ 'action': self.ticket.state,
'status': self.ticket.status,
- 'processor': self.ticket.processor_display
+ 'processor': str(self.ticket.processor)
}
return Response(data=data, status=200)
diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py
index 086daef4d..cbbe72151 100644
--- a/apps/tickets/api/ticket.py
+++ b/apps/tickets/api/ticket.py
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
#
-from django.utils.translation import ugettext_lazy as _
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed
@@ -8,14 +7,15 @@ from rest_framework.response import Response
from common.const.http import POST, PUT
from common.mixins.api import CommonApiMixin
-from common.permissions import IsValidUser, IsOrgAdmin
+from common.permissions import IsValidUser, IsOrgAdmin, IsSuperUser
+from common.drf.api import JMSBulkModelViewSet
from tickets import serializers
-from tickets.models import Ticket
-from tickets.permissions.ticket import IsAssignee, IsAssigneeOrApplicant, NotClosed
+from tickets.models import Ticket, TicketFlow
+from tickets.filters import TicketFilter
+from tickets.permissions.ticket import IsAssignee, IsApplicant
-
-__all__ = ['TicketViewSet']
+__all__ = ['TicketViewSet', 'TicketFlowViewSet']
class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
@@ -25,12 +25,9 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
'open': serializers.TicketApplySerializer,
'approve': serializers.TicketApproveSerializer,
}
- filterset_fields = [
- 'id', 'title', 'type', 'action', 'status', 'applicant', 'applicant_display', 'processor',
- 'processor_display', 'assignees__id'
- ]
+ filterset_class = TicketFilter
search_fields = [
- 'title', 'action', 'type', 'status', 'applicant_display', 'processor_display'
+ 'title', 'action', 'type', 'status', 'applicant_display'
]
def create(self, request, *args, **kwargs):
@@ -48,6 +45,8 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
def perform_create(self, serializer):
instance = serializer.save()
+ instance.create_related_node()
+ instance.process_map = instance.create_process_map()
instance.open(applicant=self.request.user)
@action(detail=False, methods=[POST], permission_classes=[IsValidUser, ])
@@ -57,24 +56,46 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
@action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ])
def approve(self, request, *args, **kwargs):
instance = self.get_object()
- if instance.status_closed:
- return Response(data={"error": _("Ticket already closed")}, status=400)
- response = super().update(request, *args, **kwargs)
- self.get_object().approve(processor=self.request.user)
- return response
+ serializer = self.get_serializer(instance)
+ instance.approve(processor=request.user)
+ return Response(serializer.data)
@action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ])
def reject(self, request, *args, **kwargs):
instance = self.get_object()
- if instance.status_closed:
- return Response(data={"error": _("Ticket already closed")}, status=400)
serializer = self.get_serializer(instance)
instance.reject(processor=request.user)
return Response(serializer.data)
- @action(detail=True, methods=[PUT], permission_classes=[IsAssigneeOrApplicant, NotClosed])
+ @action(detail=True, methods=[PUT], permission_classes=[IsApplicant, ])
def close(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
instance.close(processor=request.user)
return Response(serializer.data)
+
+
+class TicketFlowViewSet(JMSBulkModelViewSet):
+ permission_classes = (IsOrgAdmin, IsSuperUser)
+ serializer_class = serializers.TicketFlowSerializer
+
+ filterset_fields = ['id', 'type']
+ search_fields = ['id', 'type']
+
+ def destroy(self, request, *args, **kwargs):
+ raise MethodNotAllowed(self.action)
+
+ def get_queryset(self):
+ queryset = TicketFlow.get_org_related_flows()
+ return queryset
+
+ def perform_create_or_update(self, serializer):
+ instance = serializer.save()
+ instance.save()
+ instance.rules.model.change_assignees_display(instance.rules.all())
+
+ def perform_create(self, serializer):
+ self.perform_create_or_update(serializer)
+
+ def perform_update(self, serializer):
+ self.perform_create_or_update(serializer)
diff --git a/apps/tickets/const.py b/apps/tickets/const.py
index 3397353d4..1d76ae487 100644
--- a/apps/tickets/const.py
+++ b/apps/tickets/const.py
@@ -1,10 +1,10 @@
-from django.db.models import TextChoices
+from django.db.models import TextChoices, IntegerChoices
from django.utils.translation import ugettext_lazy as _
TICKET_DETAIL_URL = '/ui/#/tickets/tickets/{id}'
-class TicketTypeChoices(TextChoices):
+class TicketType(TextChoices):
general = 'general', _("General")
login_confirm = 'login_confirm', _("Login confirm")
apply_asset = 'apply_asset', _('Apply for asset')
@@ -13,13 +13,38 @@ class TicketTypeChoices(TextChoices):
command_confirm = 'command_confirm', _('Command confirm')
-class TicketActionChoices(TextChoices):
+class TicketState(TextChoices):
open = 'open', _('Open')
- approve = 'approve', _('Approve')
- reject = 'reject', _('Reject')
- close = 'close', _('Close')
+ approved = 'approved', _('Approved')
+ rejected = 'rejected', _('Rejected')
+ closed = 'closed', _('Closed')
-class TicketStatusChoices(TextChoices):
+class ProcessStatus(TextChoices):
+ notified = 'notified', _('Notified')
+ approved = 'approved', _('Approved')
+ rejected = 'rejected', _('Rejected')
+
+
+class TicketStatus(TextChoices):
open = 'open', _("Open")
closed = 'closed', _("Closed")
+
+
+class TicketAction(TextChoices):
+ open = 'open', _("Open")
+ close = 'close', _("Close")
+ approve = 'approve', _('Approve')
+ reject = 'reject', _('Reject')
+
+
+class TicketApprovalLevel(IntegerChoices):
+ one = 1, _("One level")
+ two = 2, _("Two level")
+
+
+class TicketApprovalStrategy(TextChoices):
+ super_admin = 'super_admin', _("Super admin")
+ org_admin = 'org_admin', _("Org admin")
+ super_org_admin = 'super_org_admin', _("Super admin and org admin")
+ custom_user = 'custom_user', _("Custom user")
diff --git a/apps/tickets/errors.py b/apps/tickets/errors.py
new file mode 100644
index 000000000..716eeca94
--- /dev/null
+++ b/apps/tickets/errors.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+#
+from django.utils.translation import ugettext_lazy as _
+
+from common.exceptions import JMSException
+
+
+class AlreadyClosed(JMSException):
+ default_detail = _("Ticket already closed")
diff --git a/apps/tickets/filters.py b/apps/tickets/filters.py
new file mode 100644
index 000000000..676efbea2
--- /dev/null
+++ b/apps/tickets/filters.py
@@ -0,0 +1,18 @@
+from django_filters import rest_framework as filters
+from common.drf.filters import BaseFilterSet
+
+from tickets.models import Ticket
+
+
+class TicketFilter(BaseFilterSet):
+ assignees__id = filters.UUIDFilter(method='filter_assignees_id')
+
+ class Meta:
+ model = Ticket
+ fields = (
+ 'id', 'title', 'type', 'status', 'applicant', 'assignees__id',
+ 'applicant_display',
+ )
+
+ def filter_assignees_id(self, queryset, name, value):
+ return queryset.filter(ticket_steps__ticket_assignees__assignee__id=value)
diff --git a/apps/tickets/handler/apply_application.py b/apps/tickets/handler/apply_application.py
index f4643b5c6..8d03a995d 100644
--- a/apps/tickets/handler/apply_application.py
+++ b/apps/tickets/handler/apply_application.py
@@ -1,17 +1,19 @@
from django.utils.translation import ugettext as _
from orgs.utils import tmp_to_org, tmp_to_root_org
-from applications.models import Application
from applications.const import AppCategory, AppType
-from assets.models import SystemUser
+from applications.models import Application
from perms.models import ApplicationPermission
+from assets.models import SystemUser
+
from .base import BaseHandler
class Handler(BaseHandler):
def _on_approve(self):
- super()._on_approve()
- self._create_application_permission()
+ is_finished = super()._on_approve()
+ if is_finished:
+ self._create_application_permission()
# display
def _construct_meta_display_of_open(self):
@@ -22,27 +24,21 @@ class Handler(BaseHandler):
apply_type_display = AppType.get_label(apply_type)
meta_display_values = [apply_category_display, apply_type_display]
meta_display = dict(zip(meta_display_fields, meta_display_values))
- return meta_display
+ apply_system_users = self.ticket.meta.get('apply_system_users')
+ apply_applications = self.ticket.meta.get('apply_applications')
+ meta_display.update({
+ 'apply_system_users_display': [str(i) for i in SystemUser.objects.filter(id__in=apply_system_users)],
+ 'apply_applications_display': [str(i) for i in Application.objects.filter(id__in=apply_applications)]
+ })
- def _construct_meta_display_of_approve(self):
- meta_display_fields = ['approve_applications_display', 'approve_system_users_display']
- approve_application_ids = self.ticket.meta.get('approve_applications', [])
- approve_system_user_ids = self.ticket.meta.get('approve_system_users', [])
- with tmp_to_org(self.ticket.org_id):
- approve_applications = Application.objects.filter(id__in=approve_application_ids)
- system_users = SystemUser.objects.filter(id__in=approve_system_user_ids)
- approve_applications_display = [str(application) for application in approve_applications]
- approve_system_users_display = [str(system_user) for system_user in system_users]
- meta_display_values = [approve_applications_display, approve_system_users_display]
- meta_display = dict(zip(meta_display_fields, meta_display_values))
return meta_display
# body
def _construct_meta_body_of_open(self):
apply_category_display = self.ticket.meta.get('apply_category_display')
apply_type_display = self.ticket.meta.get('apply_type_display')
- apply_application_group = self.ticket.meta.get('apply_application_group', [])
- apply_system_user_group = self.ticket.meta.get('apply_system_user_group', [])
+ apply_applications = self.ticket.meta.get('apply_applications', [])
+ apply_system_users = self.ticket.meta.get('apply_system_users', [])
apply_date_start = self.ticket.meta.get('apply_date_start')
apply_date_expired = self.ticket.meta.get('apply_date_expired')
applied_body = '''{}: {},
@@ -54,31 +50,13 @@ class Handler(BaseHandler):
'''.format(
_('Applied category'), apply_category_display,
_('Applied type'), apply_type_display,
- _('Applied application group'), apply_application_group,
- _('Applied system user group'), apply_system_user_group,
+ _('Applied application group'), apply_applications,
+ _('Applied system user group'), apply_system_users,
_('Applied date start'), apply_date_start,
_('Applied date expired'), apply_date_expired,
)
return applied_body
- def _construct_meta_body_of_approve(self):
- # 审批信息
- approve_applications_display = self.ticket.meta.get('approve_applications_display', [])
- approve_system_users_display = self.ticket.meta.get('approve_system_users_display', [])
- approve_date_start = self.ticket.meta.get('approve_date_start')
- approve_date_expired = self.ticket.meta.get('approve_date_expired')
- approved_body = '''{}: {},
- {}: {},
- {}: {},
- {}: {},
- '''.format(
- _('Approved applications'), approve_applications_display,
- _('Approved system users'), approve_system_users_display,
- _('Approved date start'), approve_date_start,
- _('Approved date expired'), approve_date_expired
- )
- return approved_body
-
# permission
def _create_application_permission(self):
with tmp_to_root_org():
@@ -88,11 +66,11 @@ class Handler(BaseHandler):
apply_category = self.ticket.meta.get('apply_category')
apply_type = self.ticket.meta.get('apply_type')
- approve_permission_name = self.ticket.meta.get('approve_permission_name', '')
- approved_application_ids = self.ticket.meta.get('approve_applications', [])
- approve_system_user_ids = self.ticket.meta.get('approve_system_users', [])
- approve_date_start = self.ticket.meta.get('approve_date_start')
- approve_date_expired = self.ticket.meta.get('approve_date_expired')
+ apply_permission_name = self.ticket.meta.get('apply_permission_name', '')
+ apply_applications = self.ticket.meta.get('apply_applications', [])
+ apply_system_users = self.ticket.meta.get('apply_system_users', [])
+ apply_date_start = self.ticket.meta.get('apply_date_start')
+ apply_date_expired = self.ticket.meta.get('apply_date_expired')
permission_created_by = '{}:{}'.format(
str(self.ticket.__class__.__name__), str(self.ticket.id)
)
@@ -105,23 +83,24 @@ class Handler(BaseHandler):
).format(
self.ticket.title,
self.ticket.applicant_display,
- self.ticket.processor_display,
+ str(self.ticket.processor),
str(self.ticket.id)
)
permissions_data = {
'id': self.ticket.id,
- 'name': approve_permission_name,
+ 'name': apply_permission_name,
+ 'from_ticket': True,
'category': apply_category,
'type': apply_type,
'comment': str(permission_comment),
'created_by': permission_created_by,
- 'date_start': approve_date_start,
- 'date_expired': approve_date_expired,
+ 'date_start': apply_date_start,
+ 'date_expired': apply_date_expired,
}
with tmp_to_org(self.ticket.org_id):
application_permission = ApplicationPermission.objects.create(**permissions_data)
application_permission.users.add(self.ticket.applicant)
- application_permission.applications.set(approved_application_ids)
- application_permission.system_users.set(approve_system_user_ids)
+ application_permission.applications.set(apply_applications)
+ application_permission.system_users.set(apply_system_users)
return application_permission
diff --git a/apps/tickets/handler/apply_asset.py b/apps/tickets/handler/apply_asset.py
index 9af310294..ae8ded3b1 100644
--- a/apps/tickets/handler/apply_asset.py
+++ b/apps/tickets/handler/apply_asset.py
@@ -1,16 +1,19 @@
+from assets.models import Asset
+from assets.models import SystemUser
+
from .base import BaseHandler
from django.utils.translation import ugettext as _
from perms.models import AssetPermission, Action
-from assets.models import Asset, SystemUser
from orgs.utils import tmp_to_org, tmp_to_root_org
class Handler(BaseHandler):
def _on_approve(self):
- super()._on_approve()
- self._create_asset_permission()
+ is_finished = super()._on_approve()
+ if is_finished:
+ self._create_asset_permission()
# display
def _construct_meta_display_of_open(self):
@@ -19,32 +22,18 @@ class Handler(BaseHandler):
apply_actions_display = Action.value_to_choices_display(apply_actions)
meta_display_values = [apply_actions_display]
meta_display = dict(zip(meta_display_fields, meta_display_values))
- return meta_display
-
- def _construct_meta_display_of_approve(self):
- meta_display_fields = [
- 'approve_actions_display', 'approve_assets_display', 'approve_system_users_display'
- ]
- approve_actions = self.ticket.meta.get('approve_actions', Action.NONE)
- approve_actions_display = Action.value_to_choices_display(approve_actions)
- approve_asset_ids = self.ticket.meta.get('approve_assets', [])
- approve_system_user_ids = self.ticket.meta.get('approve_system_users', [])
- with tmp_to_org(self.ticket.org_id):
- assets = Asset.objects.filter(id__in=approve_asset_ids)
- system_users = SystemUser.objects.filter(id__in=approve_system_user_ids)
- approve_assets_display = [str(asset) for asset in assets]
- approve_system_users_display = [str(system_user) for system_user in system_users]
- meta_display_values = [
- approve_actions_display, approve_assets_display, approve_system_users_display
- ]
- meta_display = dict(zip(meta_display_fields, meta_display_values))
+ apply_assets = self.ticket.meta.get('apply_assets')
+ apply_system_users = self.ticket.meta.get('apply_system_users')
+ meta_display.update({
+ 'apply_assets_display': [str(i) for i in Asset.objects.filter(id__in=apply_assets)],
+ 'apply_system_users_display': [str(i)for i in SystemUser.objects.filter(id__in=apply_system_users)]
+ })
return meta_display
# body
def _construct_meta_body_of_open(self):
- apply_ip_group = self.ticket.meta.get('apply_ip_group', [])
- apply_hostname_group = self.ticket.meta.get('apply_hostname_group', [])
- apply_system_user_group = self.ticket.meta.get('apply_system_user_group', [])
+ apply_assets = self.ticket.meta.get('apply_assets', [])
+ apply_system_users = self.ticket.meta.get('apply_system_users', [])
apply_actions_display = self.ticket.meta.get('apply_actions_display', [])
apply_date_start = self.ticket.meta.get('apply_date_start')
apply_date_expired = self.ticket.meta.get('apply_date_expired')
@@ -54,35 +43,14 @@ class Handler(BaseHandler):
{}: {},
{}: {}
'''.format(
- _('Applied IP group'), apply_ip_group,
- _("Applied hostname group"), apply_hostname_group,
- _("Applied system user group"), apply_system_user_group,
+ _("Applied hostname group"), apply_assets,
+ _("Applied system user group"), apply_system_users,
_("Applied actions"), apply_actions_display,
_('Applied date start'), apply_date_start,
_('Applied date expired'), apply_date_expired,
)
return applied_body
- def _construct_meta_body_of_approve(self):
- approve_assets_display = self.ticket.meta.get('approve_assets_display', [])
- approve_system_users_display = self.ticket.meta.get('approve_system_users_display', [])
- approve_actions_display = self.ticket.meta.get('approve_actions_display', [])
- approve_date_start = self.ticket.meta.get('approve_date_start')
- approve_date_expired = self.ticket.meta.get('approve_date_expired')
- approved_body = '''{}: {},
- {}: {},
- {}: {},
- {}: {},
- {}: {}
- '''.format(
- _('Approved assets'), approve_assets_display,
- _('Approved system users'), approve_system_users_display,
- _('Approved actions'), ', '.join(approve_actions_display),
- _('Approved date start'), approve_date_start,
- _('Approved date expired'), approve_date_expired,
- )
- return approved_body
-
# permission
def _create_asset_permission(self):
with tmp_to_root_org():
@@ -90,12 +58,12 @@ class Handler(BaseHandler):
if asset_permission:
return asset_permission
- approve_permission_name = self.ticket.meta.get('approve_permission_name', )
- approve_asset_ids = self.ticket.meta.get('approve_assets', [])
- approve_system_user_ids = self.ticket.meta.get('approve_system_users', [])
- approve_actions = self.ticket.meta.get('approve_actions', Action.NONE)
- approve_date_start = self.ticket.meta.get('approve_date_start')
- approve_date_expired = self.ticket.meta.get('approve_date_expired')
+ apply_permission_name = self.ticket.meta.get('apply_permission_name', )
+ apply_assets = self.ticket.meta.get('apply_assets', [])
+ apply_system_users = self.ticket.meta.get('apply_system_users', [])
+ apply_actions = self.ticket.meta.get('apply_actions', Action.NONE)
+ apply_date_start = self.ticket.meta.get('apply_date_start')
+ apply_date_expired = self.ticket.meta.get('apply_date_expired')
permission_created_by = '{}:{}'.format(
str(self.ticket.__class__.__name__), str(self.ticket.id)
)
@@ -108,23 +76,24 @@ class Handler(BaseHandler):
).format(
self.ticket.title,
self.ticket.applicant_display,
- self.ticket.processor_display,
+ str(self.ticket.processor),
str(self.ticket.id)
)
permission_data = {
'id': self.ticket.id,
- 'name': approve_permission_name,
+ 'name': apply_permission_name,
+ 'from_ticket': True,
'comment': str(permission_comment),
'created_by': permission_created_by,
- 'actions': approve_actions,
- 'date_start': approve_date_start,
- 'date_expired': approve_date_expired,
+ 'actions': apply_actions,
+ 'date_start': apply_date_start,
+ 'date_expired': apply_date_expired,
}
with tmp_to_org(self.ticket.org_id):
asset_permission = AssetPermission.objects.create(**permission_data)
asset_permission.users.add(self.ticket.applicant)
- asset_permission.assets.set(approve_asset_ids)
- asset_permission.system_users.set(approve_system_user_ids)
+ asset_permission.assets.set(apply_assets)
+ asset_permission.system_users.set(apply_system_users)
return asset_permission
diff --git a/apps/tickets/handler/base.py b/apps/tickets/handler/base.py
index 24a35268a..e308d7255 100644
--- a/apps/tickets/handler/base.py
+++ b/apps/tickets/handler/base.py
@@ -3,7 +3,7 @@ from common.utils import get_logger
from tickets.utils import (
send_ticket_processed_mail_to_applicant, send_ticket_applied_mail_to_assignees
)
-
+from tickets.const import TicketAction
logger = get_logger(__name__)
@@ -16,48 +16,72 @@ class BaseHandler(object):
# on action
def _on_open(self):
self.ticket.applicant_display = str(self.ticket.applicant)
- self.ticket.assignees_display = [str(assignee) for assignee in self.ticket.assignees.all()]
meta_display = getattr(self, '_construct_meta_display_of_open', lambda: {})()
self.ticket.meta.update(meta_display)
self.ticket.save()
self._send_applied_mail_to_assignees()
def _on_approve(self):
- meta_display = getattr(self, '_construct_meta_display_of_approve', lambda: {})()
- self.ticket.meta.update(meta_display)
- self.__on_process()
+ if self.ticket.approval_step != len(self.ticket.process_map):
+ self.ticket.approval_step += 1
+ self.ticket.create_related_node()
+ is_finished = False
+ else:
+ self.ticket.set_state_approve()
+ self.ticket.set_status_closed()
+ is_finished = True
+ self._send_applied_mail_to_assignees()
+
+ self.__on_process(self.ticket.processor)
+ return is_finished
def _on_reject(self):
- self.__on_process()
+ self.ticket.set_state_reject()
+ self.ticket.set_status_closed()
+ self.__on_process(self.ticket.processor)
def _on_close(self):
- self.__on_process()
-
- def __on_process(self):
- self.ticket.processor_display = str(self.ticket.processor)
+ self.ticket.set_state_closed()
self.ticket.set_status_closed()
- self._send_processed_mail_to_applicant()
+ self.__on_process(self.ticket.processor)
+
+ def __on_process(self, processor):
+ self._send_processed_mail_to_applicant(processor)
self.ticket.save()
def dispatch(self, action):
- self._create_comment_on_action()
+ processor = self.ticket.processor
+ current_node = self.ticket.current_node.first()
+ self.ticket.process_map[self.ticket.approval_step - 1].update({
+ 'approval_date': str(current_node.date_updated),
+ 'state': current_node.state,
+ 'processor': processor.id if processor else '',
+ 'processor_display': str(processor) if processor else '',
+ })
+ self.ticket.save()
+ self._create_comment_on_action(action)
method = getattr(self, f'_on_{action}', lambda: None)
return method()
# email
def _send_applied_mail_to_assignees(self):
- logger.debug('Send applied email to assignees: {}'.format(self.ticket.assignees_display))
+ assignees = self.ticket.current_node.first().ticket_assignees.all()
+ assignees_display = ', '.join([str(i.assignee) for i in assignees])
+ logger.debug('Send applied email to assignees: {}'.format(assignees_display))
send_ticket_applied_mail_to_assignees(self.ticket)
- def _send_processed_mail_to_applicant(self):
+ def _send_processed_mail_to_applicant(self, processor):
logger.debug('Send processed mail to applicant: {}'.format(self.ticket.applicant_display))
- send_ticket_processed_mail_to_applicant(self.ticket)
+ send_ticket_processed_mail_to_applicant(self.ticket, processor)
# comments
- def _create_comment_on_action(self):
- user = self.ticket.applicant if self.ticket.action_open else self.ticket.processor
+ def _create_comment_on_action(self, action):
+ user = self.ticket.processor
+ # 打开或关闭工单,备注显示是自己,其他是受理人
+ if self.ticket.state_open or self.ticket.state_close:
+ user = self.ticket.applicant
user_display = str(user)
- action_display = self.ticket.get_action_display()
+ action_display = getattr(TicketAction, action).label
data = {
'body': _('{} {} the ticket').format(user_display, action_display),
'user': user,
@@ -85,18 +109,12 @@ class BaseHandler(object):
{}: {},
{}: {},
{}: {},
- {}: {},
- {}: {}
'''.format(
_('Ticket title'), self.ticket.title,
_('Ticket type'), self.ticket.get_type_display(),
_('Ticket status'), self.ticket.get_status_display(),
- _('Ticket action'), self.ticket.get_action_display(),
_('Ticket applicant'), self.ticket.applicant_display,
- _('Ticket assignees'), ', '.join(self.ticket.assignees_display),
)
- if self.ticket.status_closed:
- basic_body += '''{}: {}'''.format(_('Ticket processor'), self.ticket.processor_display)
body = self.body_html_format.format(_("Ticket basic info"), basic_body)
return body
@@ -104,9 +122,6 @@ class BaseHandler(object):
body = ''
open_body = self._base_construct_meta_body_of_open()
body += open_body
- if self.ticket.action_approve:
- approve_body = self._base_construct_meta_body_of_approve()
- body += approve_body
return body
def _base_construct_meta_body_of_open(self):
@@ -115,10 +130,3 @@ class BaseHandler(object):
)()
body = self.body_html_format.format(_('Ticket applied info'), meta_body_of_open)
return body
-
- def _base_construct_meta_body_of_approve(self):
- meta_body_of_approve = getattr(
- self, '_construct_meta_body_of_approve', lambda: _('No content')
- )()
- body = self.body_html_format.format(_('Ticket approved info'), meta_body_of_approve)
- return body
diff --git a/apps/tickets/migrations/0010_auto_20210812_1618.py b/apps/tickets/migrations/0010_auto_20210812_1618.py
new file mode 100644
index 000000000..36e4db4e2
--- /dev/null
+++ b/apps/tickets/migrations/0010_auto_20210812_1618.py
@@ -0,0 +1,233 @@
+# Generated by Django 3.1.6 on 2021-08-12 08:18
+
+import common.db.encoder
+from django.conf import settings
+from django.db import migrations, models, transaction
+import django.db.models.deletion
+import uuid
+
+from tickets.const import TicketType, TicketApprovalStrategy
+
+ticket_assignee_m2m = list()
+
+
+def get_ticket_assignee_m2m_info(apps, schema_editor):
+ ticket_model = apps.get_model("tickets", "Ticket")
+ for i in ticket_model.objects.only('id', 'assignees', 'action', 'created_by'):
+ ticket_assignee_m2m.append((i.id, list(i.assignees.values_list('id', flat=True)), i.action, i.created_by))
+
+
+def update_ticket_process_meta_state_status(apps, schema_editor):
+ ticket_model = apps.get_model("tickets", "Ticket")
+ updates = list()
+ with transaction.atomic():
+ for instance in ticket_model.objects.all():
+ if instance.action == 'open':
+ state = 'notified'
+ elif instance.action == 'approve':
+ state = 'approved'
+ elif instance.action == 'reject':
+ state = 'rejected'
+ else:
+ state = 'closed'
+ instance.process_map = [{
+ 'state': state,
+ 'approval_level': 1,
+ 'approval_date': str(instance.date_updated),
+ 'processor': instance.processor.id if instance.processor else '',
+ 'processor_display': instance.processor_display if instance.processor_display else '',
+ 'assignees': list(instance.assignees.values_list('id', flat=True)) if instance.assignees else [],
+ 'assignees_display': instance.assignees_display if instance.assignees_display else []
+ }, ]
+ instance.state = state
+ instance.meta['apply_assets'] = instance.meta.pop('approve_assets', [])
+ instance.meta['apply_assets_display'] = instance.meta.pop('approve_assets_display', [])
+ instance.meta['apply_actions'] = instance.meta.pop('approve_actions', 0)
+ instance.meta['apply_actions_display'] = instance.meta.pop('approve_actions_display', [])
+ instance.meta['apply_applications'] = instance.meta.pop('approve_applications', [])
+ instance.meta['apply_applications_display'] = instance.meta.pop('approve_applications_display', [])
+ instance.meta['apply_system_users'] = instance.meta.pop('approve_system_users', [])
+ instance.meta['apply_system_users_display'] = instance.meta.pop('approve_system_users_display', [])
+ updates.append(instance)
+ ticket_model.objects.bulk_update(updates, ['process_map', 'state', 'meta', 'status'])
+
+
+def create_step_and_assignee(apps, schema_editor):
+ ticket_step_model = apps.get_model("tickets", "TicketStep")
+ ticket_assignee_model = apps.get_model("tickets", "TicketAssignee")
+ creates = list()
+ with transaction.atomic():
+ for ticket_id, assignees, action, created_by in ticket_assignee_m2m:
+ if action == 'open':
+ state = 'notified'
+ elif action == 'approve':
+ state = 'approved'
+ else:
+ state = 'rejected'
+ step_instance = ticket_step_model.objects.create(ticket_id=ticket_id, state=state, created_by=created_by)
+ for assignee_id in assignees:
+ creates.append(
+ ticket_assignee_model(
+ step=step_instance, assignee_id=assignee_id, state=state, created_by=created_by
+ )
+ )
+ ticket_assignee_model.objects.bulk_create(creates)
+
+
+def create_ticket_flow_and_approval_rule(apps, schema_editor):
+ user_model = apps.get_model("users", "User")
+ org_id = '00000000-0000-0000-0000-000000000000'
+ ticket_flow_model = apps.get_model("tickets", "TicketFlow")
+ approval_rule_model = apps.get_model("tickets", "ApprovalRule")
+ super_user = user_model.objects.filter(role='Admin')
+ assignees_display = ['{0.name}({0.username})'.format(i) for i in super_user]
+ with transaction.atomic():
+ for ticket_type in [TicketType.apply_asset, TicketType.apply_application]:
+ ticket_flow_instance = ticket_flow_model.objects.create(created_by='System', type=ticket_type, org_id=org_id)
+ approval_rule_instance = approval_rule_model.objects.create(strategy=TicketApprovalStrategy.super_admin, assignees_display=assignees_display)
+ approval_rule_instance.assignees.set(list(super_user))
+ ticket_flow_instance.rules.set([approval_rule_instance, ])
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('tickets', '0009_auto_20210426_1720'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ApprovalRule',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
+ ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
+ ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
+ ('level', models.SmallIntegerField(choices=[(1, 'One level'), (2, 'Two level')], default=1,
+ verbose_name='Approve level')),
+ ('strategy', models.CharField(
+ choices=[('super_admin', 'Super admin'), ('org_admin', 'Org admin'), ('super_org_admin', 'Super admin and org admin'),
+ ('custom_user', 'Custom user')],
+ default='super_admin', max_length=64, verbose_name='Approve strategy')),
+ ('assignees_display', models.JSONField(default=list, encoder=common.db.encoder.ModelJSONFieldEncoder,
+ verbose_name='Assignees display')),
+ ('assignees',
+ models.ManyToManyField(related_name='assigned_ticket_flow_approval_rule', to=settings.AUTH_USER_MODEL,
+ verbose_name='Assignees')),
+ ],
+ options={
+ 'verbose_name': 'Ticket flow approval rule',
+ },
+ ),
+ migrations.RunPython(get_ticket_assignee_m2m_info),
+ migrations.AddField(
+ model_name='ticket',
+ name='process_map',
+ field=models.JSONField(default=list, encoder=common.db.encoder.ModelJSONFieldEncoder,
+ verbose_name='Process'),
+ ),
+ migrations.AddField(
+ model_name='ticket',
+ name='state',
+ field=models.CharField(
+ choices=[('open', 'Open'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('closed', 'Closed')],
+ default='open', max_length=16, verbose_name='State'),
+ ),
+ migrations.RunPython(update_ticket_process_meta_state_status),
+ migrations.RemoveField(
+ model_name='ticket',
+ name='action',
+ ),
+ migrations.RemoveField(
+ model_name='ticket',
+ name='assignees',
+ ),
+ migrations.RemoveField(
+ model_name='ticket',
+ name='assignees_display',
+ ),
+ migrations.RemoveField(
+ model_name='ticket',
+ name='processor',
+ ),
+ migrations.RemoveField(
+ model_name='ticket',
+ name='processor_display',
+ ),
+ migrations.AddField(
+ model_name='ticket',
+ name='approval_step',
+ field=models.SmallIntegerField(choices=[(1, 'One level'), (2, 'Two level')], default=1,
+ verbose_name='Approval step'),
+ ),
+ migrations.CreateModel(
+ name='TicketStep',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
+ ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
+ ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
+ ('level', models.SmallIntegerField(choices=[(1, 'One level'), (2, 'Two level')], default=1,
+ verbose_name='Approve level')),
+ ('state', models.CharField(
+ choices=[('notified', 'Notified'), ('approved', 'Approved'), ('rejected', 'Rejected')],
+ default='notified', max_length=64)),
+ ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_steps',
+ to='tickets.ticket', verbose_name='Ticket')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='TicketFlow',
+ fields=[
+ ('org_id',
+ models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
+ ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
+ ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
+ ('type', models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'),
+ ('apply_asset', 'Apply for asset'),
+ ('apply_application', 'Apply for application'),
+ ('login_asset_confirm', 'Login asset confirm'),
+ ('command_confirm', 'Command confirm')], default='general',
+ max_length=64, verbose_name='Type')),
+ ('approval_level', models.SmallIntegerField(choices=[(1, 'One level'), (2, 'Two level')], default=1,
+ verbose_name='Approve level')),
+ ('rules', models.ManyToManyField(related_name='ticket_flows', to='tickets.ApprovalRule')),
+ ],
+ options={
+ 'verbose_name': 'Ticket flow',
+ },
+ ),
+ migrations.CreateModel(
+ name='TicketAssignee',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
+ ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
+ ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
+ ('state', models.CharField(
+ choices=[('notified', 'Notified'), ('approved', 'Approved'), ('rejected', 'Rejected')],
+ default='notified', max_length=64)),
+ ('assignee',
+ models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_assignees',
+ to=settings.AUTH_USER_MODEL, verbose_name='Assignee')),
+ ('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_assignees',
+ to='tickets.ticketstep')),
+ ],
+ options={
+ 'verbose_name': 'Ticket assignee',
+ },
+ ),
+ migrations.RunPython(create_step_and_assignee),
+ migrations.RunPython(create_ticket_flow_and_approval_rule),
+ migrations.AddField(
+ model_name='ticket',
+ name='flow',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets',
+ to='tickets.ticketflow', verbose_name='TicketFlow'),
+ ),
+ ]
diff --git a/apps/tickets/models/__init__.py b/apps/tickets/models/__init__.py
index 4f7ca772b..fd2bd9057 100644
--- a/apps/tickets/models/__init__.py
+++ b/apps/tickets/models/__init__.py
@@ -2,3 +2,5 @@
#
from .ticket import *
from .comment import *
+from .flow import *
+
diff --git a/apps/tickets/models/flow.py b/apps/tickets/models/flow.py
new file mode 100644
index 000000000..9f008f166
--- /dev/null
+++ b/apps/tickets/models/flow.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+#
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from common.mixins.models import CommonModelMixin
+from common.db.encoder import ModelJSONFieldEncoder
+from orgs.mixins.models import OrgModelMixin
+from orgs.utils import tmp_to_root_org
+from ..const import TicketType, TicketApprovalLevel, TicketApprovalStrategy
+from ..signals import post_or_update_change_ticket_flow_approval
+
+__all__ = ['TicketFlow', 'ApprovalRule']
+
+
+class ApprovalRule(CommonModelMixin):
+ level = models.SmallIntegerField(
+ default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices,
+ verbose_name=_('Approve level')
+ )
+ strategy = models.CharField(
+ max_length=64, default=TicketApprovalStrategy.super_admin,
+ choices=TicketApprovalStrategy.choices,
+ verbose_name=_('Approve strategy')
+ )
+ # 受理人列表
+ assignees = models.ManyToManyField(
+ 'users.User', related_name='assigned_ticket_flow_approval_rule',
+ verbose_name=_("Assignees")
+ )
+ assignees_display = models.JSONField(
+ encoder=ModelJSONFieldEncoder, default=list,
+ verbose_name=_('Assignees display')
+ )
+
+ class Meta:
+ verbose_name = _('Ticket flow approval rule')
+
+ def __str__(self):
+ return '{}({})'.format(self.id, self.level)
+
+ @classmethod
+ def change_assignees_display(cls, qs):
+ post_or_update_change_ticket_flow_approval.send(sender=cls, qs=qs)
+
+
+class TicketFlow(CommonModelMixin, OrgModelMixin):
+ type = models.CharField(
+ max_length=64, choices=TicketType.choices,
+ default=TicketType.general, verbose_name=_("Type")
+ )
+ approval_level = models.SmallIntegerField(
+ default=TicketApprovalLevel.one,
+ choices=TicketApprovalLevel.choices,
+ verbose_name=_('Approve level')
+ )
+ rules = models.ManyToManyField(ApprovalRule, related_name='ticket_flows')
+
+ class Meta:
+ verbose_name = _('Ticket flow')
+
+ def __str__(self):
+ return '{}'.format(self.type)
+
+ @classmethod
+ def get_org_related_flows(cls):
+ flows = cls.objects.all()
+ cur_flow_types = flows.values_list('type', flat=True)
+ with tmp_to_root_org():
+ diff_global_flows = cls.objects.exclude(type__in=cur_flow_types)
+ return flows | diff_global_flows
diff --git a/apps/tickets/models/ticket.py b/apps/tickets/models/ticket.py
index 41d23c8b0..9abd29c96 100644
--- a/apps/tickets/models/ticket.py
+++ b/apps/tickets/models/ticket.py
@@ -1,76 +1,78 @@
# -*- coding: utf-8 -*-
#
-import json
-import uuid
-from datetime import datetime
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
-from django.conf import settings
from common.mixins.models import CommonModelMixin
+from common.db.encoder import ModelJSONFieldEncoder
from orgs.mixins.models import OrgModelMixin
from orgs.utils import tmp_to_root_org, tmp_to_org
-from tickets.const import TicketTypeChoices, TicketActionChoices, TicketStatusChoices
+from tickets.const import TicketType, TicketStatus, TicketState, TicketApprovalLevel, ProcessStatus, TicketAction
from tickets.signals import post_change_ticket_action
from tickets.handler import get_ticket_handler
+from tickets.errors import AlreadyClosed
-__all__ = ['Ticket', 'ModelJSONFieldEncoder']
+__all__ = ['Ticket']
-class ModelJSONFieldEncoder(json.JSONEncoder):
- """ 解决一些类型的字段不能序列化的问题 """
- def default(self, obj):
- if isinstance(obj, datetime):
- return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
- if isinstance(obj, uuid.UUID):
- return str(obj)
- if isinstance(obj, type(_("ugettext_lazy"))):
- return str(obj)
- else:
- return super().default(obj)
+class TicketStep(CommonModelMixin):
+ ticket = models.ForeignKey(
+ 'Ticket', related_name='ticket_steps', on_delete=models.CASCADE, verbose_name='Ticket'
+ )
+ level = models.SmallIntegerField(
+ default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices,
+ verbose_name=_('Approve level')
+ )
+ state = models.CharField(choices=ProcessStatus.choices, max_length=64, default=ProcessStatus.notified)
+
+
+class TicketAssignee(CommonModelMixin):
+ assignee = models.ForeignKey(
+ 'users.User', related_name='ticket_assignees', on_delete=models.CASCADE, verbose_name='Assignee'
+ )
+ state = models.CharField(choices=ProcessStatus.choices, max_length=64, default=ProcessStatus.notified)
+ step = models.ForeignKey('tickets.TicketStep', related_name='ticket_assignees', on_delete=models.CASCADE)
+
+ class Meta:
+ verbose_name = _('Ticket assignee')
+
+ def __str__(self):
+ return '{0.assignee.name}({0.assignee.username})_{0.step}'.format(self)
class Ticket(CommonModelMixin, OrgModelMixin):
title = models.CharField(max_length=256, verbose_name=_("Title"))
type = models.CharField(
- max_length=64, choices=TicketTypeChoices.choices,
- default=TicketTypeChoices.general.value, verbose_name=_("Type")
+ max_length=64, choices=TicketType.choices,
+ default=TicketType.general, verbose_name=_("Type")
)
meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta"))
- action = models.CharField(
- choices=TicketActionChoices.choices, max_length=16,
- default=TicketActionChoices.open.value, verbose_name=_("Action")
+ state = models.CharField(
+ max_length=16, choices=TicketState.choices,
+ default=TicketState.open, verbose_name=_("State")
)
status = models.CharField(
- max_length=16, choices=TicketStatusChoices.choices,
- default=TicketStatusChoices.open.value, verbose_name=_("Status")
+ max_length=16, choices=TicketStatus.choices,
+ default=TicketStatus.open, verbose_name=_("Status")
+ )
+ approval_step = models.SmallIntegerField(
+ default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices,
+ verbose_name=_('Approval step')
)
# 申请人
applicant = models.ForeignKey(
'users.User', related_name='applied_tickets', on_delete=models.SET_NULL, null=True,
verbose_name=_("Applicant")
)
- applicant_display = models.CharField(
- max_length=256, default='', verbose_name=_("Applicant display")
- )
- # 处理人
- processor = models.ForeignKey(
- 'users.User', related_name='processed_tickets', on_delete=models.SET_NULL, null=True,
- verbose_name=_("Processor")
- )
- processor_display = models.CharField(
- max_length=256, blank=True, null=True, default='', verbose_name=_("Processor display")
- )
- # 受理人列表
- assignees = models.ManyToManyField(
- 'users.User', related_name='assigned_tickets', verbose_name=_("Assignees")
- )
- assignees_display = models.JSONField(
- encoder=ModelJSONFieldEncoder, default=list, verbose_name=_('Assignees display')
- )
+ applicant_display = models.CharField(max_length=256, default='', verbose_name=_("Applicant display"))
+ process_map = models.JSONField(encoder=ModelJSONFieldEncoder, default=list, verbose_name=_("Process"))
# 评论
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
+ flow = models.ForeignKey(
+ 'TicketFlow', related_name='tickets', on_delete=models.SET_NULL, null=True,
+ verbose_name=_("TicketFlow")
+ )
class Meta:
ordering = ('-date_created',)
@@ -81,77 +83,142 @@ class Ticket(CommonModelMixin, OrgModelMixin):
# type
@property
def type_apply_asset(self):
- return self.type == TicketTypeChoices.apply_asset.value
+ return self.type == TicketType.apply_asset.value
@property
def type_apply_application(self):
- return self.type == TicketTypeChoices.apply_application.value
+ return self.type == TicketType.apply_application.value
@property
def type_login_confirm(self):
- return self.type == TicketTypeChoices.login_confirm.value
+ return self.type == TicketType.login_confirm.value
# status
- @property
- def status_closed(self):
- return self.status == TicketStatusChoices.closed.value
-
@property
def status_open(self):
- return self.status == TicketStatusChoices.open.value
+ return self.status == TicketStatus.open.value
+
+ @property
+ def status_closed(self):
+ return self.status == TicketStatus.closed.value
+
+ @property
+ def state_open(self):
+ return self.state == TicketState.open.value
+
+ @property
+ def state_approve(self):
+ return self.state == TicketState.approved.value
+
+ @property
+ def state_reject(self):
+ return self.state == TicketState.rejected.value
+
+ @property
+ def state_close(self):
+ return self.state == TicketState.closed.value
+
+ @property
+ def current_node(self):
+ return self.ticket_steps.filter(level=self.approval_step)
+
+ @property
+ def processor(self):
+ processor = self.current_node.first().ticket_assignees.exclude(state=ProcessStatus.notified).first()
+ return processor.assignee if processor else None
+
+ def set_state_approve(self):
+ self.state = TicketState.approved
+
+ def set_state_reject(self):
+ self.state = TicketState.rejected
+
+ def set_state_closed(self):
+ self.state = TicketState.closed
def set_status_closed(self):
- self.status = TicketStatusChoices.closed.value
+ self.status = TicketStatus.closed
- # action
- @property
- def action_open(self):
- return self.action == TicketActionChoices.open.value
+ def create_related_node(self):
+ approval_rule = self.get_current_ticket_flow_approve()
+ ticket_step = TicketStep.objects.create(ticket=self, level=self.approval_step)
+ ticket_assignees = []
+ assignees = approval_rule.assignees.all()
+ for assignee in assignees:
+ ticket_assignees.append(TicketAssignee(step=ticket_step, assignee=assignee))
+ TicketAssignee.objects.bulk_create(ticket_assignees)
- @property
- def action_approve(self):
- return self.action == TicketActionChoices.approve.value
+ def create_process_map(self):
+ approval_rules = self.flow.rules.order_by('level')
+ nodes = list()
+ for node in approval_rules:
+ nodes.append(
+ {
+ 'approval_level': node.level,
+ 'state': ProcessStatus.notified,
+ 'assignees': [i for i in node.assignees.values_list('id', flat=True)],
+ 'assignees_display': node.assignees_display
+ }
+ )
+ return nodes
- @property
- def action_reject(self):
- return self.action == TicketActionChoices.reject.value
-
- @property
- def action_close(self):
- return self.action == TicketActionChoices.close.value
+ # TODO 兼容不存在流的工单
+ def create_process_map_and_node(self, assignees):
+ self.process_map = [{
+ 'approval_level': 1,
+ 'state': 'notified',
+ 'assignees': [assignee.id for assignee in assignees],
+ 'assignees_display': [str(assignee) for assignee in assignees]
+ }, ]
+ self.save()
+ ticket_step = TicketStep.objects.create(ticket=self, level=1)
+ ticket_assignees = []
+ for assignee in assignees:
+ ticket_assignees.append(TicketAssignee(step=ticket_step, assignee=assignee))
+ TicketAssignee.objects.bulk_create(ticket_assignees)
# action changed
def open(self, applicant):
self.applicant = applicant
- self._change_action(action=TicketActionChoices.open.value)
+ self._change_action(TicketAction.open)
+
+ def update_current_step_state_and_assignee(self, processor, state):
+ if self.status_closed:
+ raise AlreadyClosed
+ self.state = state
+ current_node = self.current_node
+ current_node.update(state=state)
+ current_node.first().ticket_assignees.filter(assignee=processor).update(state=state)
def approve(self, processor):
- self.processor = processor
- self._change_action(action=TicketActionChoices.approve.value)
+ self.update_current_step_state_and_assignee(processor, TicketState.approved)
+ self._change_action(TicketAction.approve)
def reject(self, processor):
- self.processor = processor
- self._change_action(action=TicketActionChoices.reject.value)
+ self.update_current_step_state_and_assignee(processor, TicketState.rejected)
+ self._change_action(TicketAction.reject)
def close(self, processor):
- self.processor = processor
- self._change_action(action=TicketActionChoices.close.value)
+ self.update_current_step_state_and_assignee(processor, TicketState.closed)
+ self._change_action(TicketAction.close)
def _change_action(self, action):
- self.action = action
self.save()
post_change_ticket_action.send(sender=self.__class__, ticket=self, action=action)
# ticket
def has_assignee(self, assignee):
- return self.assignees.filter(id=assignee.id).exists()
+ return self.ticket_steps.filter(ticket_assignees__assignee=assignee, level=self.approval_step).exists()
@classmethod
def get_user_related_tickets(cls, user):
- queries = Q(applicant=user) | Q(assignees=user)
+ queries = Q(applicant=user) | Q(ticket_steps__ticket_assignees__assignee=user)
tickets = cls.all().filter(queries).distinct()
return tickets
+ def get_current_ticket_flow_approve(self):
+ return self.flow.rules.filter(level=self.approval_step).first()
+
@classmethod
def all(cls):
with tmp_to_root_org():
diff --git a/apps/tickets/permissions/ticket.py b/apps/tickets/permissions/ticket.py
index dbc74e6a9..bd77421a8 100644
--- a/apps/tickets/permissions/ticket.py
+++ b/apps/tickets/permissions/ticket.py
@@ -1,4 +1,3 @@
-
from rest_framework import permissions
@@ -7,12 +6,7 @@ class IsAssignee(permissions.BasePermission):
return obj.has_assignee(request.user)
-class IsAssigneeOrApplicant(IsAssignee):
+class IsApplicant(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
- return super().has_object_permission(request, view, obj) or obj.applicant == request.user
-
-
-class NotClosed(permissions.BasePermission):
- def has_object_permission(self, request, view, obj):
- return not obj.status_closed
+ return obj.applicant == request.user
diff --git a/apps/tickets/serializers/__init__.py b/apps/tickets/serializers/__init__.py
index 6b519ef80..4f7ca772b 100644
--- a/apps/tickets/serializers/__init__.py
+++ b/apps/tickets/serializers/__init__.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
#
from .ticket import *
-from .assignee import *
from .comment import *
diff --git a/apps/tickets/serializers/assignee.py b/apps/tickets/serializers/assignee.py
deleted file mode 100644
index 217c26fa9..000000000
--- a/apps/tickets/serializers/assignee.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from rest_framework import serializers
-
-__all__ = ['AssigneeSerializer']
-
-
-class AssigneeSerializer(serializers.Serializer):
- id = serializers.UUIDField()
- name = serializers.CharField()
- username = serializers.CharField()
diff --git a/apps/tickets/serializers/comment.py b/apps/tickets/serializers/comment.py
index 5bf57180e..6cb8ab9d5 100644
--- a/apps/tickets/serializers/comment.py
+++ b/apps/tickets/serializers/comment.py
@@ -26,7 +26,7 @@ class CommentSerializer(serializers.ModelSerializer):
'body', 'user_display',
'date_created', 'date_updated'
]
- fields_fk = ['ticket', 'user',]
+ fields_fk = ['ticket', 'user', ]
fields = fields_small + fields_fk
read_only_fields = [
'user_display', 'date_created', 'date_updated'
diff --git a/apps/tickets/serializers/ticket/meta/meta.py b/apps/tickets/serializers/ticket/meta/meta.py
index 12b576857..936977dfc 100644
--- a/apps/tickets/serializers/ticket/meta/meta.py
+++ b/apps/tickets/serializers/ticket/meta/meta.py
@@ -1,6 +1,7 @@
from tickets import const
from .ticket_type import (
- apply_asset, apply_application, login_confirm, login_asset_confirm, command_confirm
+ apply_asset, apply_application, login_confirm,
+ login_asset_confirm, command_confirm
)
__all__ = [
@@ -10,35 +11,31 @@ __all__ = [
# ticket action
# -------------
-action_open = const.TicketActionChoices.open.value
-action_approve = const.TicketActionChoices.approve.value
+action_open = const.TicketAction.open.value
+action_approve = const.TicketAction.approve.value
# defines `meta` field dynamic mapping serializers
# ------------------------------------------------
type_serializer_classes_mapping = {
- const.TicketTypeChoices.apply_asset.value: {
- 'default': apply_asset.ApplyAssetSerializer,
- action_open: apply_asset.ApplySerializer,
- action_approve: apply_asset.ApproveSerializer,
+ const.TicketType.apply_asset.value: {
+ 'default': apply_asset.ApplySerializer
},
- const.TicketTypeChoices.apply_application.value: {
- 'default': apply_application.ApplyApplicationSerializer,
- action_open: apply_application.ApplySerializer,
- action_approve: apply_application.ApproveSerializer,
+ const.TicketType.apply_application.value: {
+ 'default': apply_application.ApplySerializer
},
- const.TicketTypeChoices.login_confirm.value: {
+ const.TicketType.login_confirm.value: {
'default': login_confirm.LoginConfirmSerializer,
action_open: login_confirm.ApplySerializer,
action_approve: login_confirm.LoginConfirmSerializer(read_only=True),
},
- const.TicketTypeChoices.login_asset_confirm.value: {
+ const.TicketType.login_asset_confirm.value: {
'default': login_asset_confirm.LoginAssetConfirmSerializer,
action_open: login_asset_confirm.ApplySerializer,
action_approve: login_asset_confirm.LoginAssetConfirmSerializer(read_only=True),
},
- const.TicketTypeChoices.command_confirm.value: {
+ const.TicketType.command_confirm.value: {
'default': command_confirm.CommandConfirmSerializer,
action_open: command_confirm.ApplySerializer,
action_approve: command_confirm.CommandConfirmSerializer(read_only=True)
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py
index 382a8d789..e8217bf2b 100644
--- a/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py
+++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py
@@ -1,20 +1,20 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
-from django.db.models import Q
from perms.models import ApplicationPermission
-from applications.models import Application
from applications.const import AppCategory, AppType
-from assets.models import SystemUser
from orgs.utils import tmp_to_org
from tickets.models import Ticket
from .common import DefaultPermissionName
__all__ = [
- 'ApplyApplicationSerializer', 'ApplySerializer', 'ApproveSerializer',
+ 'ApplySerializer',
]
class ApplySerializer(serializers.Serializer):
+ apply_permission_name = serializers.CharField(
+ max_length=128, default=DefaultPermissionName(), label=_('Apply name')
+ )
# 申请信息
apply_category = serializers.ChoiceField(
required=True, choices=AppCategory.choices, label=_('Category'),
@@ -31,13 +31,23 @@ class ApplySerializer(serializers.Serializer):
required=False, read_only=True, label=_('Type display'),
allow_null=True
)
- apply_application_group = serializers.ListField(
- required=False, child=serializers.CharField(), label=_('Application group'),
- default=list, allow_null=True
+ apply_applications = serializers.ListField(
+ required=True, child=serializers.UUIDField(), label=_('Apply applications'),
+ allow_null=True
)
- apply_system_user_group = serializers.ListField(
- required=False, child=serializers.CharField(), label=_('System user group'),
- default=list, allow_null=True
+ apply_applications_display = serializers.ListField(
+ required=False, read_only=True, child=serializers.CharField(),
+ label=_('Apply applications display'), allow_null=True,
+ default=list
+ )
+ apply_system_users = serializers.ListField(
+ required=True, child=serializers.UUIDField(), label=_('Apply system users'),
+ allow_null=True
+ )
+ apply_system_users_display = serializers.ListField(
+ required=False, read_only=True, child=serializers.CharField(),
+ label=_('Apply system user display'), allow_null=True,
+ default=list
)
apply_date_start = serializers.DateTimeField(
required=True, label=_('Date start'), allow_null=True
@@ -46,37 +56,6 @@ class ApplySerializer(serializers.Serializer):
required=True, label=_('Date expired'), allow_null=True
)
-
-class ApproveSerializer(serializers.Serializer):
- # 审批信息
- approve_permission_name = serializers.CharField(
- max_length=128, default=DefaultPermissionName(), label=_('Permission name')
- )
- approve_applications = serializers.ListField(
- required=True, child=serializers.UUIDField(), label=_('Approve applications'),
- allow_null=True
- )
- approve_applications_display = serializers.ListField(
- required=False, read_only=True, child=serializers.CharField(),
- label=_('Approve applications display'), allow_null=True,
- default=list
- )
- approve_system_users = serializers.ListField(
- required=True, child=serializers.UUIDField(), label=_('Approve system users'),
- allow_null=True
- )
- approve_system_users_display = serializers.ListField(
- required=False, read_only=True, child=serializers.CharField(),
- label=_('Approve system user display'), allow_null=True,
- default=list
- )
- approve_date_start = serializers.DateTimeField(
- required=True, label=_('Date start'), allow_null=True
- )
- approve_date_expired = serializers.DateTimeField(
- required=True, label=_('Date expired'), allow_null=True
- )
-
def validate_approve_permission_name(self, permission_name):
if not isinstance(self.root.instance, Ticket):
return permission_name
@@ -90,83 +69,5 @@ class ApproveSerializer(serializers.Serializer):
'Permission named `{}` already exists'.format(permission_name)
))
- def validate_approve_applications(self, approve_applications):
- if not isinstance(self.root.instance, Ticket):
- return []
-
- with tmp_to_org(self.root.instance.org_id):
- apply_type = self.root.instance.meta.get('apply_type')
- queries = Q(type=apply_type)
- queries &= Q(id__in=approve_applications)
- application_ids = Application.objects.filter(queries).values_list('id', flat=True)
- application_ids = [str(application_id) for application_id in application_ids]
- if application_ids:
- return application_ids
-
- raise serializers.ValidationError(_(
- 'No `Application` are found under Organization `{}`'.format(self.root.instance.org_name)
- ))
-
- def validate_approve_system_users(self, approve_system_users):
- if not isinstance(self.root.instance, Ticket):
- return []
-
- with tmp_to_org(self.root.instance.org_id):
- apply_type = self.root.instance.meta.get('apply_type')
- protocol = SystemUser.get_protocol_by_application_type(apply_type)
- queries = Q(protocol=protocol)
- queries &= Q(id__in=approve_system_users)
- system_user_ids = SystemUser.objects.filter(queries).values_list('id', flat=True)
- system_user_ids = [str(system_user_id) for system_user_id in system_user_ids]
- if system_user_ids:
- return system_user_ids
-
- raise serializers.ValidationError(_(
- 'No `SystemUser` are found under Organization `{}`'.format(self.root.instance.org_name)
- ))
-class ApplyApplicationSerializer(ApplySerializer, ApproveSerializer):
- # 推荐信息
- recommend_applications = serializers.SerializerMethodField()
- recommend_system_users = serializers.SerializerMethodField()
-
- def get_recommend_applications(self, value):
- if not isinstance(self.root.instance, Ticket):
- return []
-
- apply_application_group = value.get('apply_application_group', [])
- if not apply_application_group:
- return []
-
- apply_type = value.get('apply_type')
- queries = Q()
- for application in apply_application_group:
- queries |= Q(name__icontains=application)
- queries &= Q(type=apply_type)
-
- with tmp_to_org(self.root.instance.org_id):
- application_ids = Application.objects.filter(queries).values_list('id', flat=True)[:15]
- application_ids = [str(application_id) for application_id in application_ids]
- return application_ids
-
- def get_recommend_system_users(self, value):
- if not isinstance(self.root.instance, Ticket):
- return []
-
- apply_system_user_group = value.get('apply_system_user_group', [])
- if not apply_system_user_group:
- return []
-
- apply_type = value.get('apply_type')
- protocol = SystemUser.get_protocol_by_application_type(apply_type)
- queries = Q()
- for system_user in apply_system_user_group:
- queries |= Q(username__icontains=system_user)
- queries |= Q(name__icontains=system_user)
- queries &= Q(protocol=protocol)
-
- with tmp_to_org(self.root.instance.org_id):
- system_user_ids = SystemUser.objects.filter(queries).values_list('id', flat=True)[:5]
- system_user_ids = [str(system_user_id) for system_user_id in system_user_ids]
- return system_user_ids
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py
index 2ed9107c8..489ded1a8 100644
--- a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py
+++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py
@@ -1,39 +1,44 @@
from django.utils.translation import ugettext_lazy as _
-from django.db.models import Q
from rest_framework import serializers
from perms.serializers import ActionsField
from perms.models import AssetPermission
-from assets.models import Asset, SystemUser
from orgs.utils import tmp_to_org
from tickets.models import Ticket
from .common import DefaultPermissionName
-
__all__ = [
- 'ApplyAssetSerializer', 'ApplySerializer', 'ApproveSerializer',
+ 'ApplySerializer',
]
class ApplySerializer(serializers.Serializer):
+ apply_permission_name = serializers.CharField(
+ max_length=128, default=DefaultPermissionName(), label=_('Apply name')
+ )
# 申请信息
- apply_ip_group = serializers.ListField(
- required=False, child=serializers.IPAddressField(), label=_('IP group'),
- default=list, allow_null=True,
+ apply_assets = serializers.ListField(
+ required=True, allow_null=True, child=serializers.UUIDField(), label=_('Apply assets')
)
- apply_hostname_group = serializers.ListField(
- required=False, child=serializers.CharField(), label=_('Hostname group'),
- default=list, allow_null=True,
+ apply_assets_display = serializers.ListField(
+ required=False, read_only=True, child=serializers.CharField(),
+ label=_('Approve assets display'), allow_null=True,
+ default=list,
)
- apply_system_user_group = serializers.ListField(
- required=False, child=serializers.CharField(), label=_('System user group'),
- default=list, allow_null=True
+ apply_system_users = serializers.ListField(
+ required=True, allow_null=True, child=serializers.UUIDField(),
+ label=_('Approve system users')
+ )
+ apply_system_users_display = serializers.ListField(
+ required=False, read_only=True, child=serializers.CharField(),
+ label=_('Apply assets display'), allow_null=True,
+ default=list,
)
apply_actions = ActionsField(
required=True, allow_null=True
)
apply_actions_display = serializers.ListField(
required=False, read_only=True, child=serializers.CharField(),
- label=_('Approve assets display'), allow_null=True,
+ label=_('Apply assets display'), allow_null=True,
default=list,
)
apply_date_start = serializers.DateTimeField(
@@ -43,44 +48,6 @@ class ApplySerializer(serializers.Serializer):
required=True, label=_('Date expired'), allow_null=True,
)
-
-class ApproveSerializer(serializers.Serializer):
- # 审批信息
- approve_permission_name = serializers.CharField(
- max_length=128, default=DefaultPermissionName(), label=_('Permission name')
- )
- approve_assets = serializers.ListField(
- required=True, allow_null=True, child=serializers.UUIDField(), label=_('Approve assets')
- )
- approve_assets_display = serializers.ListField(
- required=False, read_only=True, child=serializers.CharField(),
- label=_('Approve assets display'), allow_null=True,
- default=list,
- )
- approve_system_users = serializers.ListField(
- required=True, allow_null=True, child=serializers.UUIDField(),
- label=_('Approve system users')
- )
- approve_system_users_display = serializers.ListField(
- required=False, read_only=True, child=serializers.CharField(),
- label=_('Approve assets display'), allow_null=True,
- default=list,
- )
- approve_actions = ActionsField(
- required=True, allow_null=True,
- )
- approve_actions_display = serializers.ListField(
- required=False, read_only=True, child=serializers.CharField(),
- label=_('Approve assets display'), allow_null=True,
- default=list,
- )
- approve_date_start = serializers.DateTimeField(
- required=True, label=_('Date start'), allow_null=True,
- )
- approve_date_expired = serializers.DateTimeField(
- required=True, label=_('Date expired'), allow_null=True
- )
-
def validate_approve_permission_name(self, permission_name):
if not isinstance(self.root.instance, Ticket):
return permission_name
@@ -93,76 +60,3 @@ class ApproveSerializer(serializers.Serializer):
raise serializers.ValidationError(_(
'Permission named `{}` already exists'.format(permission_name)
))
-
- def validate_approve_assets(self, approve_assets):
- if not isinstance(self.root.instance, Ticket):
- return []
-
- with tmp_to_org(self.root.instance.org_id):
- asset_ids = Asset.objects.filter(id__in=approve_assets).values_list('id', flat=True)
- asset_ids = [str(asset_id) for asset_id in asset_ids]
- if asset_ids:
- return asset_ids
-
- raise serializers.ValidationError(_(
- 'No `Asset` are found under Organization `{}`'.format(self.root.instance.org_name)
- ))
-
- def validate_approve_system_users(self, approve_system_users):
- if not isinstance(self.root.instance, Ticket):
- return []
-
- with tmp_to_org(self.root.instance.org_id):
- queries = Q(protocol__in=SystemUser.ASSET_CATEGORY_PROTOCOLS)
- queries &= Q(id__in=approve_system_users)
- system_user_ids = SystemUser.objects.filter(queries).values_list('id', flat=True)
- system_user_ids = [str(system_user_id) for system_user_id in system_user_ids]
- if system_user_ids:
- return system_user_ids
-
- raise serializers.ValidationError(_(
- 'No `SystemUser` are found under Organization `{}`'.format(self.root.instance.org_name)
- ))
-
-
-class ApplyAssetSerializer(ApplySerializer, ApproveSerializer):
- # 推荐信息
- recommend_assets = serializers.SerializerMethodField()
- recommend_system_users = serializers.SerializerMethodField()
-
- def get_recommend_assets(self, value):
- if not isinstance(self.root.instance, Ticket):
- return []
-
- apply_ip_group = value.get('apply_ip_group', [])
- apply_hostname_group = value.get('apply_hostname_group', [])
- queries = Q()
- if apply_ip_group:
- queries |= Q(ip__in=apply_ip_group)
- for hostname in apply_hostname_group:
- queries |= Q(hostname__icontains=hostname)
- if not queries:
- return []
- with tmp_to_org(self.root.instance.org_id):
- asset_ids = Asset.objects.filter(queries).values_list('id', flat=True)[:100]
- asset_ids = [str(asset_id) for asset_id in asset_ids]
- return asset_ids
-
- def get_recommend_system_users(self, value):
- if not isinstance(self.root.instance, Ticket):
- return []
-
- apply_system_user_group = value.get('apply_system_user_group', [])
- if not apply_system_user_group:
- return []
-
- queries = Q()
- for system_user in apply_system_user_group:
- queries |= Q(username__icontains=system_user)
- queries |= Q(name__icontains=system_user)
- queries &= Q(protocol__in=SystemUser.ASSET_CATEGORY_PROTOCOLS)
-
- with tmp_to_org(self.root.instance.org_id):
- system_user_ids = SystemUser.objects.filter(queries).values_list('id', flat=True)[:5]
- system_user_ids = [str(system_user_id) for system_user_id in system_user_ids]
- return system_user_ids
diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py
index fdf281b73..ca5f6add1 100644
--- a/apps/tickets/serializers/ticket/ticket.py
+++ b/apps/tickets/serializers/ticket/ticket.py
@@ -1,43 +1,38 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
+from django.db.transaction import atomic
from rest_framework import serializers
from common.drf.serializers import MethodSerializer
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
+from perms.models import AssetPermission
from orgs.models import Organization
+from orgs.utils import tmp_to_org
from users.models import User
-from tickets.models import Ticket
+from tickets.models import Ticket, TicketFlow, ApprovalRule
+from tickets.const import TicketApprovalStrategy
from .meta import type_serializer_classes_mapping
-
__all__ = [
- 'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketApproveSerializer',
+ 'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketApproveSerializer', 'TicketFlowSerializer'
]
class TicketSerializer(OrgResourceModelSerializerMixin):
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
- action_display = serializers.ReadOnlyField(
- source='get_action_display', label=_('Action display')
- )
- status_display = serializers.ReadOnlyField(
- source='get_status_display', label=_('Status display')
- )
+ status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display'))
meta = MethodSerializer()
class Meta:
model = Ticket
fields_mini = ['id', 'title']
fields_small = fields_mini + [
- 'type', 'type_display', 'meta', 'body',
- 'action', 'action_display', 'status', 'status_display',
- 'applicant_display', 'processor_display', 'assignees_display',
- 'date_created', 'date_updated',
- 'comment', 'org_id', 'org_name',
+ 'type', 'type_display', 'meta', 'state', 'approval_step',
+ 'status', 'status_display', 'applicant_display', 'process_map',
+ 'date_created', 'date_updated', 'comment', 'org_id', 'org_name', 'body'
]
- fields_fk = ['applicant', 'processor',]
- fields_m2m = ['assignees']
- fields = fields_small + fields_fk + fields_m2m
+ fields_fk = ['applicant', ]
+ fields = fields_small + fields_fk
def get_meta_serializer(self):
default_serializer = serializers.Serializer(read_only=True)
@@ -71,7 +66,6 @@ class TicketSerializer(OrgResourceModelSerializerMixin):
class TicketDisplaySerializer(TicketSerializer):
-
class Meta:
model = Ticket
fields = TicketSerializer.Meta.fields
@@ -87,7 +81,7 @@ class TicketApplySerializer(TicketSerializer):
model = Ticket
fields = TicketSerializer.Meta.fields
writeable_fields = [
- 'id', 'title', 'type', 'meta', 'assignees', 'comment', 'org_id'
+ 'id', 'title', 'type', 'meta', 'comment', 'org_id'
]
read_only_fields = list(set(fields) - set(writeable_fields))
extra_kwargs = {
@@ -112,27 +106,115 @@ class TicketApplySerializer(TicketSerializer):
raise serializers.ValidationError(error)
return org_id
- def validate_assignees(self, assignees):
- org_id = self.initial_data.get('org_id')
- self.validate_org_id(org_id)
- org = Organization.get_instance(org_id)
- admins = User.get_super_and_org_admins(org)
- valid_assignees = list(set(assignees) & set(admins))
- if not valid_assignees:
- error = _('None of the assignees belong to Organization `{}` admins'.format(org.name))
+ def validate(self, attrs):
+ ticket_type = attrs.get('type')
+ flow = TicketFlow.get_org_related_flows().filter(type=ticket_type).first()
+ if flow:
+ attrs['flow'] = flow
+ else:
+ error = _('The ticket flow `{}` does not exist'.format(ticket_type))
raise serializers.ValidationError(error)
- return valid_assignees
+ return attrs
+
+ @atomic
+ def create(self, validated_data):
+ instance = super().create(validated_data)
+ name = _('Created by ticket ({}-{})').format(instance.title, str(instance.id)[:4])
+ with tmp_to_org(instance.org_id):
+ if not AssetPermission.objects.filter(name=name).exists():
+ instance.meta.update({'apply_permission_name': name})
+ return instance
+ raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name)))
class TicketApproveSerializer(TicketSerializer):
+ meta = serializers.ReadOnlyField()
class Meta:
model = Ticket
fields = TicketSerializer.Meta.fields
- writeable_fields = ['meta']
- read_only_fields = list(set(fields) - set(writeable_fields))
+ read_only_fields = fields
- def validate_meta(self, meta):
- _meta = self.instance.meta if self.instance else {}
- _meta.update(meta)
- return _meta
+
+class TicketFlowApproveSerializer(serializers.ModelSerializer):
+ strategy_display = serializers.ReadOnlyField(source='get_strategy_display', label=_('Approve strategy'))
+ assignees_read_only = serializers.SerializerMethodField(label=_("Assignees"))
+
+ class Meta:
+ model = ApprovalRule
+ fields_small = [
+ 'level', 'strategy', 'assignees_read_only', 'assignees_display', 'strategy_display'
+ ]
+ fields_m2m = ['assignees', ]
+ fields = fields_small + fields_m2m
+ read_only_fields = ['level', 'assignees_display']
+ extra_kwargs = {
+ 'assignees': {'write_only': True, 'allow_empty': True}
+ }
+
+ def get_assignees_read_only(self, obj):
+ if obj.strategy == TicketApprovalStrategy.custom_user:
+ return obj.assignees.values_list('id', flat=True)
+ return []
+
+
+class TicketFlowSerializer(OrgResourceModelSerializerMixin):
+ type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
+ rules = TicketFlowApproveSerializer(many=True, required=True)
+
+ class Meta:
+ model = TicketFlow
+ fields_mini = ['id', ]
+ fields_small = fields_mini + [
+ 'type', 'type_display', 'approval_level', 'created_by', 'date_created', 'date_updated',
+ 'org_id', 'org_name'
+ ]
+ fields = fields_small + ['rules', ]
+ read_only_fields = ['created_by', 'org_id', 'date_created', 'date_updated']
+ extra_kwargs = {
+ 'type': {'required': True},
+ 'approval_level': {'required': True}
+ }
+
+ def validate_type(self, value):
+ if not self.instance or (self.instance and self.instance.type != value):
+ if self.Meta.model.objects.filter(type=value).exists():
+ error = _('The current organization type already exists')
+ raise serializers.ValidationError(error)
+ return value
+
+ def create_or_update(self, action, validated_data, related, assignees, instance=None):
+ childs = validated_data.pop(related, [])
+ if not instance:
+ instance = getattr(super(), action)(validated_data)
+ else:
+ instance = getattr(super(), action)(instance, validated_data)
+ getattr(instance, related).all().delete()
+ instance_related = getattr(instance, related)
+ child_instances = []
+ related_model = instance_related.model
+ for level, data in enumerate(childs, 1):
+ data_m2m = data.pop(assignees, None)
+ child_instance = related_model.objects.create(**data, level=level)
+ if child_instance.strategy == TicketApprovalStrategy.super_admin:
+ data_m2m = list(User.get_super_admins())
+ elif child_instance.strategy == TicketApprovalStrategy.org_admin:
+ data_m2m = list(User.get_org_admins())
+ elif child_instance.strategy == TicketApprovalStrategy.super_org_admin:
+ data_m2m = list(User.get_super_and_org_admins())
+ getattr(child_instance, assignees).set(data_m2m)
+ child_instances.append(child_instance)
+ instance_related.set(child_instances)
+ return instance
+
+ @atomic
+ def create(self, validated_data):
+ return self.create_or_update('create', validated_data, 'rules', 'assignees')
+
+ @atomic
+ def update(self, instance, validated_data):
+ if instance.org_id == Organization.ROOT_ID:
+ instance = self.create(validated_data)
+ else:
+ instance = self.create_or_update('update', validated_data, 'rules', 'assignees', instance)
+ return instance
diff --git a/apps/tickets/signals.py b/apps/tickets/signals.py
index 10951716d..b626cfa35 100644
--- a/apps/tickets/signals.py
+++ b/apps/tickets/signals.py
@@ -1,4 +1,5 @@
from django.dispatch import Signal
-
post_change_ticket_action = Signal()
+
+post_or_update_change_ticket_flow_approval = Signal()
diff --git a/apps/tickets/signals_handler/ticket.py b/apps/tickets/signals_handler/ticket.py
index aae620295..2d036f936 100644
--- a/apps/tickets/signals_handler/ticket.py
+++ b/apps/tickets/signals_handler/ticket.py
@@ -3,9 +3,8 @@
from django.dispatch import receiver
from common.utils import get_logger
-from tickets.models import Ticket
-from ..signals import post_change_ticket_action
-
+from tickets.models import Ticket, ApprovalRule
+from ..signals import post_change_ticket_action, post_or_update_change_ticket_flow_approval
logger = get_logger(__name__)
@@ -13,3 +12,12 @@ logger = get_logger(__name__)
@receiver(post_change_ticket_action, sender=Ticket)
def on_post_change_ticket_action(sender, ticket, action, **kwargs):
ticket.handler.dispatch(action)
+
+
+@receiver(post_or_update_change_ticket_flow_approval, sender=ApprovalRule)
+def post_or_update_change_ticket_flow_approval(sender, qs, **kwargs):
+ updates = []
+ for instance in qs:
+ instance.assignees_display = [str(assignee) for assignee in instance.assignees.all()]
+ updates.append(instance)
+ sender.objects.bulk_update(updates, ['assignees_display', ])
diff --git a/apps/tickets/urls/api_urls.py b/apps/tickets/urls/api_urls.py
index 93286f645..ef72013a1 100644
--- a/apps/tickets/urls/api_urls.py
+++ b/apps/tickets/urls/api_urls.py
@@ -8,7 +8,7 @@ app_name = 'tickets'
router = BulkRouter()
router.register('tickets', api.TicketViewSet, 'ticket')
-router.register('assignees', api.AssigneeViewSet, 'assignee')
+router.register('flows', api.TicketFlowViewSet, 'flows')
router.register('comments', api.CommentViewSet, 'comment')
urlpatterns = []
diff --git a/apps/tickets/utils.py b/apps/tickets/utils.py
index 1bd789fda..7999a5e1d 100644
--- a/apps/tickets/utils.py
+++ b/apps/tickets/utils.py
@@ -26,9 +26,10 @@ EMAIL_TEMPLATE = '''
def send_ticket_applied_mail_to_assignees(ticket):
- if not ticket.assignees:
+ assignees = ticket.current_node.first().ticket_assignees.all()
+ if not assignees:
logger.debug("Not found assignees, ticket: {}({}), assignees: {}".format(
- ticket, str(ticket.id), ticket.assignees)
+ ticket, str(ticket.id), assignees)
)
return
@@ -42,24 +43,24 @@ def send_ticket_applied_mail_to_assignees(ticket):
)
if settings.DEBUG:
logger.debug(message)
- recipient_list = [assignee.email for assignee in ticket.assignees.all()]
+ recipient_list = [i.assignee.email for i in assignees]
send_mail_async.delay(subject, message, recipient_list, html_message=message)
-def send_ticket_processed_mail_to_applicant(ticket):
+def send_ticket_processed_mail_to_applicant(ticket, processor):
if not ticket.applicant:
logger.error("Not found applicant: {}({})".format(ticket.title, ticket.id))
return
-
+ processor_display = str(processor)
ticket_detail_url = urljoin(settings.SITE_URL, const.TICKET_DETAIL_URL.format(id=str(ticket.id)))
- subject = _('Ticket has processed - {} ({})').format(ticket.title, ticket.processor_display)
+ subject = _('Ticket has processed - {} ({})').format(ticket.title, processor_display)
message = EMAIL_TEMPLATE.format(
- title=_('Your ticket has been processed, processor - {}').format(ticket.processor_display),
+ title=_('Your ticket has been processed, processor - {}').format(processor_display),
ticket_detail_url=ticket_detail_url,
ticket_detail_url_description=_('click here to review'),
body=ticket.body.replace('\n', '
'),
)
if settings.DEBUG:
logger.debug(message)
- recipient_list = [ticket.applicant.email]
+ recipient_list = [ticket.applicant.email, ]
send_mail_async.delay(subject, message, recipient_list, html_message=message)
diff --git a/apps/users/api/profile.py b/apps/users/api/profile.py
index 9dcb42515..02bdda07c 100644
--- a/apps/users/api/profile.py
+++ b/apps/users/api/profile.py
@@ -4,14 +4,13 @@ import uuid
from rest_framework import generics
from common.permissions import IsOrgAdmin
from rest_framework.permissions import IsAuthenticated
-from django.conf import settings
+from users.notifications import ResetPasswordMsg, ResetPasswordSuccessMsg, ResetSSHKeyMsg
from common.permissions import (
IsCurrentUserOrReadOnly
)
from .. import serializers
from ..models import User
-from ..utils import send_reset_password_success_mail
from .mixins import UserQuerysetMixin
__all__ = [
@@ -29,11 +28,10 @@ class UserResetPasswordApi(UserQuerysetMixin, generics.UpdateAPIView):
def perform_update(self, serializer):
# Note: we are not updating the user object here.
# We just do the reset-password stuff.
- from ..utils import send_reset_password_mail
user = self.get_object()
user.password_raw = str(uuid.uuid4())
user.save()
- send_reset_password_mail(user)
+ ResetPasswordMsg(user).publish_async()
class UserResetPKApi(UserQuerysetMixin, generics.UpdateAPIView):
@@ -41,11 +39,11 @@ class UserResetPKApi(UserQuerysetMixin, generics.UpdateAPIView):
permission_classes = (IsOrgAdmin,)
def perform_update(self, serializer):
- from ..utils import send_reset_ssh_key_mail
user = self.get_object()
user.public_key = None
user.save()
- send_reset_ssh_key_mail(user)
+
+ ResetSSHKeyMsg(user).publish_async()
# 废弃
@@ -84,4 +82,4 @@ class UserPublicKeyApi(generics.RetrieveUpdateAPIView):
def perform_update(self, serializer):
super().perform_update(serializer)
- send_reset_password_success_mail(self.request, self.get_object())
+ ResetPasswordSuccessMsg(self.get_object(), self.request).publish_async()
diff --git a/apps/users/api/user.py b/apps/users/api/user.py
index edd9b396b..f55a96964 100644
--- a/apps/users/api/user.py
+++ b/apps/users/api/user.py
@@ -8,6 +8,7 @@ from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from django.db.models import Prefetch
+from users.notifications import ResetMFAMsg
from common.permissions import (
IsOrgAdmin, IsOrgAdminOrAppUser,
CanUpdateDeleteUser, IsSuperUser
@@ -16,7 +17,7 @@ from common.mixins import CommonApiMixin
from common.utils import get_logger
from orgs.utils import current_org
from orgs.models import ROLE as ORG_ROLE, OrganizationMember
-from users.utils import send_reset_mfa_mail, LoginBlockUtil, MFABlockUtils
+from users.utils import LoginBlockUtil, MFABlockUtils
from .. import serializers
from ..serializers import UserSerializer, UserRetrieveSerializer, MiniUserSerializer, InviteSerializer
from .mixins import UserQuerysetMixin
@@ -130,14 +131,7 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
@action(methods=['get'], detail=False, permission_classes=(IsOrgAdmin,))
def suggestion(self, request):
queryset = User.objects.exclude(role=User.ROLE.APP)
- queryset = self.filter_queryset(queryset)
- queryset = queryset[:3]
-
- page = self.paginate_queryset(queryset)
- if page is not None:
- serializer = self.get_serializer(page, many=True)
- return self.get_paginated_response(serializer.data)
-
+ queryset = self.filter_queryset(queryset)[:3]
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@@ -216,5 +210,6 @@ class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
if user.mfa_enabled:
user.reset_mfa()
user.save()
- send_reset_mfa_mail(user)
+
+ ResetMFAMsg(user).publish_async()
return Response({"msg": "success"})
diff --git a/apps/users/exceptions.py b/apps/users/exceptions.py
index ff873d3dc..e69e65966 100644
--- a/apps/users/exceptions.py
+++ b/apps/users/exceptions.py
@@ -8,3 +8,13 @@ class MFANotEnabled(JMSException):
status_code = status.HTTP_403_FORBIDDEN
default_code = 'mfa_not_enabled'
default_detail = _('MFA not enabled')
+
+
+class PhoneNotSet(JMSException):
+ default_code = 'phone_not_set'
+ default_detail = _('Phone not set')
+
+
+class MFAMethodNotSupport(JMSException):
+ default_code = 'mfa_not_support'
+ default_detail = _('MFA method not support')
diff --git a/apps/users/forms/profile.py b/apps/users/forms/profile.py
index 63c1078d6..d2b005e12 100644
--- a/apps/users/forms/profile.py
+++ b/apps/users/forms/profile.py
@@ -19,7 +19,7 @@ __all__ = [
class UserCheckPasswordForm(forms.Form):
password = forms.CharField(
label=_('Password'), widget=forms.PasswordInput,
- max_length=128, strip=False
+ max_length=1024, strip=False
)
diff --git a/apps/users/models/user.py b/apps/users/models/user.py
index 836e0b383..9911bf91b 100644
--- a/apps/users/models/user.py
+++ b/apps/users/models/user.py
@@ -20,19 +20,24 @@ from django.shortcuts import reverse
from orgs.utils import current_org
from orgs.models import OrganizationMember, Organization
+from common.exceptions import JMSException
from common.utils import date_expired_default, get_logger, lazyproperty, random_string
from common import fields
from common.const import choices
from common.db.models import TextChoices
-from users.exceptions import MFANotEnabled
+from users.exceptions import MFANotEnabled, PhoneNotSet
from ..signals import post_user_change_password
-
-__all__ = ['User', 'UserPasswordHistory']
+__all__ = ['User', 'UserPasswordHistory', 'MFAType']
logger = get_logger(__file__)
+class MFAType(TextChoices):
+ OTP = 'otp', _('One-time password')
+ SMS_CODE = 'sms', _('SMS verify code')
+
+
class AuthMixin:
date_password_last_updated: datetime.datetime
is_local: bool
@@ -369,12 +374,9 @@ class RoleMixin:
@classmethod
def get_super_and_org_admins(cls, org=None):
super_admins = cls.get_super_admins()
- super_admin_ids = list(super_admins.values_list('id', flat=True))
- org_admins = cls.get_org_admins(org)
- org_admin_ids = list(org_admins.values_list('id', flat=True))
- admin_ids = set(org_admin_ids + super_admin_ids)
- admins = User.objects.filter(id__in=admin_ids)
- return admins
+ org_admins = cls.get_org_admins(org=org)
+ admins = org_admins | super_admins
+ return admins.distinct()
class TokenMixin:
@@ -518,21 +520,62 @@ class MFAMixin:
from ..utils import check_otp_code
return check_otp_code(self.otp_secret_key, code)
- def check_mfa(self, code):
+ def check_mfa(self, code, mfa_type=MFAType.OTP):
if not self.mfa_enabled:
raise MFANotEnabled
- if settings.OTP_IN_RADIUS:
- return self.check_radius(code)
- else:
- return self.check_otp(code)
+ if mfa_type == MFAType.OTP:
+ if settings.OTP_IN_RADIUS:
+ return self.check_radius(code)
+ else:
+ return self.check_otp(code)
+ elif mfa_type == MFAType.SMS_CODE:
+ return self.check_sms_code(code)
+
+ def get_supported_mfa_types(self):
+ methods = []
+ if self.otp_secret_key:
+ methods.append(MFAType.OTP)
+ if self.phone:
+ methods.append(MFAType.SMS_CODE)
+ return methods
+
+ def check_sms_code(self, code):
+ from authentication.sms_verify_code import VerifyCodeUtil
+
+ if not self.phone:
+ raise PhoneNotSet
+
+ try:
+ util = VerifyCodeUtil(self.phone)
+ return util.verify(code)
+ except JMSException:
+ return False
+
+ def send_sms_code(self):
+ from authentication.sms_verify_code import VerifyCodeUtil
+
+ if not self.phone:
+ raise PhoneNotSet
+
+ util = VerifyCodeUtil(self.phone)
+ util.touch()
+ return util.timeout
def mfa_enabled_but_not_set(self):
if not self.mfa_enabled:
return False, None
- if self.mfa_is_otp() and not self.otp_secret_key:
- return True, reverse('authentication:user-otp-enable-start')
- return False, None
+
+ if not self.mfa_is_otp():
+ return False, None
+
+ if self.mfa_is_otp() and self.otp_secret_key:
+ return False, None
+
+ if self.phone and settings.SMS_ENABLED and settings.XPACK_ENABLED:
+ return False, None
+
+ return True, reverse('authentication:user-otp-enable-start')
class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
@@ -626,6 +669,10 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
group_ids = list(group_ids)
return group_ids
+ @property
+ def receive_backends(self):
+ return self.user_msg_subscription.receive_backends
+
@property
def is_wecom_bound(self):
return bool(self.wecom_id)
diff --git a/apps/users/notifications.py b/apps/users/notifications.py
new file mode 100644
index 000000000..7d860ca14
--- /dev/null
+++ b/apps/users/notifications.py
@@ -0,0 +1,389 @@
+from datetime import datetime
+
+from django.utils.translation import ugettext as _
+
+from common.utils import reverse, get_request_ip_or_data, get_request_user_agent, lazyproperty
+from notifications.notifications import UserMessage
+
+
+class BaseUserMessage(UserMessage):
+ def get_text_msg(self) -> dict:
+ raise NotImplementedError
+
+ def get_html_msg(self) -> dict:
+ raise NotImplementedError
+
+ @lazyproperty
+ def text_msg(self) -> dict:
+ return self.get_text_msg()
+
+ @lazyproperty
+ def html_msg(self) -> dict:
+ return self.get_html_msg()
+
+ def get_dingtalk_msg(self) -> dict:
+ return self.text_msg
+
+ def get_wecom_msg(self) -> dict:
+ return self.text_msg
+
+ def get_feishu_msg(self) -> dict:
+ return self.text_msg
+
+ def get_email_msg(self) -> dict:
+ return self.html_msg
+
+ def get_site_msg_msg(self) -> dict:
+ return self.html_msg
+
+
+class ResetPasswordMsg(BaseUserMessage):
+ def get_text_msg(self) -> dict:
+ user = self.user
+ subject = _('Reset password')
+ message = _("""
+Hello %(name)s:
+Please click the link below to reset your password, if not your request, concern your account security
+
+Click here reset password 👇
+%(rest_password_url)s?token=%(rest_password_token)s
+
+This link is valid for 1 hour. After it expires,
+
+request new one 👇
+%(forget_password_url)s?email=%(email)s
+
+-------------------
+
+Login direct 👇
+%(login_url)s
+
+""") % {
+ 'name': user.name,
+ 'rest_password_url': reverse('authentication:reset-password', external=True),
+ 'rest_password_token': user.generate_reset_token(),
+ 'forget_password_url': reverse('authentication:forgot-password', external=True),
+ 'email': user.email,
+ 'login_url': reverse('authentication:login', external=True),
+ }
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+ def get_html_msg(self) -> dict:
+ user = self.user
+ subject = _('Reset password')
+ message = _("""
+ Hello %(name)s:
+
+ Please click the link below to reset your password, if not your request, concern your account security
+
+ Click here reset password
+
+ This link is valid for 1 hour. After it expires, request new one
+
+
+ ---
+
+
+ Login direct
+
+
+ """) % {
+ 'name': user.name,
+ 'rest_password_url': reverse('authentication:reset-password', external=True),
+ 'rest_password_token': user.generate_reset_token(),
+ 'forget_password_url': reverse('authentication:forgot-password', external=True),
+ 'email': user.email,
+ 'login_url': reverse('authentication:login', external=True),
+ }
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+
+class ResetPasswordSuccessMsg(BaseUserMessage):
+ def __init__(self, user, request):
+ super().__init__(user)
+ self.ip_address = get_request_ip_or_data(request)
+ self.browser = get_request_user_agent(request)
+
+ def get_text_msg(self) -> dict:
+ user = self.user
+
+ subject = _('Reset password success')
+ message = _("""
+
+Hi %(name)s:
+
+Your JumpServer password has just been successfully updated.
+
+If the password update was not initiated by you, your account may have security issues.
+It is recommended that you log on to the JumpServer immediately and change your password.
+
+If you have any questions, you can contact the administrator.
+
+-------------------
+
+
+IP Address: %(ip_address)s
+
+
+Browser: %(browser)s
+
+
+ """) % {
+ 'name': user.name,
+ 'ip_address': self.ip_address,
+ 'browser': self.browser,
+ }
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+ def get_html_msg(self) -> dict:
+ user = self.user
+
+ subject = _('Reset password success')
+ message = _("""
+
+ Hi %(name)s:
+
+
+
+
+ Your JumpServer password has just been successfully updated.
+
+
+
+ If the password update was not initiated by you, your account may have security issues.
+ It is recommended that you log on to the JumpServer immediately and change your password.
+
+
+
+ If you have any questions, you can contact the administrator.
+
+
+ ---
+
+
+ IP Address: %(ip_address)s
+
+
+ Browser: %(browser)s
+
+
+ """) % {
+ 'name': user.name,
+ 'ip_address': self.ip_address,
+ 'browser': self.browser,
+ }
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+
+class PasswordExpirationReminderMsg(BaseUserMessage):
+ def get_text_msg(self) -> dict:
+ user = self.user
+
+ subject = _('Security notice')
+ message = _("""
+Hello %(name)s:
+
+Your password will expire in %(date_password_expired)s,
+
+For your account security, please click on the link below to update your password in time
+
+Click here update password 👇
+%(update_password_url)s
+
+If your password has expired, please click 👇 to apply for a password reset email.
+%(forget_password_url)s?email=%(email)s
+
+-------------------
+
+Login direct 👇
+%(login_url)s
+
+ """) % {
+ '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('authentication:forgot-password', external=True),
+ 'email': user.email,
+ 'login_url': reverse('authentication:login', external=True),
+ }
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+ def get_html_msg(self) -> dict:
+ user = self.user
+
+ subject = _('Security notice')
+ message = _("""
+ Hello %(name)s:
+
+ Your password will expire in %(date_password_expired)s,
+
+ For your account security, please click on the link below to update your password in time
+
+ Click here update password
+
+ If your password has expired, please click
+ Password expired
+ to apply for a password reset email.
+
+
+ ---
+
+
+ Login direct
+
+
+ """) % {
+ '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('authentication:forgot-password', external=True),
+ 'email': user.email,
+ 'login_url': reverse('authentication:login', external=True),
+ }
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+
+class UserExpirationReminderMsg(BaseUserMessage):
+ def get_text_msg(self) -> dict:
+ subject = _('Expiration notice')
+ message = _("""
+Hello %(name)s:
+
+Your account will expire in %(date_expired)s,
+
+In order not to affect your normal work, please contact the administrator for confirmation.
+
+ """) % {
+ 'name': self.user.name,
+ 'date_expired': datetime.fromtimestamp(datetime.timestamp(
+ self.user.date_expired)).strftime('%Y-%m-%d %H:%M'),
+ }
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+ def get_html_msg(self) -> dict:
+ subject = _('Expiration notice')
+ message = _("""
+ Hello %(name)s:
+
+ Your account will expire in %(date_expired)s,
+
+ In order not to affect your normal work, please contact the administrator for confirmation.
+
+ """) % {
+ 'name': self.user.name,
+ 'date_expired': datetime.fromtimestamp(datetime.timestamp(
+ self.user.date_expired)).strftime('%Y-%m-%d %H:%M'),
+ }
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+
+class ResetSSHKeyMsg(BaseUserMessage):
+ def get_text_msg(self) -> dict:
+ subject = _('SSH Key Reset')
+ message = _("""
+Hello %(name)s:
+
+Your ssh public key has been reset by site administrator.
+Please login and reset your ssh public key.
+
+Login direct 👇
+%(login_url)s
+
+ """) % {
+ 'name': self.user.name,
+ 'login_url': reverse('authentication:login', external=True),
+ }
+
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+ def get_html_msg(self) -> dict:
+ subject = _('SSH Key Reset')
+ message = _("""
+ Hello %(name)s:
+
+ Your ssh public key has been reset by site administrator.
+ Please login and reset your ssh public key.
+
+ Login direct
+
+
+ """) % {
+ 'name': self.user.name,
+ 'login_url': reverse('authentication:login', external=True),
+ }
+
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+
+class ResetMFAMsg(BaseUserMessage):
+ def get_text_msg(self) -> dict:
+ subject = _('MFA Reset')
+ message = _("""
+Hello %(name)s:
+
+Your MFA has been reset by site administrator.
+Please login and reset your MFA.
+
+Login direct 👇
+%(login_url)s
+
+ """) % {
+ 'name': self.user.name,
+ 'login_url': reverse('authentication:login', external=True),
+ }
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+ def get_html_msg(self) -> dict:
+ subject = _('MFA Reset')
+ message = _("""
+ Hello %(name)s:
+
+ Your MFA has been reset by site administrator.
+ Please login and reset your MFA.
+
+ Login direct
+
+
+ """) % {
+ 'name': self.user.name,
+ 'login_url': reverse('authentication:login', external=True),
+ }
+ return {
+ 'subject': subject,
+ 'message': message
+ }
diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py
index 09ab50cc0..74e8e460b 100644
--- a/apps/users/serializers/profile.py
+++ b/apps/users/serializers/profile.py
@@ -101,16 +101,17 @@ class UserProfileSerializer(UserSerializer):
)
mfa_level = serializers.ChoiceField(choices=MFA_LEVEL_CHOICES, label=_('MFA'), required=False)
guide_url = serializers.SerializerMethodField()
+ receive_backends = serializers.ListField(child=serializers.CharField())
class Meta(UserSerializer.Meta):
fields = UserSerializer.Meta.fields + [
'public_key_comment', 'public_key_hash_md5',
'admin_or_audit_orgs', 'current_org_roles',
'guide_url', 'user_all_orgs', 'is_org_admin',
- 'is_superuser'
+ 'is_superuser', 'receive_backends',
]
read_only_fields = [
- 'date_joined', 'last_login', 'created_by', 'source'
+ 'date_joined', 'last_login', 'created_by', 'source', 'receive_backends',
]
extra_kwargs = dict(UserSerializer.Meta.extra_kwargs)
extra_kwargs.update({
diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py
index d4abf8d34..85fd40359 100644
--- a/apps/users/serializers/user.py
+++ b/apps/users/serializers/user.py
@@ -181,7 +181,7 @@ class UserRetrieveSerializer(UserSerializer):
class MiniUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
- fields = ['id', 'name', 'username']
+ fields = UserSerializer.Meta.fields_mini
class InviteSerializer(serializers.Serializer):
diff --git a/apps/users/tasks.py b/apps/users/tasks.py
index fc938499b..58ce4e3ed 100644
--- a/apps/users/tasks.py
+++ b/apps/users/tasks.py
@@ -5,15 +5,14 @@ import sys
from celery import shared_task
from django.conf import settings
+from users.notifications import PasswordExpirationReminderMsg
from ops.celery.utils import (
create_or_update_celery_periodic_tasks, disable_celery_periodic_task
)
from ops.celery.decorator import after_app_ready_start
from common.utils import get_logger
from .models import User
-from .utils import (
- send_password_expiration_reminder_mail, send_user_expiration_reminder_mail
-)
+from users.notifications import UserExpirationReminderMsg
from settings.utils import LDAPServerUtil, LDAPImportUtil
@@ -30,7 +29,8 @@ def check_password_expired():
continue
msg = "The user {} password expires in {} days"
logger.info(msg.format(user, user.password_expired_remain_days))
- send_password_expiration_reminder_mail(user)
+
+ PasswordExpirationReminderMsg(user).publish_async()
@shared_task
@@ -57,7 +57,8 @@ def check_user_expired():
continue
msg = "The user {} will expires in {} days"
logger.info(msg.format(user, user.expired_remain_days))
- send_user_expiration_reminder_mail(user)
+
+ UserExpirationReminderMsg(user).publish_async()
@shared_task
@@ -92,8 +93,8 @@ def import_ldap_user():
def import_ldap_user_periodic():
if not settings.AUTH_LDAP:
return
+ task_name = 'import_ldap_user_periodic'
if not settings.AUTH_LDAP_SYNC_IS_PERIODIC:
- task_name = sys._getframe().f_code.co_name
disable_celery_periodic_task(task_name)
return
@@ -104,7 +105,7 @@ def import_ldap_user_periodic():
interval = None
crontab = settings.AUTH_LDAP_SYNC_CRONTAB
tasks = {
- 'import_ldap_user_periodic': {
+ task_name: {
'task': import_ldap_user.name,
'interval': interval,
'crontab': crontab,
diff --git a/apps/users/templates/users/user_otp_check_password.html b/apps/users/templates/users/user_otp_check_password.html
index f83e03b78..2f04c4b33 100644
--- a/apps/users/templates/users/user_otp_check_password.html
+++ b/apps/users/templates/users/user_otp_check_password.html
@@ -7,14 +7,34 @@
{% endblock %}
{% block content %}
-
+
+
{% endblock %}
diff --git a/apps/users/utils.py b/apps/users/utils.py
index 8c55a99e5..b89dacdb2 100644
--- a/apps/users/utils.py
+++ b/apps/users/utils.py
@@ -10,7 +10,6 @@ import time
from django.conf import settings
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, get_request_ip_or_data, get_request_user_agent
@@ -79,184 +78,6 @@ def send_user_created_mail(user):
send_mail_async.delay(subject, message, recipient_list, html_message=message)
-def send_reset_password_mail(user):
- subject = _('Reset password')
- recipient_list = [user.email]
- message = _("""
- Hello %(name)s:
-
- Please click the link below to reset your password, if not your request, concern your account security
-
- Click here reset password
-
- This link is valid for 1 hour. After it expires, request new one
-
-
- ---
-
-
- Login direct
-
-
- """) % {
- 'name': user.name,
- 'rest_password_url': reverse('authentication:reset-password', external=True),
- 'rest_password_token': user.generate_reset_token(),
- 'forget_password_url': reverse('authentication:forgot-password', external=True),
- 'email': user.email,
- 'login_url': reverse('authentication:login', external=True),
- }
- if settings.DEBUG:
- logger.debug(message)
-
- send_mail_async.delay(subject, message, recipient_list, html_message=message)
-
-
-def send_reset_password_success_mail(request, user):
- subject = _('Reset password success')
- recipient_list = [user.email]
- message = _("""
-
- Hi %(name)s:
-
-
-
-
- Your JumpServer password has just been successfully updated.
-
-
-
- If the password update was not initiated by you, your account may have security issues.
- It is recommended that you log on to the JumpServer immediately and change your password.
-
-
-
- If you have any questions, you can contact the administrator.
-
-
- ---
-
-
- IP Address: %(ip_address)s
-
-
- Browser: %(browser)s
-
-
- """) % {
- 'name': user.name,
- 'ip_address': get_request_ip_or_data(request),
- 'browser': get_request_user_agent(request),
- }
- if settings.DEBUG:
- logger.debug(message)
-
- send_mail_async.delay(subject, message, recipient_list, html_message=message)
-
-
-def send_password_expiration_reminder_mail(user):
- subject = _('Security notice')
- recipient_list = [user.email]
- message = _("""
- Hello %(name)s:
-
- Your password will expire in %(date_password_expired)s,
-
- For your account security, please click on the link below to update your password in time
-
- Click here update password
-
- If your password has expired, please click
- Password expired
- to apply for a password reset email.
-
-
- ---
-
-
- Login direct
-
-
- """) % {
- '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('authentication:forgot-password', external=True),
- 'email': user.email,
- 'login_url': reverse('authentication:login', external=True),
- }
- if settings.DEBUG:
- logger.debug(message)
-
- send_mail_async.delay(subject, message, recipient_list, html_message=message)
-
-
-def send_user_expiration_reminder_mail(user):
- subject = _('Expiration notice')
- recipient_list = [user.email]
- message = _("""
- Hello %(name)s:
-
- Your account will expire in %(date_expired)s,
-
- In order not to affect your normal work, please contact the administrator for confirmation.
-
- """) % {
- 'name': user.name,
- 'date_expired': datetime.fromtimestamp(datetime.timestamp(
- user.date_expired)).strftime('%Y-%m-%d %H:%M'),
- }
- 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]
- message = _("""
- Hello %(name)s:
-
- Your ssh public key has been reset by site administrator.
- Please login and reset your ssh public key.
-
- Login direct
-
-
- """) % {
- 'name': user.name,
- 'login_url': reverse('authentication:login', external=True),
- }
- if settings.DEBUG:
- logger.debug(message)
-
- send_mail_async.delay(subject, message, recipient_list, html_message=message)
-
-
-def send_reset_mfa_mail(user):
- subject = _('MFA Reset')
- recipient_list = [user.email]
- message = _("""
- Hello %(name)s:
-
- Your MFA has been reset by site administrator.
- Please login and reset your MFA.
-
- Login direct
-
-
- """) % {
- 'name': user.name,
- 'login_url': reverse('authentication:login', external=True),
- }
- if settings.DEBUG:
- logger.debug(message)
-
- send_mail_async.delay(subject, message, recipient_list, html_message=message)
-
-
def get_user_or_pre_auth_user(request):
user = request.user
if user.is_authenticated:
diff --git a/apps/users/views/profile/password.py b/apps/users/views/profile/password.py
index e2dde8e92..8c92487c1 100644
--- a/apps/users/views/profile/password.py
+++ b/apps/users/views/profile/password.py
@@ -6,6 +6,8 @@ from django.contrib.auth import authenticate
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from django.views.generic.edit import FormView
+from authentication.mixins import PasswordEncryptionViewMixin
+from authentication import errors
from common.utils import get_logger
from ... import forms
@@ -18,13 +20,17 @@ __all__ = ['UserVerifyPasswordView']
logger = get_logger(__name__)
-class UserVerifyPasswordView(FormView):
+class UserVerifyPasswordView(PasswordEncryptionViewMixin, FormView):
template_name = 'users/user_password_verify.html'
form_class = forms.UserCheckPasswordForm
def form_valid(self, form):
user = get_user_or_pre_auth_user(self.request)
- password = form.cleaned_data.get('password')
+ try:
+ password = self.get_decrypted_password(username=user.username)
+ except errors.AuthFailedError as e:
+ form.add_error("password", _(f"Password invalid") + f'({e.msg})')
+ return self.form_invalid(form)
user = authenticate(request=self.request, username=user.username, password=password)
if not user:
form.add_error("password", _("Password invalid"))
diff --git a/apps/users/views/profile/reset.py b/apps/users/views/profile/reset.py
index 793322cc9..c3529c73c 100644
--- a/apps/users/views/profile/reset.py
+++ b/apps/users/views/profile/reset.py
@@ -9,13 +9,13 @@ from django.conf import settings
from django.urls import reverse_lazy
from django.views.generic import FormView
+from users.notifications import ResetPasswordSuccessMsg, ResetPasswordMsg
from common.utils import get_object_or_none, FlashMessageUtil
from common.permissions import IsValidUser
from common.mixins.views import PermissionsMixin
from ...models import User
from ...utils import (
- send_reset_password_mail, get_password_check_rules, check_password_rules,
- send_reset_password_success_mail
+ get_password_check_rules, check_password_rules,
)
from ... import forms
@@ -59,7 +59,8 @@ class UserForgotPasswordView(FormView):
).format(user.get_source_display())
form.add_error('email', error)
return self.form_invalid(form)
- send_reset_password_mail(user)
+
+ ResetPasswordMsg(user).publish_async()
url = self.get_redirect_message_url()
return redirect(url)
@@ -115,7 +116,8 @@ class UserResetPasswordView(FormView):
user.reset_password(password)
User.expired_reset_password_token(token)
- send_reset_password_success_mail(self.request, user)
+
+ ResetPasswordSuccessMsg(user, self.request).publish_async()
url = self.get_redirect_url()
return redirect(url)
diff --git a/config_example.yml b/config_example.yml
index d2124df2e..dd509c0aa 100644
--- a/config_example.yml
+++ b/config_example.yml
@@ -1,5 +1,5 @@
# SECURITY WARNING: keep the secret key used in production secret!
-# 加密秘钥 生产环境中请修改为随机字符串,请勿外泄, 可使用命令生成
+# 加密密钥 生产环境中请修改为随机字符串,请勿外泄, 可使用命令生成
# $ cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 49;echo
SECRET_KEY:
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 21be4262c..aaa1b662a 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -59,7 +59,7 @@ PyNaCl==1.2.1
python-dateutil==2.6.1
#python-gssapi==0.6.4
pytz==2018.3
-PyYAML==5.1
+PyYAML==5.2
redis==3.5.3
requests==2.25.1
jms-storage==0.0.39
@@ -77,7 +77,7 @@ aliyun-python-sdk-core-v3==2.9.1
aliyun-python-sdk-ecs==4.10.1
rest_condition==1.0.3
python-ldap==3.3.1
-tencentcloud-sdk-python==3.0.40
+tencentcloud-sdk-python==3.0.477
django-radius==1.4.0
ipip-ipdb==1.2.1
django-redis-sessions==0.6.1
@@ -114,3 +114,8 @@ azure-identity==1.5.0
azure-mgmt-subscription==1.0.0
qingcloud-sdk==1.2.12
django-simple-history==3.0.0
+google-cloud-compute==0.5.0
+PyMySQL==1.0.2
+cx-Oracle==8.2.1
+psycopg2-binary==2.9.1
+alibabacloud_dysmsapi20170525==2.0.2