diff --git a/README.md b/README.md index fdcf28b11..8f5da3631 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ - [ENGLISH](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md) -|![notification](https://raw.githubusercontent.com/goharbor/website/master/docs/img/readme/bell-outline-badged.svg)安全通知| + +|《新一代堡垒机建设指南》开放下载| |------------------| -|2021年1月15日 JumpServer 发现远程执行漏洞,请速度修复 [详见](https://github.com/jumpserver/jumpserver/issues/5533), 非常感谢 **reactivity of Alibaba Hackerone bug bounty program**(瑞典) 向我们报告了此 BUG| +|本白皮书由JumpServer开源项目组编著而成。编写团队从企业实践和技术演进的双重视角出发,结合自身在身份与访问安全领域长期研发及落地经验组织撰写,同时积极听取行业内专家的意见和建议,在此基础上完成了本白皮书的编写任务。下载链接:https://jinshuju.net/f/E0qAl8| -------------------------- diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index ed525e6cf..d689675b0 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -5,7 +5,7 @@ from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView from common.permissions import IsAppUser from common.utils import reverse, lazyproperty from orgs.utils import tmp_to_org, tmp_to_root_org -from tickets.models import Ticket +from tickets.api import GenericTicketStatusRetrieveCloseAPI from ..models import LoginAssetACL from .. import serializers @@ -48,7 +48,7 @@ class LoginAssetCheckAPI(CreateAPIView): org_id=self.serializer.org.id ) confirm_status_url = reverse( - view_name='acls:login-asset-confirm-status', + view_name='api-acls:login-asset-confirm-status', kwargs={'pk': str(ticket.id)} ) ticket_detail_url = reverse( @@ -72,34 +72,6 @@ class LoginAssetCheckAPI(CreateAPIView): return serializer -class LoginAssetConfirmStatusAPI(RetrieveDestroyAPIView): - permission_classes = (IsAppUser, ) +class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI): + pass - def retrieve(self, request, *args, **kwargs): - if self.ticket.action_open: - status = 'await' - elif self.ticket.action_approve: - status = 'approve' - else: - status = 'reject' - data = { - 'status': status, - 'action': self.ticket.action, - 'processor': self.ticket.processor_display - } - return Response(data=data, status=200) - - def destroy(self, request, *args, **kwargs): - if self.ticket.status_open: - self.ticket.close(processor=self.ticket.applicant) - data = { - 'action': self.ticket.action, - 'status': self.ticket.status, - 'processor': self.ticket.processor_display - } - return Response(data=data, status=200) - - @lazyproperty - def ticket(self): - with tmp_to_root_org(): - return get_object_or_404(Ticket, pk=self.kwargs['pk']) diff --git a/apps/acls/models/login_acl.py b/apps/acls/models/login_acl.py index 44d100098..323599f4c 100644 --- a/apps/acls/models/login_acl.py +++ b/apps/acls/models/login_acl.py @@ -33,6 +33,9 @@ class LoginACL(BaseACL): class Meta: ordering = ('priority', '-date_updated', 'name') + def __str__(self): + return self.name + @property def action_reject(self): return self.action == self.ActionChoices.reject diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index 162665c83..8ac140370 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -38,6 +38,9 @@ class LoginAssetACL(BaseACL, OrgModelMixin): unique_together = ('name', 'org_id') ordering = ('priority', '-date_updated', 'name') + def __str__(self): + return self.name + @classmethod def filter(cls, user, asset, system_user, action): queryset = cls.objects.filter(action=action) diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index ee1a5ac25..b303db533 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -35,10 +35,15 @@ class LoginACLSerializer(BulkModelSerializer): class Meta: model = LoginACL - fields = [ - 'id', 'name', 'priority', 'ip_group', 'user', 'user_display', 'action', - 'action_display', 'is_active', 'comment', 'created_by', 'date_created', 'date_updated' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'priority', 'ip_group', 'action', 'action_display', + 'is_active', + 'date_created', 'date_updated', + 'comment', 'created_by', ] + fields_fk = ['user', 'user_display',] + fields = fields_small + fields_fk extra_kwargs = { 'priority': {'default': 50}, 'is_active': {'default': True}, diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py index 842cbc8a0..bbb31af94 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -54,7 +54,7 @@ class LoginAssetACLSystemUsersSerializer(serializers.Serializer): protocol_group = serializers.ListField( default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'), help_text=protocol_group_help_text.format( - ', '.join(SystemUser.ASSET_CATEGORY_PROTOCOLS) + ', '.join([SystemUser.PROTOCOL_SSH, SystemUser.PROTOCOL_TELNET]) ) ) @@ -76,11 +76,15 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer): class Meta: model = models.LoginAssetACL - fields = [ - 'id', 'name', 'priority', 'users', 'system_users', 'assets', 'action', 'action_display', - 'is_active', 'comment', 'reviewers', 'reviewers_amount', 'created_by', 'date_created', - 'date_updated', 'org_id' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'users', 'system_users', 'assets', + 'is_active', + 'date_created', 'date_updated', + 'priority', 'action', 'action_display', 'comment', 'created_by', 'org_id' ] + fields_m2m = ['reviewers', 'reviewers_amount'] + fields = fields_small + fields_m2m extra_kwargs = { "reviewers": {'allow_null': False, 'required': True}, 'priority': {'default': 50}, diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py index 580ce9a12..bff8c270a 100644 --- a/apps/applications/serializers/application.py +++ b/apps/applications/serializers/application.py @@ -49,10 +49,14 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri class Meta: model = models.Application - fields = [ - 'id', 'name', 'category', 'category_display', 'type', 'type_display', 'attrs', - 'domain', 'created_by', 'date_created', 'date_updated', 'comment' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'category', 'category_display', 'type', 'type_display', 'attrs', + 'date_created', 'date_updated', + 'created_by', 'comment' ] + fields_fk = ['domain'] + fields = fields_small + fields_fk read_only_fields = [ 'created_by', 'date_created', 'date_updated', 'get_type_display', ] diff --git a/apps/assets/api/asset_user.py b/apps/assets/api/asset_user.py index e2844a5e5..da0fe8c7e 100644 --- a/apps/assets/api/asset_user.py +++ b/apps/assets/api/asset_user.py @@ -10,10 +10,10 @@ from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify from common.utils import get_object_or_none, get_logger from common.mixins import CommonApiMixin from ..backends import AssetUserManager -from ..models import Asset, Node, SystemUser +from ..models import Node from .. import serializers from ..tasks import ( - test_asset_users_connectivity_manual, push_system_user_a_asset_manual + test_asset_users_connectivity_manual ) @@ -100,12 +100,6 @@ class AssetUserViewSet(CommonApiMixin, BulkModelViewSet): obj = queryset.get(id=pk) return obj - def get_exception_handler(self): - def handler(e, context): - logger.error(e, exc_info=True) - return Response({"error": str(e)}, status=400) - return handler - def perform_destroy(self, instance): manager = AssetUserManager() manager.delete(instance) diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py index 95aac8af9..56cbbd6c3 100644 --- a/apps/assets/api/cmd_filter.py +++ b/apps/assets/api/cmd_filter.py @@ -1,15 +1,25 @@ # -*- coding: utf-8 -*- # +from rest_framework.response import Response +from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView from django.shortcuts import get_object_or_404 +from common.utils import reverse +from common.utils import lazyproperty from orgs.mixins.api import OrgBulkModelViewSet -from ..hands import IsOrgAdmin +from orgs.utils import tmp_to_root_org +from tickets.models import Ticket +from tickets.api import GenericTicketStatusRetrieveCloseAPI +from ..hands import IsOrgAdmin, IsAppUser from ..models import CommandFilter, CommandFilterRule from .. import serializers -__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet'] +__all__ = [ + 'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI', + 'CommandConfirmStatusAPI' +] class CommandFilterViewSet(OrgBulkModelViewSet): @@ -35,3 +45,50 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet): return cmd_filter.rules.all() +class CommandConfirmAPI(CreateAPIView): + permission_classes = (IsAppUser, ) + serializer_class = serializers.CommandConfirmSerializer + + def create(self, request, *args, **kwargs): + ticket = self.create_command_confirm_ticket() + response_data = self.get_response_data(ticket) + return Response(data=response_data, status=200) + + def create_command_confirm_ticket(self): + ticket = self.serializer.cmd_filter_rule.create_command_confirm_ticket( + run_command=self.serializer.data.get('run_command'), + session=self.serializer.session, + cmd_filter_rule=self.serializer.cmd_filter_rule, + org_id=self.serializer.org.id + ) + return ticket + + @staticmethod + def get_response_data(ticket): + confirm_status_url = reverse( + view_name='api-assets:command-confirm-status', + kwargs={'pk': str(ticket.id)} + ) + ticket_detail_url = reverse( + view_name='api-tickets:ticket-detail', + kwargs={'pk': str(ticket.id)}, + external=True, api_to_ui=True + ) + ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type) + return { + 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, + 'close_confirm': {'method': 'DELETE', 'url': confirm_status_url}, + 'ticket_detail_url': ticket_detail_url, + 'reviewers': [str(user) for user in ticket.assignees.all()] + } + + @lazyproperty + def serializer(self): + serializer = self.get_serializer(data=self.request.data) + serializer.is_valid(raise_exception=True) + return serializer + + +class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI): + pass + diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index c756e9efd..a8164cd13 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -71,8 +71,8 @@ class NodeViewSet(OrgModelViewSet): if node.is_org_root(): error = _("You can't delete the root node ({})".format(node.value)) return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN) - if node.has_children_or_has_assets(): - error = _("Deletion failed and the node contains children or assets") + if node.has_offspring_assets(): + error = _("Deletion failed and the node contains assets") return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN) return super().destroy(request, *args, **kwargs) diff --git a/apps/assets/migrations/0070_auto_20210426_1515.py b/apps/assets/migrations/0070_auto_20210426_1515.py new file mode 100644 index 000000000..ca6ff4273 --- /dev/null +++ b/apps/assets/migrations/0070_auto_20210426_1515.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1 on 2021-04-26 07:15 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0069_change_node_key0_to_key1'), + ] + + operations = [ + migrations.AddField( + model_name='commandfilterrule', + name='reviewers', + field=models.ManyToManyField(blank=True, related_name='review_cmd_filter_rules', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'), + ), + migrations.AlterField( + model_name='commandfilterrule', + name='action', + field=models.IntegerField(choices=[(0, 'Deny'), (1, 'Allow'), (2, 'Reconfirm')], default=0, verbose_name='Action'), + ), + ] diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index a21778a42..c009d00a1 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -35,7 +35,7 @@ def default_node(): try: from .node import Node root = Node.org_root() - return root + return Node.objects.filter(id=root.id) except: return None diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index c1242fd7e..1ef14bad0 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -41,11 +41,12 @@ class CommandFilterRule(OrgModelMixin): (TYPE_COMMAND, _('Command')), ) - ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3) - ACTION_CHOICES = ( - (ACTION_DENY, _('Deny')), - (ACTION_ALLOW, _('Allow')), - ) + ACTION_UNKNOWN = 10 + + class ActionChoices(models.IntegerChoices): + deny = 0, _('Deny') + allow = 1, _('Allow') + confirm = 2, _('Reconfirm') id = models.UUIDField(default=uuid.uuid4, primary_key=True) filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules') @@ -53,7 +54,13 @@ class CommandFilterRule(OrgModelMixin): priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)]) content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command")) - action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action")) + action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action")) + # 动作: 附加字段 + # - confirm: 命令复核人 + reviewers = models.ManyToManyField( + 'users.User', related_name='review_cmd_filter_rules', blank=True, + verbose_name=_("Reviewers") + ) comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment")) date_created = models.DateTimeField(auto_now_add=True) date_updated = models.DateTimeField(auto_now=True) @@ -89,10 +96,32 @@ class CommandFilterRule(OrgModelMixin): if not found: return self.ACTION_UNKNOWN, '' - if self.action == self.ACTION_ALLOW: - return self.ACTION_ALLOW, found.group() + if self.action == self.ActionChoices.allow: + return self.ActionChoices.allow, found.group() else: - return self.ACTION_DENY, found.group() + return self.ActionChoices.deny, found.group() def __str__(self): 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.models import Ticket + data = { + 'title': _('Command confirm') + ' ({})'.format(session.user), + 'type': TicketTypeChoices.command_confirm, + 'meta': { + 'apply_run_user': session.user, + 'apply_run_asset': session.asset, + 'apply_run_system_user': session.system_user, + 'apply_run_command': run_command, + 'apply_from_session_id': str(session.id), + 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id), + 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id) + }, + 'org_id': org_id, + } + ticket = Ticket.objects.create(**data) + ticket.assignees.set(self.reviewers.all()) + ticket.open(applicant=session.user_obj) + return ticket diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 973df4b4a..2e3b890a5 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -38,8 +38,7 @@ def compute_parent_key(key): class NodeQuerySet(models.QuerySet): - def delete(self): - raise NotImplementedError + pass class FamilyMixin: @@ -622,14 +621,14 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin): tree_node = TreeNode(**data) return tree_node - def has_children_or_has_assets(self): - if self.children or self.get_assets().exists(): - return True - return False + def has_offspring_assets(self): + # 拥有后代资产 + return self.get_all_assets().exists() def delete(self, using=None, keep_parents=False): - if self.has_children_or_has_assets(): + if self.has_offspring_assets(): return + self.all_children.delete() return super().delete(using=using, keep_parents=keep_parents) def update_child_full_value(self): diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 1640c7f32..37af8ea86 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -196,9 +196,9 @@ class SystemUser(BaseUser): def is_command_can_run(self, command): for rule in self.cmd_filter_rules: action, matched_cmd = rule.match(command) - if action == rule.ACTION_ALLOW: + if action == rule.ActionChoices.allow: return True, None - elif action == rule.ACTION_DENY: + elif action == rule.ActionChoices.deny: return False, matched_cmd return True, None diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index 0211d88cc..2e1913b47 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -16,10 +16,14 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = AdminUser - fields = [ - 'id', 'name', 'username', 'password', 'private_key', 'public_key', - 'comment', 'assets_amount', 'date_created', 'date_updated', 'created_by', + fields_mini = ['id', 'name', 'username'] + fields_write_only = ['password', 'private_key', 'public_key'] + fields_small = fields_mini + fields_write_only + [ + 'date_created', 'date_updated', + 'comment', 'created_by' ] + fields_fk = ['assets_amount'] + fields = fields_small + fields_fk read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount'] extra_kwargs = { diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index 9d8f9f27a..80e923b00 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -65,7 +65,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer): platform = serializers.SlugRelatedField( slug_field='name', queryset=Platform.objects.all(), label=_("Platform") ) - protocols = ProtocolsField(label=_('Protocols'), required=False) + protocols = ProtocolsField(label=_('Protocols'), required=False, default=['ssh/22']) domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name')) admin_user_display = serializers.ReadOnlyField(source='admin_user.name', label=_('Admin user name')) nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False) diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py index 7eba6ba41..19cb2adc7 100644 --- a/apps/assets/serializers/asset_user.py +++ b/apps/assets/serializers/asset_user.py @@ -22,10 +22,11 @@ class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializ class Meta: model = AuthBook list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'username', 'password', 'private_key', "public_key", - 'asset', 'comment', - ] + fields_mini = ['id', 'username'] + fields_write_only = ['password', 'private_key', "public_key"] + fields_small = fields_mini + fields_write_only + ['comment'] + fields_fk = ['asset'] + fields = fields_small + fields_fk extra_kwargs = { 'username': {'required': True}, 'password': {'write_only': True}, @@ -52,11 +53,15 @@ class AssetUserReadSerializer(AssetUserWriteSerializer): 'date_created', 'date_updated', 'created_by', 'version', ) - fields = [ - 'id', 'username', 'password', 'private_key', "public_key", - 'asset', 'hostname', 'ip', 'backend', 'version', - 'date_created', "date_updated", 'comment', + fields_mini = ['id', 'username'] + fields_write_only = ['password', 'private_key', "public_key"] + fields_small = fields_mini + fields_write_only + [ + 'backend', 'version', + 'date_created', "date_updated", + 'comment' ] + fields_fk = ['asset', 'hostname', 'ip'] + fields = fields_small + fields_fk extra_kwargs = { 'username': {'required': True}, 'password': {'write_only': True}, diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index 0c8eca3d4..052452825 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -6,6 +6,9 @@ from rest_framework import serializers from common.drf.serializers import AdaptedBulkListSerializer from ..models import CommandFilter, CommandFilterRule, SystemUser from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from orgs.utils import tmp_to_root_org +from common.utils import get_object_or_none, lazyproperty +from terminal.models import Session class CommandFilterSerializer(BulkOrgResourceModelSerializer): @@ -13,11 +16,16 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer): class Meta: model = CommandFilter list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'org_id', 'org_name', 'is_active', 'comment', - 'created_by', 'date_created', 'date_updated', 'rules', 'system_users' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'org_id', 'org_name', + 'is_active', + 'date_created', 'date_updated', + 'comment', 'created_by', ] - + fields_fk = ['rules'] + fields_m2m = ['system_users'] + fields = fields_small + fields_fk + fields_m2m extra_kwargs = { 'rules': {'read_only': True}, 'system_users': {'required': False}, @@ -34,8 +42,9 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): fields_mini = ['id'] fields_small = fields_mini + [ 'type', 'type_display', 'content', 'priority', - 'action', 'action_display', - 'comment', 'created_by', 'date_created', 'date_updated' + 'action', 'action_display', 'reviewers', + 'date_created', 'date_updated', + 'comment', 'created_by', ] fields_fk = ['filter'] fields = '__all__' @@ -50,3 +59,35 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): # msg = _("Content should not be contain: {}").format(invalid_char) # raise serializers.ValidationError(msg) # return content + + +class CommandConfirmSerializer(serializers.Serializer): + session_id = serializers.UUIDField(required=True, allow_null=False) + cmd_filter_rule_id = serializers.UUIDField(required=True, allow_null=False) + run_command = serializers.CharField(required=True, allow_null=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.session = None + self.cmd_filter_rule = None + + def validate_session_id(self, session_id): + self.session = self.validate_object_exist(Session, session_id) + return session_id + + def validate_cmd_filter_rule_id(self, cmd_filter_rule_id): + self.cmd_filter_rule = self.validate_object_exist(CommandFilterRule, cmd_filter_rule_id) + return cmd_filter_rule_id + + @staticmethod + def validate_object_exist(model, field_id): + with tmp_to_root_org(): + obj = get_object_or_none(model, id=field_id) + if not obj: + error = '{} Model object does not exist'.format(model.__name__) + raise serializers.ValidationError(error) + return obj + + @lazyproperty + def org(self): + return self.session.org diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index f7b402f0e..5f0a44ec2 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -48,11 +48,18 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = Gateway list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'ip', 'port', 'protocol', 'username', 'password', - 'private_key', 'public_key', 'domain', 'is_active', 'date_created', - 'date_updated', 'created_by', 'comment', + fields_mini = ['id', 'name'] + fields_write_only = [ + 'password', 'private_key', 'public_key', ] + fields_small = fields_mini + fields_write_only + [ + 'username', 'ip', 'port', 'protocol', + 'is_active', + 'date_created', 'date_updated', + 'created_by', 'comment', + ] + fields_fk = ['domain'] + fields = fields_small + fields_fk extra_kwargs = { 'password': {'validators': [NoSpecialChars()]} } diff --git a/apps/assets/serializers/gathered_user.py b/apps/assets/serializers/gathered_user.py index 2629a8327..86e7dcfae 100644 --- a/apps/assets/serializers/gathered_user.py +++ b/apps/assets/serializers/gathered_user.py @@ -10,11 +10,14 @@ from ..models import GatheredUser class GatheredUserSerializer(OrgResourceModelSerializerMixin): class Meta: model = GatheredUser - fields = [ - 'id', 'asset', 'hostname', 'ip', 'username', - 'date_last_login', 'ip_last_login', - 'present', 'date_created', 'date_updated' + fields_mini = ['id'] + fields_small = fields_mini + [ + 'username', 'ip_last_login', + 'present', + 'date_last_login', 'date_created', 'date_updated' ] + fields_fk = ['asset', 'hostname', 'ip'] + fields = fields_small + fields_fk read_only_fields = fields extra_kwargs = { 'hostname': {'label': _("Hostname")}, diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py index a98d4757d..f922655ef 100644 --- a/apps/assets/serializers/label.py +++ b/apps/assets/serializers/label.py @@ -15,10 +15,15 @@ class LabelSerializer(BulkOrgResourceModelSerializer): class Meta: model = Label - fields = [ - 'id', 'name', 'value', 'category', 'is_active', 'comment', - 'date_created', 'asset_count', 'assets', 'category_display' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'value', 'category', 'category_display', + 'is_active', + 'date_created', + 'comment', ] + fields_m2m = ['asset_count', 'assets'] + fields = fields_small + fields_m2m read_only_fields = ( 'category', 'date_created', 'asset_count', ) diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 1b3b5de5c..e1ada114d 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -26,16 +26,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class Meta: model = SystemUser list_serializer_class = AdaptedBulkListSerializer - fields = [ - 'id', 'name', 'username', 'protocol', - 'password', 'public_key', 'private_key', - 'login_mode', 'login_mode_display', - 'priority', 'username_same_with_user', - 'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', - 'auto_generate_key', 'sftp_root', 'token', - 'assets_amount', 'date_created', 'date_updated', 'created_by', - 'home', 'system_groups', 'ad_domain' + fields_mini = ['id', 'name', 'username'] + fields_write_only = ['password', 'public_key', 'private_key'] + fields_small = fields_mini + fields_write_only + [ + 'protocol', 'login_mode', 'login_mode_display', 'priority', + 'sudo', 'shell', 'sftp_root', 'token', + 'home', 'system_groups', 'ad_domain', + 'username_same_with_user', 'auto_push', 'auto_generate_key', + 'date_created', 'date_updated', + 'comment', 'created_by', ] + fields_m2m = [ 'cmd_filters', 'assets_amount'] + fields = fields_small + fields_m2m extra_kwargs = { 'password': {"write_only": True}, 'public_key': {"write_only": True}, @@ -147,17 +149,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): class SystemUserListSerializer(SystemUserSerializer): class Meta(SystemUserSerializer.Meta): - fields = [ - 'id', 'name', 'username', 'protocol', - 'password', 'public_key', 'private_key', - 'login_mode', 'login_mode_display', - 'priority', "username_same_with_user", - 'auto_push', 'sudo', 'shell', 'comment', - "assets_amount", 'home', 'system_groups', - 'auto_generate_key', 'ad_domain', - 'sftp_root', 'created_by', 'date_created', - 'date_updated', + fields_mini = ['id', 'name', 'username'] + fields_write_only = ['password', 'public_key', 'private_key'] + fields_small = fields_mini + fields_write_only + [ + 'protocol', 'login_mode', 'login_mode_display', 'priority', + 'sudo', 'shell', 'home', 'system_groups', + 'ad_domain', 'sftp_root', + "username_same_with_user", 'auto_push', 'auto_generate_key', + 'date_created', 'date_updated', + 'comment', 'created_by', ] + fields_m2m = ["assets_amount",] + fields = fields_small + fields_m2m extra_kwargs = { 'password': {"write_only": True}, 'public_key': {"write_only": True}, @@ -178,15 +181,15 @@ class SystemUserListSerializer(SystemUserSerializer): class SystemUserWithAuthInfoSerializer(SystemUserSerializer): class Meta(SystemUserSerializer.Meta): - fields = [ - 'id', 'name', 'username', 'protocol', - 'password', 'public_key', 'private_key', - 'login_mode', 'login_mode_display', - 'priority', 'username_same_with_user', - 'auto_push', 'sudo', 'shell', 'comment', - 'auto_generate_key', 'sftp_root', 'token', - 'ad_domain', + fields_mini = ['id', 'name', 'username'] + fields_write_only = ['password', 'public_key', 'private_key'] + fields_small = fields_mini + fields_write_only + [ + 'protocol', 'login_mode', 'login_mode_display', 'priority', + 'sudo', 'shell', 'ad_domain', 'sftp_root', 'token', + "username_same_with_user", 'auto_push', 'auto_generate_key', + 'comment', ] + fields = fields_small extra_kwargs = { 'nodes_amount': {'label': _('Node')}, 'assets_amount': {'label': _('Asset')}, diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py index 88b3a62a6..f640e03a1 100644 --- a/apps/assets/tasks/push_system_user.py +++ b/apps/assets/tasks/push_system_user.py @@ -56,8 +56,8 @@ def get_push_unixlike_system_user_tasks(system_user, username=None): 'shell': system_user.shell or Empty, 'state': 'present', 'home': system_user.home or Empty, + 'expires': -1, 'groups': groups or Empty, - 'expires': 99999, 'comment': comment } diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 5a5e6d803..c8413f83e 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -63,6 +63,9 @@ urlpatterns = [ path('gateways//test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), + path('cmd-filters/command-confirm/', api.CommandConfirmAPI.as_view(), name='command-confirm'), + path('cmd-filters/command-confirm//status/', api.CommandConfirmStatusAPI.as_view(), name='command-confirm-status') + ] old_version_urlpatterns = [ diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 74eebbecc..d7ab3e996 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -16,10 +16,14 @@ class FTPLogSerializer(serializers.ModelSerializer): class Meta: model = models.FTPLog - fields = ( - 'id', 'user', 'remote_addr', 'asset', 'system_user', 'org_id', - 'operate', 'filename', 'is_success', 'date_start', 'operate_display' - ) + fields_mini = ['id'] + fields_small = fields_mini + [ + 'user', 'remote_addr', 'asset', 'system_user', 'org_id', + 'operate', 'filename', 'operate_display', + 'is_success', + 'date_start', + ] + fields = fields_small class UserLoginLogSerializer(serializers.ModelSerializer): @@ -29,11 +33,14 @@ class UserLoginLogSerializer(serializers.ModelSerializer): class Meta: model = models.UserLoginLog - fields = ( - 'id', 'username', 'type', 'type_display', 'ip', 'city', 'user_agent', - 'mfa', 'reason', 'status', 'status_display', 'datetime', 'mfa_display', - 'backend' - ) + fields_mini = ['id'] + fields_small = fields_mini + [ + 'username', 'type', 'type_display', 'ip', 'city', 'user_agent', + 'mfa', 'mfa_display', 'reason', 'backend', + 'status', 'status_display', + 'datetime', + ] + fields = fields_small extra_kwargs = { "user_agent": {'label': _('User agent')} } @@ -42,10 +49,13 @@ class UserLoginLogSerializer(serializers.ModelSerializer): class OperateLogSerializer(serializers.ModelSerializer): class Meta: model = models.OperateLog - fields = ( - 'id', 'user', 'action', 'resource_type', 'resource', - 'remote_addr', 'datetime', 'org_id' - ) + fields_mini = ['id'] + fields_small = fields_mini + [ + 'user', 'action', 'resource_type', 'resource', 'remote_addr', + 'datetime', + 'org_id' + ] + fields = fields_small class PasswordChangeLogSerializer(serializers.ModelSerializer): diff --git a/apps/audits/signals_handler.py b/apps/audits/signals_handler.py index 853030287..930a1f03d 100644 --- a/apps/audits/signals_handler.py +++ b/apps/audits/signals_handler.py @@ -27,11 +27,23 @@ json_render = JSONRenderer() MODELS_NEED_RECORD = ( - 'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser', - 'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter', - 'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask', - 'Platform', 'ChangeAuthPlan', 'GatherUserTask', - 'RemoteApp', 'RemoteAppPermission', 'DatabaseApp', 'DatabaseAppPermission', + # users + 'User', 'UserGroup', + # acls + 'LoginACL', 'LoginAssetACL', + # assets + 'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule', + 'CommandFilter', 'Platform', + # applications + 'Application', + # orgs + 'Organization', + # settings + 'Setting', + # perms + 'AssetPermission', 'ApplicationPermission', + # xpack + 'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask', ) @@ -45,6 +57,8 @@ class AuthBackendLabelMapping(LazyObject): backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key') backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password') backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO') + backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom') + backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk') return backend_label_mapping def _setup(self): diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py index 12b83421f..6ef54b09b 100644 --- a/apps/authentication/api/__init__.py +++ b/apps/authentication/api/__init__.py @@ -7,3 +7,6 @@ from .mfa import * from .access_key import * from .login_confirm import * from .sso import * +from .wecom import * +from .dingtalk import * +from .password import * diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index ae9e4a2de..ceebf3bde 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +import urllib.parse + from django.conf import settings from django.core.cache import cache from django.shortcuts import get_object_or_404 @@ -77,7 +79,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView }) key = self.CACHE_KEY_PREFIX.format(token) - cache.set(key, value, timeout=20) + cache.set(key, value, timeout=30*60) return token def create(self, request, *args, **kwargs): @@ -100,7 +102,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView 'desktopwidth:i': '1280', 'desktopheight:i': '800', 'use multimon:i': '1', - 'session bpp:i': '24', + 'session bpp:i': '32', 'audiomode:i': '0', 'disable wallpaper:i': '0', 'disable full window drag:i': '0', @@ -140,15 +142,16 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView # Todo: 上线后地址是 JumpServerAddr:3389 address = self.request.query_params.get('address') or '1.1.1.1' options['full address:s'] = address - options['username:s'] = '{}@{}'.format(user.username, token) + options['username:s'] = '{}|{}'.format(user.username, token) options['desktopwidth:i'] = width options['desktopheight:i'] = height data = '' for k, v in options.items(): data += f'{k}:{v}\n' - response = HttpResponse(data, content_type='text/plain') + response = HttpResponse(data, content_type='application/octet-stream') filename = "{}-{}-jumpserver.rdp".format(user.username, asset.hostname) - response['Content-Disposition'] = 'attachment; filename={}'.format(filename) + filename = urllib.parse.quote(filename) + response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename return response @staticmethod diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py new file mode 100644 index 000000000..e4b2ea85b --- /dev/null +++ b/apps/authentication/api/dingtalk.py @@ -0,0 +1,35 @@ +from rest_framework.views import APIView +from rest_framework.request import Request +from rest_framework.response import Response + +from users.permissions import IsAuthPasswdTimeValid +from users.models import User +from common.utils import get_logger +from common.permissions import IsOrgAdmin +from common.mixins.api import RoleUserMixin, RoleAdminMixin +from authentication import errors + +logger = get_logger(__file__) + + +class DingTalkQRUnBindBase(APIView): + user: User + + def post(self, request: Request, **kwargs): + user = self.user + + if not user.dingtalk_id: + raise errors.DingTalkNotBound + + user.dingtalk_id = '' + user.save() + return Response() + + +class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase): + permission_classes = (IsAuthPasswdTimeValid,) + + +class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase): + user_id_url_kwarg = 'user_id' + permission_classes = (IsOrgAdmin,) diff --git a/apps/authentication/api/password.py b/apps/authentication/api/password.py new file mode 100644 index 000000000..af8b41358 --- /dev/null +++ b/apps/authentication/api/password.py @@ -0,0 +1,26 @@ +from rest_framework.generics import CreateAPIView +from rest_framework.response import Response + +from authentication.serializers import PasswordVerifySerializer +from common.permissions import IsValidUser +from authentication.mixins import authenticate +from authentication.errors import PasswdInvalid +from authentication.mixins import AuthMixin + + +class UserPasswordVerifyApi(AuthMixin, CreateAPIView): + permission_classes = (IsValidUser,) + serializer_class = PasswordVerifySerializer + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + password = serializer.validated_data['password'] + user = self.request.user + + user = authenticate(request=request, username=user.username, password=password) + if not user: + raise PasswdInvalid + + self.set_passwd_verify_on_session(user) + return Response() diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py new file mode 100644 index 000000000..1ab5ff725 --- /dev/null +++ b/apps/authentication/api/wecom.py @@ -0,0 +1,35 @@ +from rest_framework.views import APIView +from rest_framework.request import Request +from rest_framework.response import Response + +from users.permissions import IsAuthPasswdTimeValid +from users.models import User +from common.utils import get_logger +from common.permissions import IsOrgAdmin +from common.mixins.api import RoleUserMixin, RoleAdminMixin +from authentication import errors + +logger = get_logger(__file__) + + +class WeComQRUnBindBase(APIView): + user: User + + def post(self, request: Request, **kwargs): + user = self.user + + if not user.wecom_id: + raise errors.WeComNotBound + + user.wecom_id = '' + user.save() + return Response() + + +class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase): + permission_classes = (IsAuthPasswdTimeValid,) + + +class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase): + user_id_url_kwarg = 'user_id' + permission_classes = (IsOrgAdmin,) diff --git a/apps/authentication/backends/api.py b/apps/authentication/backends/api.py index 1fd315abb..63356eff6 100644 --- a/apps/authentication/backends/api.py +++ b/apps/authentication/backends/api.py @@ -205,3 +205,21 @@ class SSOAuthentication(ModelBackend): def authenticate(self, request, sso_token=None, **kwargs): pass + + +class WeComAuthentication(ModelBackend): + """ + 什么也不做呀😺 + """ + + def authenticate(self, request, **kwargs): + pass + + +class DingTalkAuthentication(ModelBackend): + """ + 什么也不做呀😺 + """ + + def authenticate(self, request, **kwargs): + pass diff --git a/apps/authentication/errors.py b/apps/authentication/errors.py index 8f9ba9307..bcd83c97d 100644 --- a/apps/authentication/errors.py +++ b/apps/authentication/errors.py @@ -19,6 +19,7 @@ reason_user_inactive = 'user_inactive' reason_user_expired = 'user_expired' reason_backend_not_match = 'backend_not_match' reason_acl_not_allow = 'acl_not_allow' +only_local_users_are_allowed = 'only_local_users_are_allowed' reason_choices = { reason_password_failed: _('Username/password check failed'), @@ -32,6 +33,7 @@ reason_choices = { reason_user_expired: _("This account is expired"), reason_backend_not_match: _("Auth backend not match"), reason_acl_not_allow: _("ACL is not allowed"), + only_local_users_are_allowed: _("Only local users are allowed") } old_reason_choices = { '0': '-', @@ -275,6 +277,15 @@ class PasswdTooSimple(JMSException): self.url = url +class PasswdNeedUpdate(JMSException): + default_code = 'passwd_need_update' + default_detail = _('You should to change your password before login') + + def __init__(self, url, *args, **kwargs): + super().__init__(*args, **kwargs) + self.url = url + + class PasswordRequireResetError(JMSException): default_code = 'passwd_has_expired' default_detail = _('Your password has expired, please reset before logging in') @@ -282,3 +293,28 @@ class PasswordRequireResetError(JMSException): def __init__(self, url, *args, **kwargs): super().__init__(*args, **kwargs) self.url = url + + +class WeComCodeInvalid(JMSException): + default_code = 'wecom_code_invalid' + default_detail = 'Code invalid, can not get user info' + + +class WeComBindAlready(JMSException): + default_code = 'wecom_bind_already' + default_detail = 'WeCom already binded' + + +class WeComNotBound(JMSException): + default_code = 'wecom_not_bound' + default_detail = 'WeCom is not bound' + + +class DingTalkNotBound(JMSException): + default_code = 'dingtalk_not_bound' + default_detail = 'DingTalk is not bound' + + +class PasswdInvalid(JMSException): + default_code = 'passwd_invalid' + default_detail = _('Your password is invalid') diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index 89d7b85fd..5eeceb7c3 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -5,15 +5,17 @@ from urllib.parse import urlencode from functools import partial import time +from django.core.cache import cache from django.conf import settings from django.contrib import auth +from django.utils.translation import ugettext as _ from django.contrib.auth import ( BACKEND_SESSION_KEY, _get_backends, PermissionDenied, user_login_failed, _clean_credentials ) -from django.shortcuts import reverse +from django.shortcuts import reverse, redirect -from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get +from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil from users.models import User from users.utils import LoginBlockUtil, MFABlockUtils from . import errors @@ -81,6 +83,8 @@ class AuthMixin: request = None partial_credential_error = None + key_prefix_captcha = "_LOGIN_INVALID_{}" + def get_user_from_session(self): if self.request.session.is_empty(): raise errors.SessionEmptyError() @@ -109,11 +113,7 @@ class AuthMixin: ip = ip or get_request_ip(self.request) return ip - def check_is_block(self, raise_exception=True): - if hasattr(self.request, 'data'): - username = self.request.data.get("username") - else: - username = self.request.POST.get("username") + def _check_is_block(self, username, raise_exception=True): ip = self.get_request_ip() if LoginBlockUtil(username, ip).is_block(): logger.warn('Ip was blocked' + ': ' + username + ':' + ip) @@ -123,6 +123,13 @@ class AuthMixin: else: return exception + def check_is_block(self, raise_exception=True): + if hasattr(self.request, 'data'): + username = self.request.data.get("username") + else: + 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) @@ -139,6 +146,9 @@ class AuthMixin: def raise_credential_error(self, error): raise self.partial_credential_error(error=error) + def _set_partial_credential_error(self, username, ip, request): + self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request) + def get_auth_data(self, decrypt_passwd=False): request = self.request if hasattr(request, 'data'): @@ -150,7 +160,7 @@ class AuthMixin: username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='') password = password + challenge.strip() ip = self.get_request_ip() - self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request) + self._set_partial_credential_error(username=username, ip=ip, request=request) if decrypt_passwd: password = self.decrypt_passwd(password) @@ -183,6 +193,21 @@ class AuthMixin: if not is_allowed: raise errors.LoginIPNotAllowed(username=user.username, request=self.request) + def set_login_failed_mark(self): + ip = self.get_request_ip() + cache.set(self.key_prefix_captcha.format(ip), 1, 3600) + + def set_passwd_verify_on_session(self, user: User): + self.request.session['user_id'] = str(user.id) + self.request.session['auth_password'] = 1 + self.request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS + + def check_is_need_captcha(self): + # 最近有登录失败时需要填写验证码 + ip = get_request_ip(self.request) + need = cache.get(self.key_prefix_captcha.format(ip)) + return need + def check_user_auth(self, decrypt_passwd=False): self.check_is_block() request = self.request @@ -194,6 +219,7 @@ class AuthMixin: self._check_login_acl(user, ip) self._check_password_require_reset_or_not(user) self._check_passwd_is_too_simple(user, password) + self._check_passwd_need_update(user) LoginBlockUtil(username, ip).clean_failed_count() request.session['auth_password'] = 1 @@ -202,34 +228,62 @@ class AuthMixin: request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL) return user + def _check_is_local_user(self, user: User): + if user.source != User.Source.local: + raise self.raise_credential_error(error=errors.only_local_users_are_allowed) + + def check_oauth2_auth(self, user: User, auth_backend): + ip = self.get_request_ip() + request = self.request + + self._set_partial_credential_error(user.username, ip, request) + self._check_is_local_user(user) + self._check_is_block(user.username) + self._check_login_acl(user, ip) + + LoginBlockUtil(user.username, ip).clean_failed_count() + MFABlockUtils(user.username, ip).clean_failed_count() + + request.session['auth_password'] = 1 + request.session['user_id'] = str(user.id) + request.session['auth_backend'] = auth_backend + return user + @classmethod - def generate_reset_password_url_with_flash_msg(cls, user: User, flash_view_name): + def generate_reset_password_url_with_flash_msg(cls, user, message): reset_passwd_url = reverse('authentication:reset-password') query_str = urlencode({ 'token': user.generate_reset_token() }) reset_passwd_url = f'{reset_passwd_url}?{query_str}' - flash_page_url = reverse(flash_view_name) - query_str = urlencode({ - 'redirect_url': reset_passwd_url - }) - return f'{flash_page_url}?{query_str}' + message_data = { + 'title': _('Please change your password'), + 'message': message, + 'interval': 3, + 'redirect_url': reset_passwd_url, + } + return FlashMessageUtil.gen_message_url(message_data) @classmethod def _check_passwd_is_too_simple(cls, user: User, password): if user.is_superuser and password == 'admin': - url = cls.generate_reset_password_url_with_flash_msg( - user, 'authentication:passwd-too-simple-flash-msg' - ) + message = _('Your password is too simple, please change it for security') + url = cls.generate_reset_password_url_with_flash_msg(user, message=message) raise errors.PasswdTooSimple(url) + @classmethod + def _check_passwd_need_update(cls, user: User): + if user.need_update_password: + message = _('You should to change your password before login') + url = cls.generate_reset_password_url_with_flash_msg(user, message) + raise errors.PasswdNeedUpdate(url) + @classmethod def _check_password_require_reset_or_not(cls, user: User): if user.password_has_expired: - url = cls.generate_reset_password_url_with_flash_msg( - user, 'authentication:passwd-has-expired-flash-msg' - ) + message = _('Your password has expired, please reset before logging in') + url = cls.generate_reset_password_url_with_flash_msg(user, message) raise errors.PasswordRequireResetError(url) def check_user_auth_if_need(self, decrypt_passwd=False): @@ -345,3 +399,10 @@ class AuthMixin: sender=self.__class__, username=username, request=self.request, reason=reason ) + + def redirect_to_guard_view(self): + guard_url = reverse('authentication:login-guard') + args = self.request.META.get('QUERY_STRING', '') + if args: + guard_url = "%s?%s" % (guard_url, args) + return redirect(guard_url) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index c86b48039..72b54e3ee 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -8,14 +8,16 @@ from users.models import User from assets.models import Asset, SystemUser, Gateway from applications.models import Application from users.serializers import UserProfileSerializer +from assets.serializers import ProtocolsField from perms.serializers.asset.permission import ActionsField -from .models import AccessKey, LoginConfirmSetting, SSOToken +from .models import AccessKey, LoginConfirmSetting __all__ = [ 'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer', 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer', - 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer' + 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer', + 'PasswordVerifySerializer', ] @@ -30,6 +32,10 @@ class OtpVerifySerializer(serializers.Serializer): code = serializers.CharField(max_length=6, min_length=6) +class PasswordVerifySerializer(serializers.Serializer): + password = serializers.CharField() + + class BearerTokenSerializer(serializers.Serializer): username = serializers.CharField(allow_null=True, required=False, write_only=True) password = serializers.CharField(write_only=True, allow_null=True, @@ -150,9 +156,11 @@ class ConnectionTokenUserSerializer(serializers.ModelSerializer): class ConnectionTokenAssetSerializer(serializers.ModelSerializer): + protocols = ProtocolsField(label='Protocols', read_only=True) + class Meta: model = Asset - fields = ['id', 'hostname', 'ip', 'port', 'org_id'] + fields = ['id', 'hostname', 'ip', 'protocols', 'org_id'] class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer): diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 722ef5d16..0104d0e44 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -117,6 +117,15 @@ float: right; margin: 10px 10px 0 0; } + .more-login-item { + border-right: 1px dashed #dedede; + padding-left: 5px; + padding-right: 5px; + } + + .more-login-item:last-child { + border: none; + } @@ -182,10 +191,10 @@
- {% if AUTH_OPENID or AUTH_CAS %} + {% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK %}
- {% trans "More login options" %} + {% trans "More login options" %} {% if AUTH_OPENID %} {% endif %} + {% if AUTH_WECOM %} + + {% endif %} + {% if AUTH_DINGTALK %} + + {% endif %} +
{% else %}
diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index d9b302800..0849cc82a 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -14,10 +14,17 @@ router.register('connection-token', api.UserConnectionTokenViewSet, 'connection- urlpatterns = [ # path('token/', api.UserToken.as_view(), name='user-token'), + path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'), + path('wecom/qr/unbind//', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'), + + path('dingtalk/qr/unbind/', api.DingTalkQRUnBindForUserApi.as_view(), name='dingtalk-qr-unbind'), + path('dingtalk/qr/unbind//', api.DingTalkQRUnBindForAdminApi.as_view(), name='dingtalk-qr-unbind-for-admin'), + 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('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), + 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/urls/view_urls.py b/apps/authentication/urls/view_urls.py index a95342fa6..8e754340c 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -18,14 +18,25 @@ urlpatterns = [ # 原来在users中的 path('password/forgot/', users_view.UserForgotPasswordView.as_view(), name='forgot-password'), - path('password/forgot/sendmail-success/', users_view.UserForgotPasswordSendmailSuccessView.as_view(), - name='forgot-password-sendmail-success'), path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'), - path('password/too-simple-flash-msg/', views.FlashPasswdTooSimpleMsgView.as_view(), name='passwd-too-simple-flash-msg'), - path('password/has-expired-msg/', views.FlashPasswdHasExpiredMsgView.as_view(), name='passwd-has-expired-flash-msg'), - path('password/reset/success/', users_view.UserResetPasswordSuccessView.as_view(), name='reset-password-success'), path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'), + path('wecom/bind/success-flash-msg/', views.FlashWeComBindSucceedMsgView.as_view(), name='wecom-bind-success-flash-msg'), + path('wecom/bind/failed-flash-msg/', views.FlashWeComBindFailedMsgView.as_view(), name='wecom-bind-failed-flash-msg'), + path('wecom/bind/start/', views.WeComEnableStartView.as_view(), name='wecom-bind-start'), + path('wecom/qr/bind/', views.WeComQRBindView.as_view(), name='wecom-qr-bind'), + path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'), + path('wecom/qr/bind//callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'), + path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'), + + path('dingtalk/bind/success-flash-msg/', views.FlashDingTalkBindSucceedMsgView.as_view(), name='dingtalk-bind-success-flash-msg'), + path('dingtalk/bind/failed-flash-msg/', views.FlashDingTalkBindFailedMsgView.as_view(), name='dingtalk-bind-failed-flash-msg'), + path('dingtalk/bind/start/', views.DingTalkEnableStartView.as_view(), name='dingtalk-bind-start'), + path('dingtalk/qr/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'), + path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'), + path('dingtalk/qr/bind//callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'), + path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'), + # Profile path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'), path('profile/otp/enable/start/', users_view.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'), diff --git a/apps/authentication/views/__init__.py b/apps/authentication/views/__init__.py index 5a1a40f7a..0467e321a 100644 --- a/apps/authentication/views/__init__.py +++ b/apps/authentication/views/__init__.py @@ -2,3 +2,5 @@ # from .login import * from .mfa import * +from .wecom import * +from .dingtalk import * diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py new file mode 100644 index 000000000..521a93c26 --- /dev/null +++ b/apps/authentication/views/dingtalk.py @@ -0,0 +1,243 @@ +import urllib + +from django.http.response import HttpResponseRedirect +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext as _ +from django.views.decorators.cache import never_cache +from django.views.generic import TemplateView +from django.views import View +from django.conf import settings +from django.http.request import HttpRequest +from rest_framework.permissions import IsAuthenticated, AllowAny + +from users.views import UserVerifyPasswordView +from users.utils import is_auth_password_time_valid +from users.models import User +from common.utils import get_logger +from common.utils.random import random_string +from common.utils.django import reverse, get_object_or_none +from common.message.backends.dingtalk import URL +from common.mixins.views import PermissionsMixin +from authentication import errors +from authentication.mixins import AuthMixin +from common.message.backends.dingtalk import DingTalk + +logger = get_logger(__file__) + + +DINGTALK_STATE_SESSION_KEY = '_dingtalk_state' + + +class DingTalkQRMixin(PermissionsMixin, View): + def verify_state(self): + state = self.request.GET.get('state') + session_state = self.request.session.get(DINGTALK_STATE_SESSION_KEY) + if state != session_state: + return False + return True + + def get_verify_state_failed_response(self, redirect_uri): + msg = _("You've been hacked") + return self.get_failed_reponse(redirect_uri, msg, msg) + + def get_qr_url(self, redirect_uri): + state = random_string(16) + self.request.session[DINGTALK_STATE_SESSION_KEY] = state + + params = { + 'appid': settings.DINGTALK_APPKEY, + 'response_type': 'code', + 'scope': 'snsapi_login', + 'state': state, + 'redirect_uri': redirect_uri, + } + url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params) + return url + + def get_success_reponse(self, redirect_url, title, msg): + ok_flash_msg_url = reverse('authentication:dingtalk-bind-success-flash-msg') + ok_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(ok_flash_msg_url) + + def get_failed_reponse(self, redirect_url, title, msg): + failed_flash_msg_url = reverse('authentication:dingtalk-bind-failed-flash-msg') + failed_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(failed_flash_msg_url) + + def get_already_bound_response(self, redirect_url): + msg = _('DingTalk is already bound') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + +class DingTalkQRBindView(DingTalkQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest): + user = request.user + redirect_url = request.GET.get('redirect_url') + + if not is_auth_password_time_valid(request.session): + msg = _('Please verify your password first') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + redirect_uri = reverse('authentication:dingtalk-qr-bind-callback', kwargs={'user_id': user.id}, external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class DingTalkQRBindCallbackView(DingTalkQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest, user_id): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + user = get_object_or_none(User, id=user_id) + if user is None: + logger.error(f'DingTalkQR bind callback error, user_id invalid: user_id={user_id}') + msg = _('Invalid user_id') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + if user.dingtalk_id: + response = self.get_already_bound_response(redirect_url) + return response + + dingtalk = DingTalk( + appid=settings.DINGTALK_APPKEY, + appsecret=settings.DINGTALK_APPSECRET, + agentid=settings.DINGTALK_AGENTID + ) + userid = dingtalk.get_userid_by_code(code) + + if not userid: + msg = _('DingTalk query user failed') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + user.dingtalk_id = userid + user.save() + + msg = _('Binding DingTalk successfully') + response = self.get_success_reponse(redirect_url, msg, msg) + return response + + +class DingTalkEnableStartView(UserVerifyPasswordView): + + def get_success_url(self): + referer = self.request.META.get('HTTP_REFERER') + redirect_url = self.request.GET.get("redirect_url") + + success_url = reverse('authentication:dingtalk-qr-bind') + + success_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url or referer + }) + + return success_url + + +class DingTalkQRLoginView(DingTalkQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + redirect_url = request.GET.get('redirect_url') + + redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + login_url = reverse('authentication:login') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + dingtalk = DingTalk( + appid=settings.DINGTALK_APPKEY, + appsecret=settings.DINGTALK_APPSECRET, + agentid=settings.DINGTALK_AGENTID + ) + userid = dingtalk.get_userid_by_code(code) + if not userid: + # 正常流程不会出这个错误,hack 行为 + msg = _('Failed to get user from DingTalk') + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + user = get_object_or_none(User, dingtalk_id=userid) + if user is None: + title = _('DingTalk is not bound') + msg = _('Please login with a password and then bind the WoCom') + response = self.get_failed_reponse(login_url, title=title, msg=msg) + return response + + try: + self.check_oauth2_auth(user, settings.AUTH_BACKEND_DINGTALK) + except errors.AuthFailedError as e: + self.set_login_failed_mark() + msg = e.msg + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + return self.redirect_to_guard_view() + + +@method_decorator(never_cache, name='dispatch') +class FlashDingTalkBindSucceedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding DingTalk successfully'), + 'messages': msg or _('Binding DingTalk successfully'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) + + +@method_decorator(never_cache, name='dispatch') +class FlashDingTalkBindFailedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding DingTalk failed'), + 'messages': msg or _('Binding DingTalk failed'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index e1a3ab4b6..083418940 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import os import datetime -from django.core.cache import cache from django.contrib.auth import login as auth_login, logout as auth_logout from django.http import HttpResponse from django.shortcuts import reverse, redirect @@ -19,7 +18,7 @@ from django.conf import settings from django.urls import reverse_lazy from django.contrib.auth import BACKEND_SESSION_KEY -from common.utils import get_request_ip, get_object_or_none +from common.utils import get_request_ip, FlashMessageUtil from users.utils import ( redirect_user_first_login_or_index ) @@ -31,7 +30,6 @@ from ..forms import get_user_login_form_cls __all__ = [ 'UserLoginView', 'UserLogoutView', 'UserLoginGuardView', 'UserLoginWaitConfirmView', - 'FlashPasswdTooSimpleMsgView', 'FlashPasswdHasExpiredMsgView' ] @@ -39,16 +37,45 @@ __all__ = [ @method_decorator(csrf_protect, name='dispatch') @method_decorator(never_cache, name='dispatch') class UserLoginView(mixins.AuthMixin, FormView): - key_prefix_captcha = "_LOGIN_INVALID_{}" redirect_field_name = 'next' template_name = 'authentication/login.html' + def redirect_third_party_auth_if_need(self, request): + # show jumpserver login page if request http://{JUMP-SERVER}/?admin=1 + if self.request.GET.get("admin", 0): + return None + auth_type = '' + auth_url = '' + if settings.AUTH_OPENID: + auth_type = 'OIDC' + auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) + elif settings.AUTH_CAS: + auth_type = 'CAS' + auth_url = reverse(settings.CAS_LOGIN_URL_NAME) + if not auth_url: + return None + + message_data = { + 'title': _('Redirecting'), + 'message': _("Redirecting to {} authentication").format(auth_type), + 'redirect_url': auth_url, + 'has_cancel': True, + 'cancel_url': reverse('authentication:login') + '?admin=1' + } + redirect_url = FlashMessageUtil.gen_message_url(message_data) + query_string = request.GET.urlencode() + redirect_url = "{}&{}".format(redirect_url, query_string) + return redirect_url + def get(self, request, *args, **kwargs): if request.user.is_staff: first_login_url = redirect_user_first_login_or_index( request, self.redirect_field_name ) return redirect(first_login_url) + redirect_url = self.redirect_third_party_auth_if_need(request) + if redirect_url: + return redirect(redirect_url) request.session.set_test_cookie() return super().get(request, *args, **kwargs) @@ -61,31 +88,22 @@ class UserLoginView(mixins.AuthMixin, FormView): try: self.check_user_auth(decrypt_passwd=True) except errors.AuthFailedError as e: - e = self.check_is_block(raise_exception=False) or e form.add_error(None, e.msg) - ip = self.get_request_ip() - cache.set(self.key_prefix_captcha.format(ip), 1, 3600) + self.set_login_failed_mark() + form_cls = get_user_login_form_cls(captcha=True) new_form = form_cls(data=form.data) new_form._errors = form.errors context = self.get_context_data(form=new_form) self.request.session.set_test_cookie() return self.render_to_response(context) - except (errors.PasswdTooSimple, errors.PasswordRequireResetError) as e: + except (errors.PasswdTooSimple, errors.PasswordRequireResetError, errors.PasswdNeedUpdate) as e: return redirect(e.url) self.clear_rsa_key() return self.redirect_to_guard_view() - def redirect_to_guard_view(self): - guard_url = reverse('authentication:login-guard') - args = self.request.META.get('QUERY_STRING', '') - if args: - guard_url = "%s?%s" % (guard_url, args) - return redirect(guard_url) - def get_form_class(self): - ip = get_request_ip(self.request) - if cache.get(self.key_prefix_captcha.format(ip)): + if self.check_is_need_captcha(): return get_user_login_form_cls(captcha=True) else: return get_user_login_form_cls() @@ -113,6 +131,8 @@ class UserLoginView(mixins.AuthMixin, FormView): 'demo_mode': os.environ.get("DEMO_MODE"), 'AUTH_OPENID': settings.AUTH_OPENID, 'AUTH_CAS': settings.AUTH_CAS, + 'AUTH_WECOM': settings.AUTH_WECOM, + 'AUTH_DINGTALK': settings.AUTH_DINGTALK, 'rsa_public_key': rsa_public_key, 'forgot_password_url': forgot_password_url } @@ -218,39 +238,9 @@ class UserLogoutView(TemplateView): context = { 'title': _('Logout success'), 'messages': _('Logout success, return login page'), - 'interval': 1, + 'interval': 3, 'redirect_url': reverse('authentication:login'), 'auto_redirect': True, } kwargs.update(context) return super().get_context_data(**kwargs) - - -@method_decorator(never_cache, name='dispatch') -class FlashPasswdTooSimpleMsgView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get(self, request, *args, **kwargs): - context = { - 'title': _('Please change your password'), - 'messages': _('Your password is too simple, please change it for security'), - 'interval': 5, - 'redirect_url': request.GET.get('redirect_url'), - 'auto_redirect': True, - } - return self.render_to_response(context) - - -@method_decorator(never_cache, name='dispatch') -class FlashPasswdHasExpiredMsgView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get(self, request, *args, **kwargs): - context = { - 'title': _('Please change your password'), - 'messages': _('Your password has expired, please reset before logging in'), - 'interval': 5, - 'redirect_url': request.GET.get('redirect_url'), - 'auto_redirect': True, - } - return self.render_to_response(context) diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py new file mode 100644 index 000000000..6d9592c8a --- /dev/null +++ b/apps/authentication/views/wecom.py @@ -0,0 +1,241 @@ +import urllib + +from django.http.response import HttpResponseRedirect +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext as _ +from django.views.decorators.cache import never_cache +from django.views.generic import TemplateView +from django.views import View +from django.conf import settings +from django.http.request import HttpRequest +from rest_framework.permissions import IsAuthenticated, AllowAny + +from users.views import UserVerifyPasswordView +from users.utils import is_auth_password_time_valid +from users.models import User +from common.utils import get_logger +from common.utils.random import random_string +from common.utils.django import reverse, get_object_or_none +from common.message.backends.wecom import URL +from common.message.backends.wecom import WeCom +from common.mixins.views import PermissionsMixin +from authentication import errors +from authentication.mixins import AuthMixin + +logger = get_logger(__file__) + + +WECOM_STATE_SESSION_KEY = '_wecom_state' + + +class WeComQRMixin(PermissionsMixin, View): + def verify_state(self): + state = self.request.GET.get('state') + session_state = self.request.session.get(WECOM_STATE_SESSION_KEY) + if state != session_state: + return False + return True + + def get_verify_state_failed_response(self, redirect_uri): + msg = _("You've been hacked") + return self.get_failed_reponse(redirect_uri, msg, msg) + + def get_qr_url(self, redirect_uri): + state = random_string(16) + self.request.session[WECOM_STATE_SESSION_KEY] = state + + params = { + 'appid': settings.WECOM_CORPID, + 'agentid': settings.WECOM_AGENTID, + 'state': state, + 'redirect_uri': redirect_uri, + } + url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params) + return url + + def get_success_reponse(self, redirect_url, title, msg): + ok_flash_msg_url = reverse('authentication:wecom-bind-success-flash-msg') + ok_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(ok_flash_msg_url) + + def get_failed_reponse(self, redirect_url, title, msg): + failed_flash_msg_url = reverse('authentication:wecom-bind-failed-flash-msg') + failed_flash_msg_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url, + 'title': title, + 'msg': msg + }) + return HttpResponseRedirect(failed_flash_msg_url) + + def get_already_bound_response(self, redirect_url): + msg = _('WeCom is already bound') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + +class WeComQRBindView(WeComQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest): + user = request.user + redirect_url = request.GET.get('redirect_url') + + if not is_auth_password_time_valid(request.session): + msg = _('Please verify your password first') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + redirect_uri = reverse('authentication:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class WeComQRBindCallbackView(WeComQRMixin, View): + permission_classes = (IsAuthenticated,) + + def get(self, request: HttpRequest, user_id): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + user = get_object_or_none(User, id=user_id) + if user is None: + logger.error(f'WeComQR bind callback error, user_id invalid: user_id={user_id}') + msg = _('Invalid user_id') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + if user.wecom_id: + response = self.get_already_bound_response(redirect_url) + return response + + wecom = WeCom( + corpid=settings.WECOM_CORPID, + corpsecret=settings.WECOM_SECRET, + agentid=settings.WECOM_AGENTID + ) + wecom_userid, __ = wecom.get_user_id_by_code(code) + if not wecom_userid: + msg = _('WeCom query user failed') + response = self.get_failed_reponse(redirect_url, msg, msg) + return response + + user.wecom_id = wecom_userid + user.save() + + msg = _('Binding WeCom successfully') + response = self.get_success_reponse(redirect_url, msg, msg) + return response + + +class WeComEnableStartView(UserVerifyPasswordView): + + def get_success_url(self): + referer = self.request.META.get('HTTP_REFERER') + redirect_url = self.request.GET.get("redirect_url") + + success_url = reverse('authentication:wecom-qr-bind') + + success_url += '?' + urllib.parse.urlencode({ + 'redirect_url': redirect_url or referer + }) + + return success_url + + +class WeComQRLoginView(WeComQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + redirect_url = request.GET.get('redirect_url') + + redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True) + redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url}) + + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: HttpRequest): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + login_url = reverse('authentication:login') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + wecom = WeCom( + corpid=settings.WECOM_CORPID, + corpsecret=settings.WECOM_SECRET, + agentid=settings.WECOM_AGENTID + ) + wecom_userid, __ = wecom.get_user_id_by_code(code) + if not wecom_userid: + # 正常流程不会出这个错误,hack 行为 + msg = _('Failed to get user from WeCom') + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + user = get_object_or_none(User, wecom_id=wecom_userid) + if user is None: + title = _('WeCom is not bound') + msg = _('Please login with a password and then bind the WoCom') + response = self.get_failed_reponse(login_url, title=title, msg=msg) + return response + + try: + self.check_oauth2_auth(user, settings.AUTH_BACKEND_WECOM) + except errors.AuthFailedError as e: + self.set_login_failed_mark() + msg = e.msg + response = self.get_failed_reponse(login_url, title=msg, msg=msg) + return response + + return self.redirect_to_guard_view() + + +@method_decorator(never_cache, name='dispatch') +class FlashWeComBindSucceedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding WeCom successfully'), + 'messages': msg or _('Binding WeCom successfully'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) + + +@method_decorator(never_cache, name='dispatch') +class FlashWeComBindFailedMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + title = request.GET.get('title') + msg = request.GET.get('msg') + + context = { + 'title': title or _('Binding WeCom failed'), + 'messages': msg or _('Binding WeCom failed'), + 'interval': 5, + 'redirect_url': request.GET.get('redirect_url'), + 'auto_redirect': True, + } + return self.render_to_response(context) diff --git a/apps/common/exceptions.py b/apps/common/exceptions.py index 3422f0e0c..9d3008c50 100644 --- a/apps/common/exceptions.py +++ b/apps/common/exceptions.py @@ -39,3 +39,9 @@ class ReferencedByOthers(JMSException): status_code = status.HTTP_400_BAD_REQUEST default_code = 'referenced_by_others' default_detail = _('Is referenced by other objects and cannot be deleted') + + +class MFAVerifyRequired(JMSException): + status_code = status.HTTP_400_BAD_REQUEST + default_code = 'mfa_verify_required' + default_detail = _('This action require verify your MFA') diff --git a/apps/common/management/commands/expire_caches.py b/apps/common/management/commands/expire_caches.py new file mode 100644 index 000000000..fb09f47eb --- /dev/null +++ b/apps/common/management/commands/expire_caches.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from assets.signals_handler.node_assets_mapping import expire_node_assets_mapping_for_memory +from orgs.models import Organization + + +def expire_node_assets_mapping(): + org_ids = Organization.objects.all().values_list('id', flat=True) + org_ids = [*org_ids, '00000000-0000-0000-0000-000000000000'] + + for org_id in org_ids: + expire_node_assets_mapping_for_memory(org_id) + + +class Command(BaseCommand): + help = 'Expire caches' + + def handle(self, *args, **options): + expire_node_assets_mapping() diff --git a/apps/common/message/__init__.py b/apps/common/message/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/common/message/backends/__init__.py b/apps/common/message/backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/common/message/backends/dingtalk/__init__.py b/apps/common/message/backends/dingtalk/__init__.py new file mode 100644 index 000000000..0ca9d5dc5 --- /dev/null +++ b/apps/common/message/backends/dingtalk/__init__.py @@ -0,0 +1,168 @@ +import time +import hmac +import base64 + +from common.message.backends.utils import request +from common.message.backends.utils import digest +from common.message.backends.mixin import BaseRequest + + +def sign(secret, data): + + digest = hmac.HMAC( + key=secret.encode('utf8'), + msg=data.encode('utf8'), + digestmod=hmac._hashlib.sha256).digest() + signature = base64.standard_b64encode(digest).decode('utf8') + # signature = urllib.parse.quote(signature, safe='') + # signature = signature.replace('+', '%20').replace('*', '%2A').replace('~', '%7E').replace('/', '%2F') + return signature + + +class ErrorCode: + INVALID_TOKEN = 88 + + +class URL: + QR_CONNECT = 'https://oapi.dingtalk.com/connect/qrconnect' + GET_USER_INFO_BY_CODE = 'https://oapi.dingtalk.com/sns/getuserinfo_bycode' + GET_TOKEN = 'https://oapi.dingtalk.com/gettoken' + SEND_MESSAGE_BY_TEMPLATE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/sendbytemplate' + SEND_MESSAGE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2' + GET_SEND_MSG_PROGRESS = 'https://oapi.dingtalk.com/topapi/message/corpconversation/getsendprogress' + GET_USERID_BY_UNIONID = 'https://oapi.dingtalk.com/topapi/user/getbyunionid' + + +class DingTalkRequests(BaseRequest): + invalid_token_errcode = ErrorCode.INVALID_TOKEN + + def __init__(self, appid, appsecret, agentid, timeout=None): + self._appid = appid + self._appsecret = appsecret + self._agentid = agentid + + super().__init__(timeout=timeout) + + def get_access_token_cache_key(self): + return digest(self._appid, self._appsecret) + + def request_access_token(self): + # https://developers.dingtalk.com/document/app/obtain-orgapp-token?spm=ding_open_doc.document.0.0.3a256573JEWqIL#topic-1936350 + params = {'appkey': self._appid, 'appsecret': self._appsecret} + data = self.raw_request('get', url=URL.GET_TOKEN, params=params) + + access_token = data['access_token'] + expires_in = data['expires_in'] + return access_token, expires_in + + @request + def get(self, url, params=None, + with_token=False, with_sign=False, + check_errcode_is_0=True, + **kwargs): + pass + + @request + def post(self, url, json=None, params=None, + with_token=False, with_sign=False, + check_errcode_is_0=True, + **kwargs): + pass + + def _add_sign(self, params: dict): + timestamp = str(int(time.time() * 1000)) + signature = sign(self._appsecret, timestamp) + accessKey = self._appid + + params['timestamp'] = timestamp + params['signature'] = signature + params['accessKey'] = accessKey + + def request(self, method, url, params=None, + with_token=False, with_sign=False, + check_errcode_is_0=True, + **kwargs): + if not isinstance(params, dict): + params = {} + + if with_token: + params['access_token'] = self.access_token + + if with_sign: + self._add_sign(params) + + data = self.raw_request(method, url, params=params, **kwargs) + if check_errcode_is_0: + self.check_errcode_is_0(data) + + return data + + +class DingTalk: + def __init__(self, appid, appsecret, agentid, timeout=None): + self._appid = appid + self._appsecret = appsecret + self._agentid = agentid + + self._request = DingTalkRequests( + appid=appid, appsecret=appsecret, agentid=agentid, + timeout=timeout + ) + + def get_userinfo_bycode(self, code): + # https://developers.dingtalk.com/document/app/obtain-the-user-information-based-on-the-sns-temporary-authorization?spm=ding_open_doc.document.0.0.3a256573y8Y7yg#topic-1995619 + body = { + "tmp_auth_code": code + } + + data = self._request.post(URL.GET_USER_INFO_BY_CODE, json=body, with_sign=True) + return data['user_info'] + + def get_userid_by_code(self, code): + user_info = self.get_userinfo_bycode(code) + unionid = user_info['unionid'] + userid = self.get_userid_by_unionid(unionid) + return userid + + def get_userid_by_unionid(self, unionid): + body = { + 'unionid': unionid + } + data = self._request.post(URL.GET_USERID_BY_UNIONID, json=body, with_token=True) + userid = data['result']['userid'] + return userid + + def send_by_template(self, template_id, user_ids, dept_ids, data): + body = { + 'agent_id': self._agentid, + 'template_id': template_id, + 'userid_list': ','.join(user_ids), + 'dept_id_list': ','.join(dept_ids), + 'data': data + } + data = self._request.post(URL.SEND_MESSAGE_BY_TEMPLATE, json=body, with_token=True) + + def send_text(self, user_ids, msg): + body = { + 'agent_id': self._agentid, + 'userid_list': ','.join(user_ids), + # 'dept_id_list': '', + 'to_all_user': False, + 'msg': { + 'msgtype': 'text', + 'text': { + 'content': msg + } + } + } + data = self._request.post(URL.SEND_MESSAGE, json=body, with_token=True) + return data + + def get_send_msg_progress(self, task_id): + body = { + 'agent_id': self._agentid, + 'task_id': task_id + } + + data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True) + return data diff --git a/apps/common/message/backends/exceptions.py b/apps/common/message/backends/exceptions.py new file mode 100644 index 000000000..f72e8694d --- /dev/null +++ b/apps/common/message/backends/exceptions.py @@ -0,0 +1,28 @@ +from django.utils.translation import gettext_lazy as _ + +from rest_framework.exceptions import APIException + + +class HTTPNot200(APIException): + default_code = 'http_not_200' + default_detail = 'HTTP status is not 200' + + +class ErrCodeNot0(APIException): + default_code = 'errcode_not_0' + default_detail = 'Error code is not 0' + + +class ResponseDataKeyError(APIException): + default_code = 'response_data_key_error' + default_detail = 'Response data key error' + + +class NetError(APIException): + default_code = 'net_error' + default_detail = _('Network error, please contact system administrator') + + +class AccessTokenError(APIException): + default_code = 'access_token_error' + default_detail = 'Access token error, check config' diff --git a/apps/common/message/backends/mixin.py b/apps/common/message/backends/mixin.py new file mode 100644 index 000000000..3beb60272 --- /dev/null +++ b/apps/common/message/backends/mixin.py @@ -0,0 +1,94 @@ +import requests +from requests import exceptions as req_exce +from rest_framework.exceptions import PermissionDenied +from django.core.cache import cache + +from .utils import DictWrapper +from common.utils.common import get_logger +from common.utils import lazyproperty +from common.message.backends.utils import set_default + +from . import exceptions as exce + +logger = get_logger(__name__) + + +class RequestMixin: + def check_errcode_is_0(self, data: DictWrapper): + errcode = data['errcode'] + if errcode != 0: + # 如果代码写的对,配置没问题,这里不该出错,系统性错误,直接抛异常 + errmsg = data['errmsg'] + logger.error(f'Response 200 but errcode is not 0: ' + f'errcode={errcode} ' + f'errmsg={errmsg} ') + raise exce.ErrCodeNot0(detail=str(data.raw_data)) + + def check_http_is_200(self, response): + if response.status_code != 200: + # 正常情况下不会返回非 200 响应码 + logger.error(f'Response error: ' + f'status_code={response.status_code} ' + f'url={response.url}' + f'\ncontent={response.content}') + raise exce.HTTPNot200 + + +class BaseRequest(RequestMixin): + invalid_token_errcode = -1 + + def __init__(self, timeout=None): + self._request_kwargs = { + 'timeout': timeout + } + self.init_access_token() + + def request_access_token(self): + raise NotImplementedError + + def get_access_token_cache_key(self): + raise NotImplementedError + + def is_token_invalid(self, data): + errcode = data['errcode'] + if errcode == self.invalid_token_errcode: + return True + return False + + @lazyproperty + def access_token_cache_key(self): + return self.get_access_token_cache_key() + + def init_access_token(self): + access_token = cache.get(self.access_token_cache_key) + if access_token: + self.access_token = access_token + return + self.refresh_access_token() + + def refresh_access_token(self): + access_token, expires_in = self.request_access_token() + self.access_token = access_token + cache.set(self.access_token_cache_key, access_token, expires_in) + + def raw_request(self, method, url, **kwargs): + set_default(kwargs, self._request_kwargs) + raw_data = '' + for i in range(3): + # 循环为了防止 access_token 失效 + try: + response = getattr(requests, method)(url, **kwargs) + self.check_http_is_200(response) + raw_data = response.json() + data = DictWrapper(raw_data) + + if self.is_token_invalid(data): + self.refresh_access_token() + continue + + return data + except req_exce.ReadTimeout as e: + logger.exception(e) + raise exce.NetError + logger.error(f'Get access_token error, check config: url={url} data={raw_data}') + raise PermissionDenied(raw_data) diff --git a/apps/common/message/backends/utils.py b/apps/common/message/backends/utils.py new file mode 100644 index 000000000..6c6f2b593 --- /dev/null +++ b/apps/common/message/backends/utils.py @@ -0,0 +1,78 @@ +import hashlib +import inspect +from inspect import Parameter + +from common.utils.common import get_logger +from common.message.backends import exceptions as exce + +logger = get_logger(__name__) + + +def digest(corpid, corpsecret): + md5 = hashlib.md5() + md5.update(corpid.encode()) + md5.update(corpsecret.encode()) + digest = md5.hexdigest() + return digest + + +def update_values(default: dict, others: dict): + for key in default.keys(): + if key in others: + default[key] = others[key] + + +def set_default(data: dict, default: dict): + for key in default.keys(): + if key not in data: + data[key] = default[key] + + +class DictWrapper: + def __init__(self, data:dict): + self.raw_data = data + + def __getitem__(self, item): + # 网络请求返回的数据,不能完全信任,所以字典操作包在异常里 + try: + return self.raw_data[item] + except KeyError as e: + msg = f'Response 200 but get field from json error: error={e} data={self.raw_data}' + logger.error(msg) + raise exce.ResponseDataKeyError(detail=msg) + + def __getattr__(self, item): + return getattr(self.raw_data, item) + + def __contains__(self, item): + return item in self.raw_data + + def __str__(self): + return str(self.raw_data) + + def __repr__(self): + return str(self.raw_data) + + +def request(func): + def inner(*args, **kwargs): + signature = inspect.signature(func) + bound_args = signature.bind(*args, **kwargs) + bound_args.apply_defaults() + + arguments = bound_args.arguments + self = arguments['self'] + request_method = func.__name__ + + parameters = {} + for k, v in signature.parameters.items(): + if k == 'self': + continue + if v.kind is Parameter.VAR_KEYWORD: + parameters.update(arguments[k]) + continue + parameters[k] = arguments[k] + + response = self.request(request_method, **parameters) + return response + return inner diff --git a/apps/common/message/backends/wecom/__init__.py b/apps/common/message/backends/wecom/__init__.py new file mode 100644 index 000000000..257da47a0 --- /dev/null +++ b/apps/common/message/backends/wecom/__init__.py @@ -0,0 +1,194 @@ +from typing import Iterable, AnyStr + +from django.utils.translation import ugettext_lazy as _ +from rest_framework.exceptions import APIException +from requests.exceptions import ReadTimeout +import requests +from django.core.cache import cache + +from common.utils.common import get_logger +from common.message.backends.utils import digest, DictWrapper, update_values, set_default +from common.message.backends.utils import request +from common.message.backends.mixin import RequestMixin, BaseRequest + +logger = get_logger(__name__) + + +class WeComError(APIException): + default_code = 'wecom_error' + default_detail = _('WeCom error, please contact system administrator') + + +class URL: + GET_TOKEN = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken' + SEND_MESSAGE = 'https://qyapi.weixin.qq.com/cgi-bin/message/send' + QR_CONNECT = 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect' + + # https://open.work.weixin.qq.com/api/doc/90000/90135/91437 + GET_USER_ID_BY_CODE = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo' + GET_USER_DETAIL = 'https://qyapi.weixin.qq.com/cgi-bin/user/get' + + +class ErrorCode: + # https://open.work.weixin.qq.com/api/doc/90000/90139/90313#%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A81013 + RECIPIENTS_INVALID = 81013 # UserID、部门ID、标签ID全部非法或无权限。 + + # https://open.work.weixin.qq.com/api/doc/90000/90135/91437 + INVALID_CODE = 40029 + + INVALID_TOKEN = 40014 # 无效的 access_token + + +class WeComRequests(BaseRequest): + """ + 处理系统级错误,抛出 API 异常,直接生成 HTTP 响应,业务代码无需关心这些错误 + - 确保 status_code == 200 + - 确保 access_token 无效时重试 + """ + invalid_token_errcode = ErrorCode.INVALID_TOKEN + + def __init__(self, corpid, corpsecret, agentid, timeout=None): + self._corpid = corpid + self._corpsecret = corpsecret + self._agentid = agentid + + super().__init__(timeout=timeout) + + def get_access_token_cache_key(self): + return digest(self._corpid, self._corpsecret) + + def request_access_token(self): + params = {'corpid': self._corpid, 'corpsecret': self._corpsecret} + data = self.raw_request('get', url=URL.GET_TOKEN, params=params) + + access_token = data['access_token'] + expires_in = data['expires_in'] + return access_token, expires_in + + @request + def get(self, url, params=None, with_token=True, + check_errcode_is_0=True, **kwargs): + # self.request ... + pass + + @request + def post(self, url, params=None, json=None, + with_token=True, check_errcode_is_0=True, + **kwargs): + # self.request ... + pass + + def request(self, method, url, + params=None, + with_token=True, + check_errcode_is_0=True, + **kwargs): + + if not isinstance(params, dict): + params = {} + + if with_token: + params['access_token'] = self.access_token + + data = self.raw_request(method, url, params=params, **kwargs) + if check_errcode_is_0: + self.check_errcode_is_0(data) + return data + + +class WeCom(RequestMixin): + """ + 非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会 + """ + + def __init__(self, corpid, corpsecret, agentid, timeout=None): + self._corpid = corpid + self._corpsecret = corpsecret + self._agentid = agentid + + self._requests = WeComRequests( + corpid=corpid, + corpsecret=corpsecret, + agentid=agentid, + timeout=timeout + ) + + def send_text(self, users: Iterable, msg: AnyStr, **kwargs): + """ + https://open.work.weixin.qq.com/api/doc/90000/90135/90236 + + 对于业务代码,只需要关心由 用户id 或 消息不对 导致的错误,其他错误不予理会 + """ + users = tuple(users) + + extra_params = { + "safe": 0, + "enable_id_trans": 0, + "enable_duplicate_check": 0, + "duplicate_check_interval": 1800 + } + update_values(extra_params, kwargs) + + body = { + "touser": '|'.join(users), + "msgtype": "text", + "agentid": self._agentid, + "text": { + "content": msg + }, + **extra_params + } + data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False) + + errcode = data['errcode'] + if errcode == ErrorCode.RECIPIENTS_INVALID: + # 全部接收人无权限或不存在 + return users + self.check_errcode_is_0(data) + + invaliduser = data['invaliduser'] + if not invaliduser: + return () + + if isinstance(invaliduser, str): + logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invaliduser}') + raise WeComError + + invalid_users = invaliduser.split('|') + return invalid_users + + def get_user_id_by_code(self, code): + # # https://open.work.weixin.qq.com/api/doc/90000/90135/91437 + + params = { + 'code': code, + } + data = self._requests.get(URL.GET_USER_ID_BY_CODE, params=params, check_errcode_is_0=False) + + errcode = data['errcode'] + if errcode == ErrorCode.INVALID_CODE: + logger.warn(f'WeCom get_user_id_by_code invalid code: code={code}') + return None, None + + self.check_errcode_is_0(data) + + USER_ID = 'UserId' + OPEN_ID = 'OpenId' + + if USER_ID in data: + return data[USER_ID], USER_ID + elif OPEN_ID in data: + return data[OPEN_ID], OPEN_ID + else: + logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId') + raise WeComError + + def get_user_detail(self, id): + # https://open.work.weixin.qq.com/api/doc/90000/90135/90196 + + params = { + 'userid': id, + } + + data = self._requests.get(URL.GET_USER_DETAIL, params) + return data diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index ea629bb84..a0d5875c6 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -6,10 +6,12 @@ from threading import Thread from collections import defaultdict from itertools import chain +from django.conf import settings from django.db.models.signals import m2m_changed from django.core.cache import cache from django.http import JsonResponse from django.utils.translation import ugettext as _ +from django.contrib.auth import get_user_model from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.decorators import action @@ -25,6 +27,9 @@ __all__ = [ ] +UserModel = get_user_model() + + class JSONResponseMixin(object): """JSON mixin""" @staticmethod @@ -332,3 +337,21 @@ class AllowBulkDestoryMixin: """ query = str(filtered.query) return '`id` IN (' in query or '`id` =' in query + + +class RoleAdminMixin: + kwargs: dict + user_id_url_kwarg = 'pk' + + @lazyproperty + def user(self): + user_id = self.kwargs.get(self.user_id_url_kwarg) + return UserModel.objects.get(id=user_id) + + +class RoleUserMixin: + request: Request + + @lazyproperty + def user(self): + return self.request.user diff --git a/apps/common/mixins/views.py b/apps/common/mixins/views.py index 13f365662..1590f48ba 100644 --- a/apps/common/mixins/views.py +++ b/apps/common/mixins/views.py @@ -5,7 +5,7 @@ from django.contrib.auth.mixins import UserPassesTestMixin from django.utils import timezone -__all__ = ["DatetimeSearchMixin"] +__all__ = ["DatetimeSearchMixin", "PermissionsMixin"] from rest_framework import permissions @@ -51,4 +51,4 @@ class PermissionsMixin(UserPassesTestMixin): for permission_class in permission_classes: if not permission_class().has_permission(self.request, self): return False - return True \ No newline at end of file + return True diff --git a/apps/common/permissions.py b/apps/common/permissions.py index 7df83046d..1fced6478 100644 --- a/apps/common/permissions.py +++ b/apps/common/permissions.py @@ -3,6 +3,7 @@ import time from rest_framework import permissions from django.conf import settings +from common.exceptions import MFAVerifyRequired from orgs.utils import current_org @@ -114,7 +115,7 @@ class NeedMFAVerify(permissions.BasePermission): mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0) if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL: return True - return False + raise MFAVerifyRequired() class CanUpdateDeleteUser(permissions.BasePermission): diff --git a/apps/common/request_log.py b/apps/common/request_log.py new file mode 100644 index 000000000..c35e6b84a --- /dev/null +++ b/apps/common/request_log.py @@ -0,0 +1,15 @@ +from django.http.request import HttpRequest +from django.http.response import HttpResponse + +from orgs.utils import current_org + + +class RequestLogMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request: HttpRequest): + print(f'Request {request.method} --> ', request.get_raw_uri()) + response: HttpResponse = self.get_response(request) + print(f'Response {current_org.name} {request.method} {response.status_code} --> ', request.get_raw_uri()) + return response diff --git a/apps/common/urls/view_urls.py b/apps/common/urls/view_urls.py new file mode 100644 index 000000000..bfbd97053 --- /dev/null +++ b/apps/common/urls/view_urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from .. import views + +app_name = 'common' + +urlpatterns = [ + # login + path('flash-message/', views.FlashMessageMsgView.as_view(), name='flash-message'), +] \ No newline at end of file diff --git a/apps/common/utils/__init__.py b/apps/common/utils/__init__.py index 8b4576221..0fdcf0dd1 100644 --- a/apps/common/utils/__init__.py +++ b/apps/common/utils/__init__.py @@ -8,3 +8,4 @@ from .http import * from .ipip import * from .crypto import * from .random import * +from .jumpserver import * \ No newline at end of file diff --git a/apps/common/utils/encode.py b/apps/common/utils/encode.py index cd130e7fe..dc68c9ba8 100644 --- a/apps/common/utils/encode.py +++ b/apps/common/utils/encode.py @@ -75,11 +75,16 @@ def ssh_key_string_to_obj(text, password=None): key = paramiko.RSAKey.from_private_key(StringIO(text), password=password) except paramiko.SSHException: pass + else: + return key try: key = paramiko.DSSKey.from_private_key(StringIO(text), password=password) except paramiko.SSHException: pass + else: + return key + return key diff --git a/apps/common/utils/jumpserver.py b/apps/common/utils/jumpserver.py new file mode 100644 index 000000000..1ae806366 --- /dev/null +++ b/apps/common/utils/jumpserver.py @@ -0,0 +1,31 @@ +from django.core.cache import cache +from django.shortcuts import reverse + +from .random import random_string + + +__all__ = ['FlashMessageUtil'] + + +class FlashMessageUtil: + @staticmethod + def get_key(code): + key = 'MESSAGE_{}'.format(code) + return key + + @classmethod + def get_message_code(cls, message_data): + code = random_string(12) + key = cls.get_key(code) + cache.set(key, message_data, 60) + return code + + @classmethod + def get_message_by_code(cls, code): + key = cls.get_key(code) + return cache.get(key) + + @classmethod + def gen_message_url(cls, message_data): + code = cls.get_message_code(message_data) + return reverse('common:flash-message') + f'?code={code}' diff --git a/apps/common/utils/random.py b/apps/common/utils/random.py index 055966947..a9ef0421f 100644 --- a/apps/common/utils/random.py +++ b/apps/common/utils/random.py @@ -4,7 +4,6 @@ import struct import random import socket import string -import secrets string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_{}~' diff --git a/apps/common/views.py b/apps/common/views.py new file mode 100644 index 000000000..79d628248 --- /dev/null +++ b/apps/common/views.py @@ -0,0 +1,40 @@ +# +from django.utils.decorators import method_decorator +from django.views.decorators.cache import never_cache +from django.http import HttpResponse +from django.views.generic.base import TemplateView + +from common.utils import bulk_get, FlashMessageUtil + + +@method_decorator(never_cache, name='dispatch') +class FlashMessageMsgView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + code = request.GET.get('code') + if not code: + return HttpResponse('Not found the code') + + message_data = FlashMessageUtil.get_message_by_code(code) + if not message_data: + return HttpResponse('Message code error') + + title, message, redirect_url, confirm_button, cancel_url = bulk_get( + message_data, 'title', 'message', 'redirect_url', 'confirm_button', 'cancel_url' + ) + + interval = message_data.get('interval', 3) + auto_redirect = message_data.get('auto_redirect', True) + has_cancel = message_data.get('has_cancel', False) + context = { + 'title': title, + 'messages': message, + 'interval': interval, + 'redirect_url': redirect_url, + 'auto_redirect': auto_redirect, + 'confirm_button': confirm_button, + 'has_cancel': has_cancel, + 'cancel_url': cancel_url, + } + return self.render_to_response(context) \ No newline at end of file diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index bda2537c8..4e734bda6 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -18,6 +18,7 @@ from terminal.utils import ComponentsPrometheusMetricsUtil from orgs.utils import current_org from common.permissions import IsOrgAdmin, IsOrgAuditor from common.utils import lazyproperty +from orgs.caches import OrgResourceStatisticsCache __all__ = ['IndexApi'] @@ -99,7 +100,7 @@ class DatesLoginMetricMixin: if count is not None: return count ds, de = self.get_date_start_2_end(date) - count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user', flat=True))) + count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user_id', flat=True))) self.__set_data_to_cache(date, tp, count) return count @@ -129,7 +130,7 @@ class DatesLoginMetricMixin: @lazyproperty def dates_total_count_active_users(self): - count = len(set(self.sessions_queryset.values_list('user', flat=True))) + count = len(set(self.sessions_queryset.values_list('user_id', flat=True))) return count @lazyproperty @@ -161,10 +162,10 @@ class DatesLoginMetricMixin: @lazyproperty def dates_total_count_disabled_assets(self): return Asset.objects.filter(is_active=False).count() - + # 以下是从week中而来 def get_dates_login_times_top5_users(self): - users = self.sessions_queryset.values_list('user', flat=True) + users = self.sessions_queryset.values_list('user_id', flat=True) users = [ {'user': user, 'total': total} for user, total in Counter(users).most_common(5) @@ -172,7 +173,7 @@ class DatesLoginMetricMixin: return users def get_dates_total_count_login_users(self): - return len(set(self.sessions_queryset.values_list('user', flat=True))) + return len(set(self.sessions_queryset.values_list('user_id', flat=True))) def get_dates_total_count_login_times(self): return self.sessions_queryset.count() @@ -186,8 +187,8 @@ class DatesLoginMetricMixin: return list(assets) def get_dates_login_times_top10_users(self): - users = self.sessions_queryset.values("user") \ - .annotate(total=Count("user")) \ + users = self.sessions_queryset.values("user_id") \ + .annotate(total=Count("user_id")) \ .annotate(last=Max("date_start")).order_by("-total")[:10] for user in users: user['last'] = str(user['last']) @@ -210,26 +211,7 @@ class DatesLoginMetricMixin: return sessions -class TotalCountMixin: - @staticmethod - def get_total_count_users(): - return current_org.get_members().count() - - @staticmethod - def get_total_count_assets(): - return Asset.objects.all().count() - - @staticmethod - def get_total_count_online_users(): - count = len(set(Session.objects.filter(is_finished=False).values_list('user', flat=True))) - return count - - @staticmethod - def get_total_count_online_sessions(): - return Session.objects.filter(is_finished=False).count() - - -class IndexApi(TotalCountMixin, DatesLoginMetricMixin, APIView): +class IndexApi(DatesLoginMetricMixin, APIView): permission_classes = (IsOrgAdmin | IsOrgAuditor,) http_method_names = ['get'] @@ -238,26 +220,28 @@ class IndexApi(TotalCountMixin, DatesLoginMetricMixin, APIView): query_params = self.request.query_params + caches = OrgResourceStatisticsCache(self.request.user.user_orgs[0]) + _all = query_params.get('all') if _all or query_params.get('total_count') or query_params.get('total_count_users'): data.update({ - 'total_count_users': self.get_total_count_users(), + 'total_count_users': caches.users_amount, }) if _all or query_params.get('total_count') or query_params.get('total_count_assets'): data.update({ - 'total_count_assets': self.get_total_count_assets(), + 'total_count_assets': caches.assets_amount, }) if _all or query_params.get('total_count') or query_params.get('total_count_online_users'): data.update({ - 'total_count_online_users': self.get_total_count_online_users(), + 'total_count_online_users': caches.total_count_online_users, }) if _all or query_params.get('total_count') or query_params.get('total_count_online_sessions'): data.update({ - 'total_count_online_sessions': self.get_total_count_online_sessions(), + 'total_count_online_sessions': caches.total_count_online_sessions, }) if _all or query_params.get('dates_metrics'): diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 179376828..7bb8dd095 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -216,6 +216,16 @@ 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': '', + 'OTP_VALID_WINDOW': 2, 'OTP_ISSUER_NAME': 'JumpServer', 'EMAIL_SUFFIX': 'jumpserver.org', @@ -259,6 +269,7 @@ class Config(dict): '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', diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index ad8c8c90f..fbd7016d3 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -14,6 +14,8 @@ def jumpserver_processor(request): 'LOGIN_IMAGE_URL': static('img/login_image.png'), 'FAVICON_URL': static('img/facio.ico'), 'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'), + 'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_log.png'), + 'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_log.png'), 'JMS_TITLE': _('JumpServer Open Source Bastion Host'), 'VERSION': settings.VERSION, 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021', diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 579a0e66f..28f65a643 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -101,6 +101,19 @@ CAS_CHECK_NEXT = lambda _next_page: True AUTH_SSO = CONFIG.AUTH_SSO AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL +# WECOM Auth +AUTH_WECOM = CONFIG.AUTH_WECOM +WECOM_CORPID = CONFIG.WECOM_CORPID +WECOM_AGENTID = CONFIG.WECOM_AGENTID +WECOM_SECRET = CONFIG.WECOM_SECRET + +# DingDing auth +AUTH_DINGTALK = CONFIG.AUTH_DINGTALK +DINGTALK_AGENTID = CONFIG.DINGTALK_AGENTID +DINGTALK_APPKEY = CONFIG.DINGTALK_APPKEY +DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET + + # Other setting TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE @@ -115,6 +128,8 @@ AUTH_BACKEND_OIDC_CODE = 'jms_oidc_rp.backends.OIDCAuthCodeBackend' AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend' AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend' AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication' +AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication' +AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication' AUTHENTICATION_BACKENDS = [AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY] @@ -128,6 +143,10 @@ if AUTH_RADIUS: AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_RADIUS) if AUTH_SSO: AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_SSO) +if AUTH_WECOM: + AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_WECOM) +if AUTH_DINGTALK: + AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_DINGTALK) ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 936b27582..ed37ea49c 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -38,6 +38,7 @@ SECURITY_LOGIN_LIMIT_TIME = CONFIG.SECURITY_LOGIN_LIMIT_TIME # Unit: minute SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day SECURITY_PASSWORD_MIN_LENGTH = CONFIG.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit +OLD_PASSWORD_HISTORY_LIMIT_COUNT = CONFIG.OLD_PASSWORD_HISTORY_LIMIT_COUNT SECURITY_PASSWORD_UPPER_CASE = CONFIG.SECURITY_PASSWORD_UPPER_CASE SECURITY_PASSWORD_LOWER_CASE = CONFIG.SECURITY_PASSWORD_LOWER_CASE SECURITY_PASSWORD_NUMBER = CONFIG.SECURITY_PASSWORD_NUMBER diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 759c6f271..687b7f2ae 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -23,12 +23,13 @@ api_v1 = [ path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')), path('acls/', include('acls.urls.api_urls', namespace='api-acls')), - path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()) + path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()), ] app_view_patterns = [ path('auth/', include('authentication.urls.view_urls'), name='auth'), 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'), ] diff --git a/apps/jumpserver/views/other.py b/apps/jumpserver/views/other.py index 9ab561c4c..293177615 100644 --- a/apps/jumpserver/views/other.py +++ b/apps/jumpserver/views/other.py @@ -1,17 +1,15 @@ # -*- coding: utf-8 -*- # import re -import time from django.http import HttpResponseRedirect, JsonResponse, Http404 from django.conf import settings from django.views.generic import View from django.shortcuts import redirect from django.utils.translation import ugettext_lazy as _ -from rest_framework.views import APIView -from rest_framework.permissions import AllowAny from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse +from rest_framework.views import APIView from common.http import HttpResponseTemporaryRedirect @@ -85,3 +83,4 @@ class KokoView(View): "
Koko is a separately deployed program, you need to deploy Koko, configure nginx for url distribution,
" "
If you see this page, prove that you are not accessing the nginx listening port. Good luck.
") return HttpResponse(msg) + diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 4e146d83b..71f503ed7 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 49694c4a0..a867a6aaa 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-14 17:52+0800\n" +"POT-Creation-Date: 2021-05-13 15:51+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -25,30 +25,21 @@ msgstr "" #: orgs/models.py:23 perms/models/base.py:49 settings/models.py:29 #: terminal/models/storage.py:23 terminal/models/storage.py:90 #: terminal/models/task.py:16 terminal/models/terminal.py:100 -#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:530 +#: users/forms/profile.py:32 users/models/group.py:15 users/models/user.py:558 #: users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_asset_permission.html:37 #: users/templates/users/user_asset_permission.html:154 #: users/templates/users/user_database_app_permission.html:36 -#: users/templates/users/user_detail.html:49 -#: users/templates/users/user_granted_database_app.html:34 -#: users/templates/users/user_granted_remote_app.html:34 -#: users/templates/users/user_group_detail.html:50 -#: users/templates/users/user_group_list.html:14 -#: users/templates/users/user_list.html:14 -#: users/templates/users/user_profile.html:51 -#: users/templates/users/user_pubkey_update.html:57 -#: users/templates/users/user_remote_app_permission.html:36 #: xpack/plugins/cloud/models.py:28 msgid "Name" msgstr "名称" -#: acls/models/base.py:27 assets/models/cmd_filter.py:53 +#: acls/models/base.py:27 assets/models/cmd_filter.py:54 #: assets/models/user.py:122 msgid "Priority" msgstr "优先级" -#: acls/models/base.py:28 assets/models/cmd_filter.py:53 +#: acls/models/base.py:28 assets/models/cmd_filter.py:54 #: assets/models/user.py:122 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" @@ -56,8 +47,6 @@ 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 -#: users/templates/users/user_detail.html:132 -#: users/templates/users/user_profile.html:63 msgid "Active" msgstr "激活中" @@ -66,29 +55,23 @@ msgstr "激活中" #: acls/models/base.py:32 applications/models/application.py:24 #: assets/models/asset.py:147 assets/models/asset.py:223 #: assets/models/base.py:255 assets/models/cluster.py:29 -#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:57 +#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:64 #: assets/models/domain.py:22 assets/models/domain.py:56 #: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 #: orgs/models.py:26 perms/models/base.py:57 settings/models.py:34 #: terminal/models/storage.py:29 terminal/models/storage.py:96 #: terminal/models/terminal.py:114 tickets/models/ticket.py:73 -#: users/models/group.py:16 users/models/user.py:563 -#: users/templates/users/user_detail.html:115 -#: users/templates/users/user_granted_database_app.html:38 -#: users/templates/users/user_granted_remote_app.html:37 -#: users/templates/users/user_group_detail.html:62 -#: users/templates/users/user_group_list.html:16 -#: users/templates/users/user_profile.html:138 +#: users/models/group.py:16 users/models/user.py:591 #: xpack/plugins/change_auth_plan/models.py:77 xpack/plugins/cloud/models.py:35 #: xpack/plugins/cloud/models.py:98 xpack/plugins/gathered_user/models.py:26 msgid "Comment" msgstr "备注" -#: acls/models/login_acl.py:16 tickets/const.py:18 +#: acls/models/login_acl.py:16 tickets/const.py:19 msgid "Reject" msgstr "拒绝" -#: acls/models/login_acl.py:17 assets/models/cmd_filter.py:47 +#: acls/models/login_acl.py:17 assets/models/cmd_filter.py:48 msgid "Allow" msgstr "允许" @@ -98,42 +81,35 @@ msgstr "登录IP" #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:26 #: acls/serializers/login_acl.py:34 acls/serializers/login_asset_acl.py:75 -#: assets/models/cmd_filter.py:56 audits/models.py:57 +#: 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/user_asset_permission.html:44 #: users/templates/users/user_asset_permission.html:79 #: users/templates/users/user_database_app_permission.html:42 -#: users/templates/users/user_group_list.html:17 -#: users/templates/users/user_list.html:20 -#: users/templates/users/user_remote_app_permission.html:42 msgid "Action" msgstr "动作" #: acls/models/login_acl.py:28 acls/models/login_asset_acl.py:20 #: acls/serializers/login_acl.py:33 assets/models/label.py:15 #: audits/models.py:36 audits/models.py:56 audits/models.py:69 -#: audits/serializers.py:84 authentication/models.py:44 +#: audits/serializers.py:94 authentication/models.py:44 #: authentication/models.py:97 orgs/models.py:18 orgs/models.py:418 #: perms/models/base.py:50 templates/index.html:78 #: terminal/backends/command/models.py:18 -#: terminal/backends/command/serializers.py:12 terminal/models/session.py:37 -#: tickets/models/comment.py:17 users/models/user.py:159 -#: users/models/user.py:707 users/serializers/group.py:20 +#: terminal/backends/command/serializers.py:12 terminal/models/session.py:38 +#: tickets/models/comment.py:17 users/models/user.py:184 +#: users/models/user.py:746 users/models/user.py:772 +#: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 #: users/templates/users/user_database_app_permission.html:37 #: users/templates/users/user_database_app_permission.html:58 -#: users/templates/users/user_group_detail.html:73 -#: users/templates/users/user_group_list.html:15 -#: users/templates/users/user_list.html:135 -#: users/templates/users/user_remote_app_permission.html:37 -#: users/templates/users/user_remote_app_permission.html:58 msgid "User" msgstr "用户" #: acls/models/login_asset_acl.py:17 authentication/models.py:72 -#: tickets/const.py:9 users/templates/users/user_detail.html:250 +#: tickets/const.py:9 msgid "Login confirm" msgstr "登录复核" @@ -144,26 +120,25 @@ msgstr "系统用户" #: acls/models/login_asset_acl.py:22 #: applications/serializers/attrs/application_category/remote_app.py:33 #: assets/models/asset.py:355 assets/models/authbook.py:26 -#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:30 -#: assets/serializers/asset_user.py:47 assets/serializers/asset_user.py:84 -#: assets/serializers/system_user.py:192 audits/models.py:38 +#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:34 +#: assets/serializers/asset_user.py:48 assets/serializers/asset_user.py:89 +#: assets/serializers/system_user.py:195 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:39 +#: 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 -#: users/templates/users/user_granted_remote_app.html:36 #: xpack/plugins/change_auth_plan/models.py:282 #: xpack/plugins/cloud/models.py:202 msgid "Asset" msgstr "资产" -#: acls/models/login_asset_acl.py:32 authentication/models.py:45 -#: users/templates/users/user_detail.html:258 +#: acls/models/login_asset_acl.py:32 assets/models/cmd_filter.py:62 +#: authentication/models.py:45 msgid "Reviewers" msgstr "审批人" -#: acls/models/login_asset_acl.py:86 tickets/const.py:12 +#: acls/models/login_asset_acl.py:89 tickets/const.py:12 msgid "Login asset confirm" msgstr "登录资产复核" @@ -183,13 +158,13 @@ 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:183 assets/models/domain.py:52 -#: assets/serializers/asset_user.py:46 settings/serializers/settings.py:112 +#: assets/serializers/asset_user.py:47 settings/serializers/settings.py:112 #: users/templates/users/_granted_assets.html:26 #: users/templates/users/user_asset_permission.html:156 msgid "IP" msgstr "IP" -#: acls/serializers/login_acl.py:50 +#: acls/serializers/login_acl.py:55 msgid "The user `{}` is not in the current organization: `{}`" msgstr "用户 `{}` 不在当前组织: `{}`" @@ -205,11 +180,8 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: applications/serializers/attrs/application_type/vmware_client.py:26 #: assets/models/base.py:251 assets/models/gathered_user.py:15 #: audits/models.py:100 authentication/forms.py:15 authentication/forms.py:17 -#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:528 +#: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:556 #: users/templates/users/_select_user_modal.html:14 -#: users/templates/users/user_detail.html:53 -#: users/templates/users/user_list.html:15 -#: users/templates/users/user_profile.html:47 #: xpack/plugins/change_auth_plan/models.py:47 #: xpack/plugins/change_auth_plan/models.py:278 #: xpack/plugins/cloud/serializers.py:51 @@ -226,7 +198,7 @@ msgstr "" "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)" #: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:184 -#: assets/serializers/asset_user.py:45 assets/serializers/gathered_user.py:20 +#: assets/serializers/asset_user.py:46 assets/serializers/gathered_user.py:23 #: settings/serializers/settings.py:111 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 @@ -241,7 +213,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议 #: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:187 #: assets/models/domain.py:54 assets/models/user.py:123 -#: terminal/serializers/session.py:29 terminal/serializers/storage.py:69 +#: terminal/serializers/session.py:32 terminal/serializers/storage.py:69 msgid "Protocol" msgstr "协议" @@ -249,19 +221,18 @@ msgstr "协议" msgid "Unsupported protocols: {}" msgstr "不支持的协议: {}" -#: acls/serializers/login_asset_acl.py:94 -#: tickets/serializers/ticket/ticket.py:109 +#: acls/serializers/login_asset_acl.py:98 +#: tickets/serializers/ticket/ticket.py:111 msgid "The organization `{}` does not exist" msgstr "组织 `{}` 不存在" -#: acls/serializers/login_asset_acl.py:99 +#: acls/serializers/login_asset_acl.py:103 msgid "None of the reviewers belong to Organization `{}`" msgstr "所有复核人都不属于组织 `{}`" #: applications/const.py:9 #: applications/serializers/attrs/application_category/db.py:14 #: applications/serializers/attrs/application_type/mysql_workbench.py:26 -#: users/templates/users/user_granted_database_app.html:37 msgid "Database" msgstr "数据库" @@ -281,14 +252,13 @@ msgstr "自定义" msgid "Category" msgstr "类别" -#: applications/models/application.py:16 assets/models/cmd_filter.py:52 +#: applications/models/application.py:16 assets/models/cmd_filter.py:53 #: perms/models/application_permission.py:23 #: perms/serializers/application/permission.py:17 #: perms/serializers/application/user_permission.py:34 #: terminal/models/storage.py:26 terminal/models/storage.py:93 #: tickets/models/ticket.py:38 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:27 -#: users/templates/users/user_granted_database_app.html:35 msgid "Type" msgstr "类型" @@ -315,9 +285,7 @@ msgid "Cluster" msgstr "集群" #: applications/serializers/attrs/application_category/db.py:11 -#: ops/models/adhoc.py:146 -#: users/templates/users/user_granted_database_app.html:36 -#: xpack/plugins/cloud/serializers.py:49 +#: ops/models/adhoc.py:146 xpack/plugins/cloud/serializers.py:49 msgid "Host" msgstr "主机" @@ -347,16 +315,13 @@ msgstr "目标URL" #: 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:252 assets/serializers/asset_user.py:71 -#: audits/signals_handler.py:46 authentication/forms.py:22 -#: authentication/templates/authentication/login.html:155 +#: assets/models/base.py:252 assets/serializers/asset_user.py:76 +#: audits/signals_handler.py:58 authentication/forms.py:22 +#: authentication/templates/authentication/login.html:164 #: settings/serializers/settings.py:93 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 -#: users/templates/users/user_profile_update.html:41 -#: users/templates/users/user_pubkey_update.html:41 -#: users/templates/users/user_update.html:20 #: xpack/plugins/change_auth_plan/models.py:68 #: xpack/plugins/change_auth_plan/models.py:190 #: xpack/plugins/change_auth_plan/models.py:285 @@ -389,8 +354,8 @@ msgid "You can't delete the root node ({})" msgstr "不能删除根节点 ({})" #: assets/api/node.py:75 -msgid "Deletion failed and the node contains children or assets" -msgstr "删除失败,节点包含子节点或资产" +msgid "Deletion failed and the node contains assets" +msgstr "删除失败,节点包含资产" #: assets/backends/db.py:244 msgid "Could not remove asset admin user" @@ -516,11 +481,10 @@ msgstr "标签管理" #: assets/models/asset.py:221 assets/models/base.py:258 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 -#: assets/models/cmd_filter.py:60 assets/models/group.py:21 +#: assets/models/cmd_filter.py:67 assets/models/group.py:21 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:24 -#: orgs/models.py:422 perms/models/base.py:55 users/models/user.py:571 -#: users/serializers/group.py:35 users/templates/users/user_detail.html:97 -#: xpack/plugins/change_auth_plan/models.py:81 +#: orgs/models.py:422 perms/models/base.py:55 users/models/user.py:599 +#: users/serializers/group.py:35 xpack/plugins/change_auth_plan/models.py:81 #: xpack/plugins/cloud/models.py:104 xpack/plugins/gathered_user/models.py:30 msgid "Created by" msgstr "创建者" @@ -533,8 +497,7 @@ msgstr "创建者" #: 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:25 #: orgs/models.py:420 perms/models/base.py:56 users/models/group.py:18 -#: users/templates/users/user_group_detail.html:58 -#: xpack/plugins/cloud/models.py:107 +#: users/models/user.py:773 xpack/plugins/cloud/models.py:107 msgid "Date created" msgstr "创建日期" @@ -580,8 +543,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:549 -#: users/templates/users/user_detail.html:62 +#: assets/models/cluster.py:22 users/models/user.py:577 msgid "Phone" msgstr "手机" @@ -607,7 +569,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:719 +#: users/models/user.py:758 msgid "System" msgstr "系统" @@ -624,30 +586,38 @@ msgid "Regex" msgstr "正则表达式" #: assets/models/cmd_filter.py:41 ops/models/command.py:25 -#: terminal/backends/command/serializers.py:15 terminal/models/session.py:48 +#: terminal/backends/command/serializers.py:15 terminal/models/session.py:49 msgid "Command" msgstr "命令" -#: assets/models/cmd_filter.py:46 +#: assets/models/cmd_filter.py:47 msgid "Deny" msgstr "拒绝" -#: assets/models/cmd_filter.py:51 +#: assets/models/cmd_filter.py:49 +msgid "Reconfirm" +msgstr "复核" + +#: assets/models/cmd_filter.py:52 msgid "Filter" msgstr "过滤器" -#: assets/models/cmd_filter.py:55 xpack/plugins/license/models.py:29 +#: assets/models/cmd_filter.py:56 xpack/plugins/license/models.py:29 msgid "Content" msgstr "内容" -#: assets/models/cmd_filter.py:55 +#: assets/models/cmd_filter.py:56 msgid "One line one command" msgstr "每行一个命令" -#: assets/models/cmd_filter.py:64 +#: assets/models/cmd_filter.py:71 msgid "Command filter rule" msgstr "命令过滤规则" +#: assets/models/cmd_filter.py:111 tickets/const.py:13 +msgid "Command confirm" +msgstr "命令复核" + #: assets/models/domain.py:64 msgid "Gateway" msgstr "网关" @@ -680,31 +650,31 @@ msgstr "资产组" msgid "Default asset group" msgstr "默认资产组" -#: assets/models/label.py:19 assets/models/node.py:547 settings/models.py:30 +#: assets/models/label.py:19 assets/models/node.py:546 settings/models.py:30 msgid "Value" msgstr "值" -#: assets/models/node.py:152 +#: assets/models/node.py:151 msgid "New node" msgstr "新节点" -#: assets/models/node.py:475 users/templates/users/_granted_assets.html:130 +#: assets/models/node.py:474 users/templates/users/_granted_assets.html:130 msgid "empty" msgstr "空" -#: assets/models/node.py:546 perms/models/asset_permission.py:156 +#: assets/models/node.py:545 perms/models/asset_permission.py:176 msgid "Key" msgstr "键" -#: assets/models/node.py:548 +#: assets/models/node.py:547 msgid "Full value" msgstr "全称" -#: assets/models/node.py:551 perms/models/asset_permission.py:157 +#: assets/models/node.py:550 perms/models/asset_permission.py:177 msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:560 assets/serializers/system_user.py:191 +#: assets/models/node.py:559 assets/serializers/system_user.py:194 #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 @@ -734,8 +704,7 @@ msgstr "资产" msgid "Users" msgstr "用户管理" -#: assets/models/user.py:121 users/templates/users/user_group_list.html:90 -#: users/templates/users/user_profile.html:124 +#: assets/models/user.py:121 msgid "User groups" msgstr "用户组" @@ -775,15 +744,13 @@ msgstr "用户组" #: perms/models/application_permission.py:31 #: perms/models/asset_permission.py:101 templates/_nav.html:45 #: terminal/backends/command/models.py:20 -#: terminal/backends/command/serializers.py:14 terminal/models/session.py:41 +#: terminal/backends/command/serializers.py:14 terminal/models/session.py:42 #: users/templates/users/_granted_assets.html:27 #: users/templates/users/user_asset_permission.html:42 #: users/templates/users/user_asset_permission.html:76 #: users/templates/users/user_asset_permission.html:159 #: users/templates/users/user_database_app_permission.html:40 #: users/templates/users/user_database_app_permission.html:67 -#: users/templates/users/user_remote_app_permission.html:40 -#: users/templates/users/user_remote_app_permission.html:67 msgid "System user" msgstr "系统用户" @@ -820,7 +787,7 @@ msgstr "网域名称" msgid "Admin user name" msgstr "管理用户名称" -#: assets/serializers/asset.py:71 +#: assets/serializers/asset.py:71 perms/serializers/asset/permission.py:49 msgid "Nodes name" msgstr "节点名称" @@ -836,25 +803,22 @@ msgstr "组织名称" msgid "Connectivity" msgstr "连接" -#: assets/serializers/asset_user.py:44 +#: assets/serializers/asset_user.py:45 #: authentication/templates/authentication/_access_key_modal.html:30 #: users/serializers/group.py:37 msgid "ID" msgstr "ID" -#: assets/serializers/asset_user.py:48 +#: assets/serializers/asset_user.py:49 msgid "Backend" msgstr "后端" -#: assets/serializers/asset_user.py:75 users/forms/profile.py:160 -#: users/models/user.py:560 users/templates/users/user_password_update.html:48 -#: users/templates/users/user_profile.html:69 -#: users/templates/users/user_profile_update.html:46 -#: users/templates/users/user_pubkey_update.html:46 +#: assets/serializers/asset_user.py:80 users/forms/profile.py:160 +#: users/models/user.py:588 users/templates/users/user_password_update.html:48 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:79 users/models/user.py:557 +#: assets/serializers/asset_user.py:84 users/models/user.py:585 msgid "Private key" msgstr "ssh私钥" @@ -874,9 +838,9 @@ msgstr "应用数量" msgid "Gateways count" msgstr "网关数量" -#: assets/serializers/label.py:13 assets/serializers/system_user.py:45 -#: assets/serializers/system_user.py:166 -#: perms/serializers/asset/permission.py:66 +#: assets/serializers/label.py:13 assets/serializers/system_user.py:47 +#: assets/serializers/system_user.py:169 +#: perms/serializers/asset/permission.py:74 msgid "Assets amount" msgstr "资产数量" @@ -897,33 +861,33 @@ msgstr "不能包含: /" msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" -#: assets/serializers/system_user.py:44 assets/serializers/system_user.py:165 -#: perms/serializers/asset/permission.py:67 +#: assets/serializers/system_user.py:46 assets/serializers/system_user.py:168 +#: perms/serializers/asset/permission.py:75 msgid "Nodes amount" msgstr "节点数量" -#: assets/serializers/system_user.py:46 assets/serializers/system_user.py:167 -#: assets/serializers/system_user.py:193 +#: assets/serializers/system_user.py:48 assets/serializers/system_user.py:170 +#: assets/serializers/system_user.py:196 msgid "Login mode display" msgstr "登录模式(显示名称)" -#: assets/serializers/system_user.py:48 assets/serializers/system_user.py:169 +#: assets/serializers/system_user.py:50 assets/serializers/system_user.py:172 msgid "Ad domain" msgstr "Ad 网域" -#: assets/serializers/system_user.py:87 +#: assets/serializers/system_user.py:89 msgid "Username same with user with protocol {} only allow 1" msgstr "用户名和用户相同的一种协议只允许存在一个" -#: assets/serializers/system_user.py:100 +#: assets/serializers/system_user.py:102 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/serializers/system_user.py:108 +#: assets/serializers/system_user.py:110 msgid "Path should starts with /" msgstr "路径应该以 / 开头" -#: assets/serializers/system_user.py:119 +#: assets/serializers/system_user.py:121 msgid "Password or private key required" msgstr "密码或密钥密码需要一个" @@ -984,25 +948,25 @@ msgid "" "The task of self-checking is already running and cannot be started repeatedly" msgstr "自检程序已经在运行,不能重复启动" -#: assets/tasks/push_system_user.py:192 +#: assets/tasks/push_system_user.py:193 #: assets/tasks/system_user_connectivity.py:89 msgid "System user is dynamic: {}" msgstr "系统用户是动态的: {}" -#: assets/tasks/push_system_user.py:232 +#: assets/tasks/push_system_user.py:233 msgid "Start push system user for platform: [{}]" msgstr "推送系统用户到平台: [{}]" -#: assets/tasks/push_system_user.py:233 +#: assets/tasks/push_system_user.py:234 #: assets/tasks/system_user_connectivity.py:81 msgid "Hosts count: {}" msgstr "主机数量: {}" -#: assets/tasks/push_system_user.py:272 assets/tasks/push_system_user.py:298 +#: assets/tasks/push_system_user.py:273 assets/tasks/push_system_user.py:299 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks/push_system_user.py:284 +#: assets/tasks/push_system_user.py:285 msgid "Push system users to asset: {}({}) => {}" msgstr "推送系统用户到入资产: {}({}) => {}" @@ -1042,12 +1006,6 @@ msgstr "没有匹配到资产,结束任务" #: authentication/templates/authentication/_access_key_modal.html:65 #: users/templates/users/user_asset_permission.html:128 #: users/templates/users/user_database_app_permission.html:111 -#: users/templates/users/user_detail.html:16 -#: users/templates/users/user_group_detail.html:27 -#: users/templates/users/user_group_list.html:53 -#: users/templates/users/user_list.html:94 -#: users/templates/users/user_list.html:98 -#: users/templates/users/user_remote_app_permission.html:111 msgid "Delete" msgstr "删除" @@ -1076,7 +1034,7 @@ msgid "Symlink" msgstr "建立软链接" #: audits/models.py:37 audits/models.py:60 audits/models.py:71 -#: terminal/models/session.py:44 +#: terminal/models/session.py:45 msgid "Remote addr" msgstr "远端地址" @@ -1089,12 +1047,11 @@ msgid "Filename" msgstr "文件名" #: audits/models.py:42 audits/models.py:96 -#: users/templates/users/user_detail.html:487 msgid "Success" msgstr "成功" #: audits/models.py:43 ops/models/command.py:30 perms/models/base.py:53 -#: terminal/models/session.py:51 +#: 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 @@ -1114,15 +1071,6 @@ msgstr "创建" #: templates/_csv_update_modal.html:6 #: users/templates/users/user_asset_permission.html:127 #: users/templates/users/user_database_app_permission.html:110 -#: users/templates/users/user_detail.html:12 -#: users/templates/users/user_group_detail.html:23 -#: users/templates/users/user_group_list.html:51 -#: users/templates/users/user_list.html:84 -#: users/templates/users/user_list.html:87 -#: users/templates/users/user_profile.html:181 -#: users/templates/users/user_profile.html:191 -#: users/templates/users/user_profile.html:201 -#: users/templates/users/user_remote_app_permission.html:110 msgid "Update" msgstr "更新" @@ -1142,12 +1090,11 @@ msgstr "日期" msgid "Change by" msgstr "修改者" -#: audits/models.py:90 users/templates/users/user_detail.html:84 +#: audits/models.py:90 msgid "Disabled" msgstr "禁用" #: audits/models.py:91 settings/models.py:33 -#: users/templates/users/user_detail.html:82 msgid "Enabled" msgstr "启用" @@ -1173,16 +1120,15 @@ msgstr "登录IP" msgid "Login city" msgstr "登录城市" -#: audits/models.py:104 audits/serializers.py:38 +#: audits/models.py:104 audits/serializers.py:45 msgid "User agent" msgstr "用户代理" #: audits/models.py:105 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: authentication/templates/authentication/login_otp.html:6 -#: users/forms/profile.py:64 users/models/user.py:552 -#: users/serializers/profile.py:99 users/templates/users/user_detail.html:77 -#: users/templates/users/user_profile.html:87 +#: users/forms/profile.py:64 users/models/user.py:580 +#: users/serializers/profile.py:104 msgid "MFA" msgstr "多因子认证" @@ -1208,56 +1154,66 @@ msgstr "认证方式" msgid "Operate for display" msgstr "操作(显示名称)" -#: audits/serializers.py:26 +#: audits/serializers.py:30 msgid "Type for display" msgstr "类型(显示名称)" -#: audits/serializers.py:27 +#: audits/serializers.py:31 msgid "Status for display" msgstr "状态(显示名称)" -#: audits/serializers.py:28 +#: audits/serializers.py:32 msgid "MFA for display" msgstr "多因子认证状态(显示名称)" -#: audits/serializers.py:66 audits/serializers.py:81 ops/models/adhoc.py:247 -#: terminal/serializers/session.py:34 +#: audits/serializers.py:76 audits/serializers.py:91 ops/models/adhoc.py:247 +#: terminal/serializers/session.py:37 msgid "Is success" msgstr "是否成功" -#: audits/serializers.py:68 +#: audits/serializers.py:78 msgid "Hosts for display" msgstr "主机 (显示名称)" -#: audits/serializers.py:80 ops/models/command.py:26 +#: audits/serializers.py:90 ops/models/command.py:26 #: xpack/plugins/cloud/models.py:155 msgid "Result" msgstr "结果" -#: audits/serializers.py:82 terminal/serializers/storage.py:178 +#: audits/serializers.py:92 terminal/serializers/storage.py:178 msgid "Hosts" msgstr "主机" -#: audits/serializers.py:83 +#: audits/serializers.py:93 msgid "Run as" msgstr "运行用户" -#: audits/serializers.py:85 +#: audits/serializers.py:95 msgid "Run as for display" msgstr "运行用户(显示名称)" -#: audits/serializers.py:86 +#: audits/serializers.py:96 msgid "User for display" msgstr "用户(显示名称)" -#: audits/signals_handler.py:45 +#: audits/signals_handler.py:57 msgid "SSH Key" msgstr "SSH 密钥" -#: audits/signals_handler.py:47 +#: audits/signals_handler.py:59 msgid "SSO" msgstr "" +#: audits/signals_handler.py:60 +#: authentication/templates/authentication/login.html:210 +msgid "WeCom" +msgstr "企业微信" + +#: audits/signals_handler.py:61 +#: authentication/templates/authentication/login.html:215 +msgid "DingTalk" +msgstr "钉钉" + #: authentication/api/mfa.py:60 msgid "Code is invalid" msgstr "Code无效" @@ -1313,55 +1269,59 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: authentication/errors.py:24 +#: authentication/errors.py:25 msgid "Username/password check failed" msgstr "用户名/密码 校验失败" -#: authentication/errors.py:25 +#: authentication/errors.py:26 msgid "Password decrypt failed" msgstr "密码解密失败" -#: authentication/errors.py:26 +#: authentication/errors.py:27 msgid "MFA failed" msgstr "多因子认证失败" -#: authentication/errors.py:27 +#: authentication/errors.py:28 msgid "MFA unset" msgstr "多因子认证没有设定" -#: authentication/errors.py:28 +#: authentication/errors.py:29 msgid "Username does not exist" msgstr "用户名不存在" -#: authentication/errors.py:29 +#: authentication/errors.py:30 msgid "Password expired" msgstr "密码已过期" -#: authentication/errors.py:30 +#: authentication/errors.py:31 msgid "Disabled or expired" msgstr "禁用或失效" -#: authentication/errors.py:31 +#: authentication/errors.py:32 msgid "This account is inactive." msgstr "此账户已禁用" -#: authentication/errors.py:32 +#: authentication/errors.py:33 msgid "This account is expired" msgstr "此账户已过期" -#: authentication/errors.py:33 +#: authentication/errors.py:34 msgid "Auth backend not match" msgstr "没有匹配到认证后端" -#: authentication/errors.py:34 +#: authentication/errors.py:35 msgid "ACL is not allowed" msgstr "ACL 不被允许" -#: authentication/errors.py:44 +#: authentication/errors.py:36 +msgid "Only local users are allowed" +msgstr "" + +#: authentication/errors.py:46 msgid "No session found, check your cookie" msgstr "会话已变更,刷新页面" -#: authentication/errors.py:46 +#: authentication/errors.py:48 #, python-brace-format msgid "" "The username or password you entered is incorrect, please enter it again. " @@ -1371,13 +1331,13 @@ msgstr "" "您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将" "被临时 锁定 {block_time} 分钟)" -#: authentication/errors.py:52 authentication/errors.py:56 +#: authentication/errors.py:54 authentication/errors.py:58 msgid "" "The account has been locked (please contact admin to unlock it or try again " "after {} minutes)" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" -#: authentication/errors.py:60 +#: authentication/errors.py:62 #, python-brace-format msgid "" "MFA code invalid, or ntp sync server time, You can also try {times_try} " @@ -1386,42 +1346,50 @@ msgstr "" "MFA验证码不正确,或者服务器端时间不对。 您还可以尝试 {times_try} 次(账号将被" "临时 锁定 {block_time} 分钟)" -#: authentication/errors.py:65 +#: authentication/errors.py:67 msgid "MFA required" msgstr "需要多因子认证" -#: authentication/errors.py:66 +#: authentication/errors.py:68 msgid "MFA not set, please set it first" msgstr "多因子认证没有设置,请先完成设置" -#: authentication/errors.py:67 +#: authentication/errors.py:69 msgid "Login confirm required" msgstr "需要登录复核" -#: authentication/errors.py:68 +#: authentication/errors.py:70 msgid "Wait login confirm ticket for accept" msgstr "等待登录复核处理" -#: authentication/errors.py:69 +#: authentication/errors.py:71 msgid "Login confirm ticket was {}" msgstr "登录复核 {}" -#: authentication/errors.py:233 +#: authentication/errors.py:235 msgid "IP is not allowed" msgstr "来源 IP 不被允许登录" -#: authentication/errors.py:266 +#: authentication/errors.py:268 msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors.py:271 authentication/views/login.py:236 +#: authentication/errors.py:273 authentication/mixins.py:271 msgid "Your password is too simple, please change it for security" msgstr "你的密码过于简单,为了安全,请修改" -#: authentication/errors.py:280 authentication/views/login.py:251 +#: authentication/errors.py:282 authentication/mixins.py:278 +msgid "You should to change your password before login" +msgstr "登录完成前,请先修改密码" + +#: authentication/errors.py:291 authentication/mixins.py:285 msgid "Your password has expired, please reset before logging in" msgstr "您的密码已过期,先修改再登录" +#: authentication/errors.py:320 +msgid "Your password is invalid" +msgstr "您的密码无效" + #: authentication/forms.py:26 msgid "{} days auto login" msgstr "{} 天内自动登录" @@ -1431,6 +1399,10 @@ msgstr "{} 天内自动登录" msgid "MFA code" msgstr "多因子认证验证码" +#: authentication/mixins.py:261 +msgid "Please change your password" +msgstr "请修改密码" + #: authentication/models.py:40 msgid "Private Token" msgstr "SSH密钥" @@ -1453,7 +1425,7 @@ msgstr "文档" #: authentication/templates/authentication/_access_key_modal.html:31 msgid "Secret" -msgstr "密文" +msgstr "秘钥" #: authentication/templates/authentication/_access_key_modal.html:33 msgid "Date" @@ -1465,18 +1437,13 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:445 users/serializers/profile.py:96 -#: users/templates/users/user_profile.html:94 -#: users/templates/users/user_profile.html:163 -#: users/templates/users/user_profile.html:166 +#: users/models/user.py:470 users/serializers/profile.py:101 #: users/templates/users/user_verify_mfa.html:32 msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:446 users/serializers/profile.py:97 -#: users/templates/users/user_profile.html:92 -#: users/templates/users/user_profile.html:170 +#: users/models/user.py:471 users/serializers/profile.py:102 msgid "Enable" msgstr "启用" @@ -1486,7 +1453,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:19 +#: templates/_modal.html:22 tickets/const.py:20 msgid "Close" msgstr "关闭" @@ -1508,14 +1475,7 @@ msgid "Need MFA for view auth" msgstr "需要多因子认证来查看账号信息" #: authentication/templates/authentication/_mfa_confirm_modal.html:20 -#: templates/_modal.html:23 users/templates/users/user_detail.html:264 -#: users/templates/users/user_detail.html:417 -#: users/templates/users/user_detail.html:443 -#: users/templates/users/user_detail.html:466 -#: users/templates/users/user_detail.html:511 -#: users/templates/users/user_group_create_update.html:28 -#: users/templates/users/user_list.html:184 -#: users/templates/users/user_password_verify.html:20 +#: templates/_modal.html:23 users/templates/users/user_password_verify.html:20 msgid "Confirm" msgstr "确认" @@ -1523,30 +1483,30 @@ msgstr "确认" msgid "Code error" msgstr "代码错误" -#: authentication/templates/authentication/login.html:148 +#: authentication/templates/authentication/login.html:157 msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/templates/authentication/login.html:174 +#: authentication/templates/authentication/login.html:183 #: users/templates/users/forgot_password.html:15 #: users/templates/users/forgot_password.html:16 msgid "Forgot password" msgstr "忘记密码" -#: authentication/templates/authentication/login.html:181 +#: authentication/templates/authentication/login.html:190 #: templates/_header_bar.html:83 msgid "Login" msgstr "登录" -#: authentication/templates/authentication/login.html:188 +#: authentication/templates/authentication/login.html:197 msgid "More login options" msgstr "更多登录方式" -#: authentication/templates/authentication/login.html:191 +#: authentication/templates/authentication/login.html:200 msgid "OpenID" msgstr "OpenID" -#: authentication/templates/authentication/login.html:196 +#: authentication/templates/authentication/login.html:205 msgid "CAS" msgstr "" @@ -1580,7 +1540,6 @@ msgid "Copy link" msgstr "复制链接" #: authentication/templates/authentication/login_wait_confirm.html:51 -#: templates/flash_message_standalone.html:34 msgid "Return" msgstr "返回" @@ -1588,11 +1547,60 @@ msgstr "返回" msgid "Copy success" msgstr "复制成功" -#: authentication/views/login.py:57 +#: authentication/views/dingtalk.py:40 authentication/views/wecom.py:40 +msgid "You've been hacked" +msgstr "" + +#: authentication/views/dingtalk.py:76 +msgid "DingTalk is already bound" +msgstr "" + +#: authentication/views/dingtalk.py:89 authentication/views/wecom.py:88 +msgid "Please verify your password first" +msgstr "请检查密码" + +#: authentication/views/dingtalk.py:113 authentication/views/wecom.py:112 +msgid "Invalid user_id" +msgstr "无效的 user_id" + +#: authentication/views/dingtalk.py:129 +msgid "DingTalk query user failed" +msgstr "" + +#: authentication/views/dingtalk.py:136 authentication/views/dingtalk.py:219 +#: authentication/views/dingtalk.py:220 +msgid "Binding DingTalk successfully" +msgstr "绑定 钉钉 成功" + +#: authentication/views/dingtalk.py:188 +msgid "Failed to get user from DingTalk" +msgstr "" + +#: authentication/views/dingtalk.py:194 +msgid "DingTalk is not bound" +msgstr "" + +#: authentication/views/dingtalk.py:195 authentication/views/wecom.py:193 +msgid "Please login with a password and then bind the WoCom" +msgstr "" + +#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:238 +msgid "Binding DingTalk failed" +msgstr "" + +#: authentication/views/login.py:59 +msgid "Redirecting" +msgstr "" + +#: authentication/views/login.py:60 +msgid "Redirecting to {} authentication" +msgstr "" + +#: authentication/views/login.py:84 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:182 +#: authentication/views/login.py:202 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -1600,21 +1608,42 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:187 +#: authentication/views/login.py:207 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:219 +#: authentication/views/login.py:239 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:220 +#: authentication/views/login.py:240 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/login.py:235 authentication/views/login.py:250 -msgid "Please change your password" -msgstr "请修改密码" +#: authentication/views/wecom.py:75 +msgid "WeCom is already bound" +msgstr "" + +#: authentication/views/wecom.py:127 +msgid "WeCom query user failed" +msgstr "" + +#: authentication/views/wecom.py:134 authentication/views/wecom.py:217 +#: authentication/views/wecom.py:218 +msgid "Binding WeCom successfully" +msgstr "绑定 企业微信 成功" + +#: authentication/views/wecom.py:186 +msgid "Failed to get user from WeCom" +msgstr "" + +#: authentication/views/wecom.py:192 +msgid "WeCom is not bound" +msgstr "没有绑定企业微信" + +#: authentication/views/wecom.py:235 authentication/views/wecom.py:236 +msgid "Binding WeCom failed" +msgstr "绑定企业微信失败" #: common/const/__init__.py:6 #, python-format @@ -1663,6 +1692,10 @@ msgstr "" msgid "Is referenced by other objects and cannot be deleted" msgstr "被其他对象关联,不能删除" +#: common/exceptions.py:47 +msgid "This action require verify your MFA" +msgstr "" + #: common/fields/model.py:80 msgid "Marshal dict data to char field" msgstr "" @@ -1691,7 +1724,15 @@ msgstr "" msgid "Encrypt field using Secret Key" msgstr "" -#: common/mixins/api.py:52 +#: common/message/backends/exceptions.py:23 +msgid "Network error, please contact system administrator" +msgstr "网络错误,请联系系统管理员" + +#: common/message/backends/wecom/__init__.py:19 +msgid "WeCom error, please contact system administrator" +msgstr "企业微信错误,请联系系统管理员" + +#: common/mixins/api.py:57 msgid "Request file format may be wrong" msgstr "上传的文件格式错误 或 其它类型资源的文件" @@ -1719,7 +1760,7 @@ msgstr "字段必须唯一" msgid "Should not contains special characters" msgstr "不能包含特殊字符" -#: jumpserver/context_processor.py:17 +#: jumpserver/context_processor.py:19 msgid "JumpServer Open Source Bastion Host" msgstr "JumpServer 开源堡垒机" @@ -1727,7 +1768,7 @@ msgstr "JumpServer 开源堡垒机" msgid "

Flow service unavailable, check it

" msgstr "" -#: jumpserver/views/other.py:27 +#: jumpserver/views/other.py:25 msgid "" "
Luna is a separately deployed program, you need to deploy Luna, koko, " "configure nginx for url distribution,
If you see this page, " @@ -1736,11 +1777,11 @@ msgstr "" "
Luna是单独部署的一个程序,你需要部署luna,koko,
如果你看到了" "这个页面,证明你访问的不是nginx监听的端口,祝你好运
" -#: jumpserver/views/other.py:71 +#: jumpserver/views/other.py:69 msgid "Websocket server run on port: {}, you should proxy it on nginx" msgstr "Websocket 服务运行在端口: {}, 请检查nginx是否代理是否设置" -#: jumpserver/views/other.py:85 +#: jumpserver/views/other.py:83 msgid "" "
Koko is a separately deployed program, you need to deploy Koko, " "configure nginx for url distribution,
If you see this page, " @@ -1758,6 +1799,10 @@ msgstr "等待任务开始" msgid "Not has host {} permission" msgstr "没有该主机 {} 权限" +#: ops/apps.py:9 +msgid "Operations" +msgstr "运维" + #: ops/mixin.py:29 ops/mixin.py:92 ops/mixin.py:160 msgid "Cycle perform" msgstr "周期执行" @@ -1826,7 +1871,7 @@ msgstr "再次执行" msgid "Become" msgstr "Become" -#: ops/models/adhoc.py:150 users/templates/users/user_group_detail.html:54 +#: ops/models/adhoc.py:150 msgid "Create by" msgstr "创建者" @@ -1853,7 +1898,7 @@ msgid "Time" msgstr "时间" #: ops/models/adhoc.py:246 ops/models/command.py:28 -#: terminal/serializers/session.py:38 +#: terminal/serializers/session.py:41 msgid "Is finished" msgstr "是否完成" @@ -1919,8 +1964,8 @@ msgid "The current organization cannot be deleted" msgstr "当前组织不能被删除" #: orgs/mixins/models.py:45 orgs/mixins/serializers.py:25 orgs/models.py:36 -#: orgs/models.py:417 orgs/serializers.py:101 -#: tickets/serializers/ticket/ticket.py:81 +#: orgs/models.py:417 orgs/serializers.py:108 +#: tickets/serializers/ticket/ticket.py:83 msgid "Organization" msgstr "组织" @@ -1936,11 +1981,8 @@ msgstr "组织审计员" msgid "GLOBAL" msgstr "全局组织" -#: orgs/models.py:419 users/models/user.py:540 +#: orgs/models.py:419 users/models/user.py:568 #: users/templates/users/_select_user_modal.html:15 -#: users/templates/users/user_detail.html:73 -#: users/templates/users/user_list.html:16 -#: users/templates/users/user_profile.html:55 msgid "Role" msgstr "角色" @@ -1952,7 +1994,7 @@ msgstr "管理员正在修改授权,请稍等" msgid "The authorization cannot be revoked for the time being" msgstr "该授权暂时不能撤销" -#: perms/models/application_permission.py:27 users/models/user.py:160 +#: perms/models/application_permission.py:27 users/models/user.py:185 msgid "Application" msgstr "应用程序" @@ -1993,7 +2035,7 @@ msgid "Clipboard copy paste" msgstr "剪贴板复制粘贴" #: perms/models/asset_permission.py:102 -#: perms/serializers/asset/permission.py:63 +#: perms/serializers/asset/permission.py:71 msgid "Actions" msgstr "动作" @@ -2002,24 +2044,20 @@ msgstr "动作" msgid "Asset permission" msgstr "资产授权" -#: perms/models/asset_permission.py:189 +#: perms/models/asset_permission.py:209 msgid "Ungrouped" msgstr "未分组" -#: perms/models/asset_permission.py:191 +#: perms/models/asset_permission.py:211 msgid "Favorite" msgstr "收藏夹" #: perms/models/base.py:51 templates/_nav.html:21 users/models/group.py:31 -#: users/models/user.py:536 users/templates/users/_select_user_modal.html:16 +#: users/models/user.py:564 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 #: users/templates/users/user_database_app_permission.html:61 -#: users/templates/users/user_detail.html:209 -#: users/templates/users/user_list.html:17 -#: users/templates/users/user_remote_app_permission.html:38 -#: users/templates/users/user_remote_app_permission.html:61 msgid "User group" msgstr "用户组" @@ -2028,8 +2066,7 @@ msgstr "用户组" #: 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:568 users/templates/users/user_detail.html:93 -#: users/templates/users/user_profile.html:120 +#: users/models/user.py:596 msgid "Date expired" msgstr "失效日期" @@ -2039,25 +2076,41 @@ msgid "" "permission type. ({})" msgstr "应用列表中包含与授权类型不同的应用。({})" -#: perms/serializers/asset/permission.py:43 -#: perms/serializers/asset/permission.py:61 users/serializers/user.py:34 -#: users/serializers/user.py:69 +#: perms/serializers/asset/permission.py:45 +#: perms/serializers/asset/permission.py:69 users/serializers/user.py:34 +#: users/serializers/user.py:82 msgid "Is expired" msgstr "是否过期" -#: perms/serializers/asset/permission.py:62 users/serializers/user.py:68 +#: perms/serializers/asset/permission.py:46 +msgid "Users name" +msgstr "用户名" + +#: perms/serializers/asset/permission.py:47 +msgid "User groups name" +msgstr "用户组数量" + +#: perms/serializers/asset/permission.py:48 +msgid "Assets name" +msgstr "资产名字" + +#: perms/serializers/asset/permission.py:50 +msgid "System users name" +msgstr "系统用户名字" + +#: perms/serializers/asset/permission.py:70 users/serializers/user.py:81 msgid "Is valid" msgstr "账户是否有效" -#: perms/serializers/asset/permission.py:64 users/serializers/group.py:36 +#: perms/serializers/asset/permission.py:72 users/serializers/group.py:36 msgid "Users amount" msgstr "用户数量" -#: perms/serializers/asset/permission.py:65 +#: perms/serializers/asset/permission.py:73 msgid "User groups amount" msgstr "用户组数量" -#: perms/serializers/asset/permission.py:68 +#: perms/serializers/asset/permission.py:76 msgid "System users amount" msgstr "系统用户数量" @@ -2070,6 +2123,10 @@ msgstr "邮件已经发送{}, 请检查" msgid "Welcome to the JumpServer open source Bastion Host" msgstr "欢迎使用JumpServer开源堡垒机" +#: settings/api/dingtalk.py:36 settings/api/wecom.py:36 +msgid "OK" +msgstr "" + #: settings/api/ldap.py:189 msgid "Get ldap users is None" msgstr "获取 LDAP 用户为 None" @@ -2079,7 +2136,6 @@ msgid "Imported {} users successfully" msgstr "导入 {} 个用户成功" #: settings/models.py:123 users/templates/users/reset_password.html:29 -#: users/templates/users/user_profile.html:20 msgid "Setting" msgstr "设置" @@ -2262,23 +2318,31 @@ msgstr "自动" msgid "Password auth" msgstr "密码认证" -#: settings/serializers/settings.py:124 +#: settings/serializers/settings.py:125 msgid "Public key auth" msgstr "密钥认证" -#: settings/serializers/settings.py:125 +#: settings/serializers/settings.py:126 +msgid "" +"Tips: If use other auth method, like AD/LDAP, you should disable this to " +"avoid being able to log in after deleting" +msgstr "" +"提示:如果你使用其它认证方式,如 AD/LDAP,你应该禁用此项,以避免第三方系统删" +"除后,还可以登录" + +#: settings/serializers/settings.py:129 msgid "List sort by" msgstr "资产列表排序" -#: settings/serializers/settings.py:126 +#: settings/serializers/settings.py:130 msgid "List page size" msgstr "资产列表每页数量" -#: settings/serializers/settings.py:128 +#: settings/serializers/settings.py:132 msgid "Session keep duration" msgstr "会话日志保存时间" -#: settings/serializers/settings.py:129 +#: settings/serializers/settings.py:133 msgid "" "Units: days, Session, record, command will be delete if more than duration, " "only in database" @@ -2286,64 +2350,64 @@ msgstr "" "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不" "受影响)" -#: settings/serializers/settings.py:131 +#: settings/serializers/settings.py:135 msgid "Telnet login regex" msgstr "Telnet 成功正则表达式" -#: settings/serializers/settings.py:136 +#: settings/serializers/settings.py:140 msgid "Global MFA auth" msgstr "全局启用 MFA 认证" -#: settings/serializers/settings.py:137 +#: settings/serializers/settings.py:141 msgid "All user enable MFA" msgstr "强制所有用户启用多因子认证" -#: settings/serializers/settings.py:140 +#: settings/serializers/settings.py:144 msgid "Batch command execution" msgstr "批量命令执行" -#: settings/serializers/settings.py:141 +#: settings/serializers/settings.py:145 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/settings.py:144 +#: settings/serializers/settings.py:148 msgid "Enable terminal register" msgstr "终端注册" -#: settings/serializers/settings.py:145 +#: settings/serializers/settings.py:149 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" -#: settings/serializers/settings.py:149 +#: settings/serializers/settings.py:153 msgid "Limit the number of login failures" msgstr "限制登录失败次数" -#: settings/serializers/settings.py:153 +#: settings/serializers/settings.py:157 msgid "Block logon interval" msgstr "禁止登录时间间隔" -#: settings/serializers/settings.py:154 +#: settings/serializers/settings.py:158 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:158 +#: settings/serializers/settings.py:162 msgid "Connection max idle time" msgstr "连接最大空闲时间" -#: settings/serializers/settings.py:159 +#: settings/serializers/settings.py:163 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" -#: settings/serializers/settings.py:163 +#: settings/serializers/settings.py:167 msgid "User password expiration" msgstr "用户密码过期时间" -#: settings/serializers/settings.py:164 +#: settings/serializers/settings.py:168 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 " @@ -2353,38 +2417,83 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: settings/serializers/settings.py:168 +#: settings/serializers/settings.py:172 +msgid "Number of repeated historical passwords" +msgstr "历史密码可重复次数" + +#: settings/serializers/settings.py:173 +msgid "" +"Tip: When the user resets the password, it cannot be the previous n " +"historical passwords of the user (the value of n here is the value filled in " +"the input box)" +msgstr "" +"提示:用户重置密码时,不能为该用户前n次历史密码 (此处的n值即为输入框中填写的" +"值)" + +#: settings/serializers/settings.py:177 msgid "Password minimum length" msgstr "密码最小长度" -#: settings/serializers/settings.py:171 +#: settings/serializers/settings.py:180 msgid "Must contain capital" msgstr "必须包含大写字符" -#: settings/serializers/settings.py:173 +#: settings/serializers/settings.py:182 msgid "Must contain lowercase" msgstr "必须包含小写字符" -#: settings/serializers/settings.py:174 +#: settings/serializers/settings.py:183 msgid "Must contain numeric" msgstr "必须包含数字" -#: settings/serializers/settings.py:175 +#: settings/serializers/settings.py:184 msgid "Must contain special" msgstr "必须包含特殊字符" -#: settings/serializers/settings.py:176 +#: settings/serializers/settings.py:185 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/settings.py:178 +#: settings/serializers/settings.py:187 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/settings.py:179 +#: settings/serializers/settings.py:188 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" +#: settings/serializers/settings.py:193 +msgid "Corporation ID(corpid)" +msgstr "企业 ID(CorpId)" + +#: settings/serializers/settings.py:194 +msgid "Agent ID(agentid)" +msgstr "应用 ID(AgentId)" + +#: settings/serializers/settings.py:195 +msgid "Secret(secret)" +msgstr "秘钥(secret)" + +#: settings/serializers/settings.py:196 +msgid "Enable WeCom Auth" +msgstr "启用企业微信认证" + +#: settings/serializers/settings.py:200 +msgid "AgentId" +msgstr "应用 ID(AgentId)" + +#: settings/serializers/settings.py:201 +msgid "AppKey" +msgstr "应用 Key(AppKey)" + +#: settings/serializers/settings.py:202 +msgid "AppSecret" +msgstr "应用密文(AppSecret)" + +#: settings/serializers/settings.py:203 +msgid "Enable DingTalk Auth" +msgstr "启用钉钉认证" + #: settings/utils/ldap.py:411 msgid "Host or port is disconnected: {}" msgstr "主机或端口不可连接: {}" @@ -2532,12 +2641,7 @@ msgstr "商业支持" #: templates/_header_bar.html:70 templates/_nav.html:30 #: templates/_nav_user.html:37 users/forms/profile.py:43 -#: users/templates/users/_user.html:44 #: users/templates/users/user_password_update.html:39 -#: users/templates/users/user_profile.html:17 -#: users/templates/users/user_profile_update.html:37 -#: users/templates/users/user_profile_update.html:61 -#: users/templates/users/user_pubkey_update.html:37 msgid "Profile" msgstr "个人信息" @@ -2659,8 +2763,6 @@ msgid "Applications" msgstr "应用管理" #: templates/_nav.html:64 templates/_nav.html:82 templates/_nav_user.html:16 -#: users/templates/users/user_remote_app_permission.html:39 -#: users/templates/users/user_remote_app_permission.html:64 msgid "RemoteApp" msgstr "远程应用" @@ -2698,7 +2800,8 @@ msgstr "Web终端" msgid "File manager" msgstr "文件管理" -#: templates/_nav.html:110 terminal/serializers/session.py:37 +#: templates/_nav.html:110 terminal/apps.py:9 +#: terminal/serializers/session.py:40 msgid "Terminal" msgstr "终端" @@ -2787,6 +2890,14 @@ msgstr "确认删除" msgid "Are you sure delete" msgstr "您确定删除吗?" +#: templates/flash_message_standalone.html:28 +msgid "Cancel" +msgstr "取消" + +#: templates/flash_message_standalone.html:37 +msgid "Go" +msgstr "" + #: templates/index.html:11 msgid "Total users" msgstr "用户总数" @@ -2981,6 +3092,10 @@ msgstr "测试成功" msgid "Test failure: Account invalid" msgstr "测试失败: 账户无效" +#: terminal/backends/command/es.py:27 +msgid "Invalid elasticsearch config" +msgstr "" + #: terminal/backends/command/models.py:14 msgid "Ordinary" msgstr "普通" @@ -3028,15 +3143,13 @@ msgid "High" msgstr "较高" #: terminal/const.py:33 users/templates/users/reset_password.html:50 -#: users/templates/users/user_create.html:35 #: users/templates/users/user_password_update.html:104 -#: users/templates/users/user_update.html:57 msgid "Normal" msgstr "正常" #: terminal/const.py:34 msgid "Offline" -msgstr "" +msgstr "离线" #: terminal/exceptions.py:8 msgid "Bulk create not support" @@ -3046,15 +3159,15 @@ msgstr "不支持批量创建" msgid "Storage is invalid" msgstr "存储无效" -#: terminal/models/session.py:43 +#: terminal/models/session.py:44 msgid "Login from" msgstr "登录来源" -#: terminal/models/session.py:47 +#: terminal/models/session.py:48 msgid "Replay" msgstr "回放" -#: terminal/models/session.py:52 +#: terminal/models/session.py:53 msgid "Date end" msgstr "结束日期" @@ -3114,27 +3227,27 @@ msgstr "命令存储" msgid "Replay storage" msgstr "录像存储" -#: terminal/serializers/session.py:30 +#: terminal/serializers/session.py:33 msgid "User ID" msgstr "用户 ID" -#: terminal/serializers/session.py:31 +#: terminal/serializers/session.py:34 msgid "Asset ID" msgstr "资产 ID" -#: terminal/serializers/session.py:32 +#: terminal/serializers/session.py:35 msgid "System user ID" msgstr "系统用户 ID" -#: terminal/serializers/session.py:33 +#: terminal/serializers/session.py:36 msgid "Login from for display" msgstr "登录来源(显示名称)" -#: terminal/serializers/session.py:35 +#: terminal/serializers/session.py:38 msgid "Can replay" msgstr "是否可重放" -#: terminal/serializers/session.py:36 +#: terminal/serializers/session.py:39 msgid "Can join" msgstr "是否可加入" @@ -3201,9 +3314,9 @@ msgstr "文档类型" #: terminal/serializers/storage.py:185 msgid "Ignore Certificate Verification" -msgstr "" +msgstr "忽略证书认证" -#: terminal/serializers/terminal.py:66 terminal/serializers/terminal.py:74 +#: terminal/serializers/terminal.py:73 terminal/serializers/terminal.py:81 msgid "Not found" msgstr "没有发现" @@ -3291,15 +3404,15 @@ msgstr "申请资产" msgid "Apply for application" msgstr "申请应用" -#: tickets/const.py:16 tickets/const.py:23 +#: tickets/const.py:17 tickets/const.py:24 msgid "Open" msgstr "打开" -#: tickets/const.py:17 +#: tickets/const.py:18 msgid "Approve" msgstr "同意" -#: tickets/const.py:24 +#: tickets/const.py:25 msgid "Closed" msgstr "关闭" @@ -3414,6 +3527,34 @@ msgstr "工单申请信息" msgid "Ticket approved info" msgstr "工单批准信息" +#: tickets/handler/command_confirm.py:24 +msgid "Applied run user" +msgstr "申请运行的用户" + +#: tickets/handler/command_confirm.py:25 +msgid "Applied run asset" +msgstr "申请运行的资产" + +#: tickets/handler/command_confirm.py:26 +msgid "Applied run system user" +msgstr "申请运行的系统用户" + +#: tickets/handler/command_confirm.py:27 +msgid "Applied run command" +msgstr "申请运行的命令" + +#: tickets/handler/command_confirm.py:28 +msgid "Applied from session" +msgstr "申请来自会话" + +#: tickets/handler/command_confirm.py:29 +msgid "Applied from command filter rules" +msgstr "申请来自命令过滤规则" + +#: tickets/handler/command_confirm.py:30 +msgid "Applied from command filter" +msgstr "申请来自命令过滤规则" + #: tickets/handler/login_asset_confirm.py:16 msgid "Applied login user" msgstr "申请登录的用户" @@ -3551,6 +3692,34 @@ msgstr "批准的资产" msgid "No `Asset` are found under Organization `{}`" msgstr "在组织 `{}` 下没有发现 `资产`" +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:12 +msgid "Run user" +msgstr "运行的用户" + +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:13 +msgid "Run asset" +msgstr "运行的资产" + +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:15 +msgid "Run system user" +msgstr "运行的系统用户" + +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:17 +msgid "Run command" +msgstr "运行的命令" + +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:18 +msgid "From session" +msgstr "来自会话" + +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:20 +msgid "From cmd filter rule" +msgstr "来自命令过滤规则" + +#: tickets/serializers/ticket/meta/ticket_type/command_confirm.py:22 +msgid "From cmd filter" +msgstr "来自命令过滤规则" + #: tickets/serializers/ticket/meta/ticket_type/common.py:11 msgid "Created by ticket ({}-{})" msgstr "通过工单创建 ({}-{})" @@ -3579,13 +3748,13 @@ msgstr "动作 (显示名称)" msgid "Status display" msgstr "状态(显示名称)" -#: tickets/serializers/ticket/ticket.py:99 +#: tickets/serializers/ticket/ticket.py:101 msgid "" "The `type` in the submission data (`{}`) is different from the type in the " "request url (`{}`)" msgstr "提交数据中的类型 (`{}`) 与请求URL地址中的类型 (`{}`) 不一致" -#: tickets/serializers/ticket/ticket.py:120 +#: tickets/serializers/ticket/ticket.py:122 msgid "None of the assignees belong to Organization `{}` admins" msgstr "所有受理人都不属于组织 `{}` 下的管理员" @@ -3655,9 +3824,7 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:101 users/models/user.py:532 -#: users/templates/users/user_detail.html:57 -#: users/templates/users/user_profile.html:59 +#: users/forms/profile.py:101 users/models/user.py:560 msgid "Email" msgstr "邮件" @@ -3673,7 +3840,7 @@ msgstr "原来密码错误" msgid "Automatically configure and download the SSH key" msgstr "自动配置并下载SSH密钥" -#: users/forms/profile.py:130 users/templates/users/user_update.html:30 +#: users/forms/profile.py:130 msgid "ssh public key" msgstr "SSH公钥" @@ -3689,50 +3856,48 @@ msgstr "复制你的公钥到这里" msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms/profile.py:149 users/serializers/profile.py:71 -#: users/serializers/profile.py:144 users/serializers/profile.py:157 +#: users/forms/profile.py:149 users/serializers/profile.py:76 +#: users/serializers/profile.py:150 users/serializers/profile.py:163 msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/models/user.py:157 +#: users/models/user.py:182 msgid "System administrator" msgstr "系统管理员" -#: users/models/user.py:158 +#: users/models/user.py:183 msgid "System auditor" msgstr "系统审计员" -#: users/models/user.py:447 users/templates/users/user_profile.html:90 +#: users/models/user.py:472 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:512 +#: users/models/user.py:537 msgid "Local" msgstr "数据库" -#: users/models/user.py:543 +#: users/models/user.py:571 msgid "Avatar" msgstr "头像" -#: users/models/user.py:546 users/templates/users/user_detail.html:68 +#: users/models/user.py:574 msgid "Wechat" msgstr "微信" -#: users/models/user.py:576 users/templates/users/user_detail.html:89 -#: users/templates/users/user_list.html:18 -#: users/templates/users/user_profile.html:102 +#: users/models/user.py:604 msgid "Source" msgstr "用户来源" -#: users/models/user.py:580 +#: users/models/user.py:608 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:715 +#: users/models/user.py:754 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:718 +#: users/models/user.py:757 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -3740,15 +3905,19 @@ msgstr "Administrator是初始的超级管理员" msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/profile.py:37 users/serializers/user.py:112 +#: users/serializers/profile.py:36 users/serializers/user.py:125 msgid "Password does not match security rules" msgstr "密码不满足安全规则" -#: users/serializers/profile.py:43 +#: users/serializers/profile.py:40 +msgid "The new password cannot be the last {} passwords" +msgstr "新密码不能是最近 {} 次的密码" + +#: users/serializers/profile.py:48 msgid "The newly set password is inconsistent" msgstr "两次密码不一致" -#: users/serializers/profile.py:115 users/serializers/user.py:67 +#: users/serializers/profile.py:121 users/serializers/user.py:80 msgid "Is first login" msgstr "首次登录" @@ -3789,35 +3958,35 @@ msgstr "是否可更新" msgid "Can delete" msgstr "是否可删除" -#: users/serializers/user.py:38 users/serializers/user.py:74 +#: users/serializers/user.py:39 users/serializers/user.py:87 msgid "Organization role name" msgstr "组织角色名称" -#: users/serializers/user.py:70 +#: users/serializers/user.py:83 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:72 +#: users/serializers/user.py:85 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:73 +#: users/serializers/user.py:86 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:75 +#: users/serializers/user.py:88 msgid "Super role name" msgstr "超级角色名称" -#: users/serializers/user.py:76 +#: users/serializers/user.py:89 msgid "Total role name" msgstr "汇总角色名称" -#: users/serializers/user.py:100 +#: users/serializers/user.py:113 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:197 +#: users/serializers/user.py:210 msgid "name not unique" msgstr "名称重复" @@ -3825,9 +3994,8 @@ msgstr "名称重复" msgid "Security token validation" msgstr "安全令牌验证" -#: users/templates/users/_base_otp.html:14 users/templates/users/_user.html:13 -#: users/templates/users/user_profile_update.html:55 -#: xpack/plugins/cloud/models.py:78 xpack/plugins/cloud/serializers.py:145 +#: users/templates/users/_base_otp.html:14 xpack/plugins/cloud/models.py:78 +#: xpack/plugins/cloud/serializers.py:145 msgid "Account" msgstr "账户" @@ -3847,36 +4015,6 @@ msgstr "选择用户" msgid "Asset num" msgstr "资产数量" -#: users/templates/users/_user.html:21 -msgid "Auth" -msgstr "认证" - -#: users/templates/users/_user.html:27 -msgid "Security and Role" -msgstr "角色安全" - -#: users/templates/users/_user.html:51 -#: users/templates/users/user_bulk_update.html:23 -#: users/templates/users/user_detail.html:168 -#: users/templates/users/user_group_create_update.html:27 -#: users/templates/users/user_password_update.html:74 -#: users/templates/users/user_profile.html:209 -#: users/templates/users/user_profile_update.html:67 -#: users/templates/users/user_pubkey_update.html:74 -#: users/templates/users/user_pubkey_update.html:80 -msgid "Reset" -msgstr "重置" - -#: users/templates/users/_user.html:52 -#: users/templates/users/forgot_password.html:32 -#: users/templates/users/user_bulk_update.html:24 -#: users/templates/users/user_list.html:40 -#: users/templates/users/user_password_update.html:75 -#: users/templates/users/user_profile_update.html:68 -#: users/templates/users/user_pubkey_update.html:81 -msgid "Submit" -msgstr "提交" - #: users/templates/users/_user_detail_nav_header.html:11 msgid "User detail" msgstr "用户详情" @@ -3886,8 +4024,6 @@ msgid "User permissions" msgstr "用户授权" #: users/templates/users/_user_detail_nav_header.html:23 -#: users/templates/users/user_group_detail.html:20 -#: users/templates/users/user_group_granted_asset.html:21 msgid "Asset granted" msgstr "授权的资产" @@ -3932,66 +4068,54 @@ msgstr "获取更多信息" msgid "Input your email, that will send a mail to your" msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中" +#: users/templates/users/forgot_password.html:32 +#: users/templates/users/user_password_update.html:75 +msgid "Submit" +msgstr "提交" + #: users/templates/users/reset_password.html:5 -#: users/templates/users/reset_password.html:6 -#: users/templates/users/user_detail.html:402 users/utils.py:83 +#: 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_create.html:13 #: users/templates/users/user_password_update.html:64 -#: users/templates/users/user_update.html:13 msgid "Your password must satisfy" msgstr "您的密码必须满足:" #: users/templates/users/reset_password.html:24 -#: users/templates/users/user_create.html:14 #: users/templates/users/user_password_update.html:65 -#: users/templates/users/user_update.html:14 msgid "Password strength" msgstr "密码强度:" #: users/templates/users/reset_password.html:48 -#: users/templates/users/user_create.html:33 #: users/templates/users/user_password_update.html:102 -#: users/templates/users/user_update.html:55 msgid "Very weak" msgstr "很弱" #: users/templates/users/reset_password.html:49 -#: users/templates/users/user_create.html:34 #: users/templates/users/user_password_update.html:103 -#: users/templates/users/user_update.html:56 msgid "Weak" msgstr "弱" #: users/templates/users/reset_password.html:51 -#: users/templates/users/user_create.html:36 #: users/templates/users/user_password_update.html:105 -#: users/templates/users/user_update.html:58 msgid "Medium" msgstr "一般" #: users/templates/users/reset_password.html:52 -#: users/templates/users/user_create.html:37 #: users/templates/users/user_password_update.html:106 -#: users/templates/users/user_update.html:59 msgid "Strong" msgstr "强" #: users/templates/users/reset_password.html:53 -#: users/templates/users/user_create.html:38 #: users/templates/users/user_password_update.html:107 -#: users/templates/users/user_update.html:60 msgid "Very strong" msgstr "很强" #: users/templates/users/user_asset_permission.html:43 #: users/templates/users/user_asset_permission.html:155 #: users/templates/users/user_database_app_permission.html:41 -#: users/templates/users/user_list.html:19 -#: users/templates/users/user_remote_app_permission.html:41 #: xpack/plugins/cloud/models.py:34 msgid "Validity" msgstr "有效" @@ -4008,229 +4132,6 @@ msgstr "包含" msgid "Exclude" msgstr "不包含" -#: users/templates/users/user_bulk_update.html:8 -msgid "Select properties that need to be modified" -msgstr "选择需要修改属性" - -#: users/templates/users/user_bulk_update.html:10 -msgid "Select all" -msgstr "全选" - -#: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:7 -msgid "Create user" -msgstr "创建用户" - -#: users/templates/users/user_detail.html:80 -msgid "Force enabled" -msgstr "强制启用" - -#: users/templates/users/user_detail.html:101 -#: users/templates/users/user_profile.html:106 -msgid "Date joined" -msgstr "创建日期" - -#: users/templates/users/user_detail.html:105 -#: users/templates/users/user_profile.html:110 -msgid "Last login" -msgstr "最后登录" - -#: users/templates/users/user_detail.html:110 -#: users/templates/users/user_profile.html:115 -msgid "Last password updated" -msgstr "最后更新密码" - -#: users/templates/users/user_detail.html:126 -#: users/templates/users/user_profile.html:150 -msgid "Quick modify" -msgstr "快速修改" - -#: users/templates/users/user_detail.html:148 -msgid "Force enabled MFA" -msgstr "强制启用多因子认证" - -#: users/templates/users/user_detail.html:165 -msgid "Reset MFA" -msgstr "重置多因子认证" - -#: users/templates/users/user_detail.html:174 -msgid "Send reset password mail" -msgstr "发送重置密码邮件" - -#: users/templates/users/user_detail.html:177 -#: users/templates/users/user_detail.html:187 -msgid "Send" -msgstr "发送" - -#: users/templates/users/user_detail.html:184 -msgid "Send reset ssh key mail" -msgstr "发送重置密钥邮件" - -#: users/templates/users/user_detail.html:193 -#: users/templates/users/user_detail.html:490 -msgid "Unblock user" -msgstr "解除登录限制" - -#: users/templates/users/user_detail.html:196 -msgid "Unblock" -msgstr "解除" - -#: users/templates/users/user_detail.html:217 -msgid "Join user groups" -msgstr "添加到用户组" - -#: users/templates/users/user_detail.html:226 -msgid "Join" -msgstr "加入" - -#: users/templates/users/user_detail.html:356 -#: users/templates/users/user_detail.html:383 -msgid "Update successfully!" -msgstr "更新成功" - -#: users/templates/users/user_detail.html:365 -msgid "Goto profile page enable MFA" -msgstr "请去个人信息页面启用自己的多因子认证" - -#: users/templates/users/user_detail.html:401 -msgid "An e-mail has been sent to the user`s mailbox." -msgstr "已发送邮件到用户邮箱" - -#: users/templates/users/user_detail.html:411 -#: users/templates/users/user_detail.html:437 -#: users/templates/users/user_detail.html:505 -#: users/templates/users/user_list.html:178 -msgid "Are you sure?" -msgstr "你确认吗?" - -#: users/templates/users/user_detail.html:412 -msgid "This will reset the user password and send a reset mail" -msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" - -#: users/templates/users/user_detail.html:415 -#: users/templates/users/user_detail.html:441 -#: users/templates/users/user_detail.html:509 -#: users/templates/users/user_list.html:182 -msgid "Cancel" -msgstr "取消" - -#: users/templates/users/user_detail.html:427 -msgid "" -"The reset-ssh-public-key E-mail has been sent successfully. Please inform " -"the user to update his new ssh public key." -msgstr "重设密钥邮件将会发送到用户邮箱" - -#: users/templates/users/user_detail.html:428 -msgid "Reset SSH public key" -msgstr "重置SSH密钥" - -#: users/templates/users/user_detail.html:438 -msgid "This will reset the user public key and send a reset mail" -msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" - -#: users/templates/users/user_detail.html:456 -msgid "Successfully updated the SSH public key." -msgstr "更新SSH密钥成功" - -#: users/templates/users/user_detail.html:457 -#: users/templates/users/user_detail.html:461 -msgid "User SSH public key update" -msgstr "SSH密钥" - -#: users/templates/users/user_detail.html:506 -msgid "After unlocking the user, the user can log in normally." -msgstr "解除用户登录限制后,此用户即可正常登录" - -#: users/templates/users/user_detail.html:520 -msgid "Reset user MFA success" -msgstr "重置用户多因子认证成功" - -#: users/templates/users/user_granted_remote_app.html:35 -msgid "App type" -msgstr "应用类型" - -#: users/templates/users/user_group_detail.html:17 -#: users/templates/users/user_group_granted_asset.html:18 -msgid "User group detail" -msgstr "用户组详情" - -#: users/templates/users/user_group_detail.html:81 -msgid "Add user" -msgstr "添加用户" - -#: users/templates/users/user_group_detail.html:87 -msgid "Add" -msgstr "添加" - -#: users/templates/users/user_group_list.html:7 -msgid "Create user group" -msgstr "创建用户组" - -#: users/templates/users/user_list.html:30 -msgid "Delete selected" -msgstr "批量删除" - -#: users/templates/users/user_list.html:32 -msgid "Remove selected" -msgstr "批量移除" - -#: users/templates/users/user_list.html:34 -msgid "Update selected" -msgstr "批量更新" - -#: users/templates/users/user_list.html:35 -msgid "Deactive selected" -msgstr "禁用所选" - -#: users/templates/users/user_list.html:36 -msgid "Active selected" -msgstr "激活所选" - -#: users/templates/users/user_list.html:106 -#: users/templates/users/user_list.html:110 -msgid "Remove" -msgstr "移除" - -#: users/templates/users/user_list.html:179 -msgid "This will delete the selected users !!!" -msgstr "删除选中用户 !!!" - -#: users/templates/users/user_list.html:190 -msgid "User Deleting failed." -msgstr "用户删除失败" - -#: users/templates/users/user_list.html:191 -msgid "User Delete" -msgstr "删除" - -#: users/templates/users/user_list.html:213 -msgid "This will remove the selected users !!" -msgstr "移除选中用户 !!!" - -#: users/templates/users/user_list.html:215 -msgid "User Removing failed." -msgstr "用户移除失败" - -#: users/templates/users/user_list.html:216 -msgid "User Remove" -msgstr "移除" - -#: users/templates/users/user_list.html:265 -msgid "Are you sure about removing it?" -msgstr "您确定移除吗?" - -#: users/templates/users/user_list.html:266 -msgid "Remove the success" -msgstr "移除成功" - -#: users/templates/users/user_list.html:271 -msgid "User is expired" -msgstr "用户已失效" - -#: users/templates/users/user_list.html:274 -msgid "User is inactive" -msgstr "用户已禁用" - #: users/templates/users/user_otp_check_password.html:6 #: users/templates/users/user_verify_mfa.html:6 msgid "Authenticate" @@ -4275,69 +4176,15 @@ msgid "" "installed, go to the next step directly)." msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接进入下一步)" +#: users/templates/users/user_password_update.html:74 +msgid "Reset" +msgstr "重置" + #: users/templates/users/user_password_verify.html:8 #: users/templates/users/user_password_verify.html:9 msgid "Verify password" msgstr "校验密码" -#: users/templates/users/user_profile.html:97 -msgid "Administrator Settings force MFA login" -msgstr "管理员设置强制使用多因子认证" - -#: users/templates/users/user_profile.html:156 -msgid "Set MFA" -msgstr "设置多因子认证" - -#: users/templates/users/user_profile.html:178 -msgid "Update MFA" -msgstr "更改多因子认证" - -#: users/templates/users/user_profile.html:188 -msgid "Update password" -msgstr "更改密码" - -#: users/templates/users/user_profile.html:198 -msgid "Update SSH public key" -msgstr "更改SSH密钥" - -#: users/templates/users/user_profile.html:206 -msgid "Reset public key and download" -msgstr "重置并下载SSH密钥" - -#: users/templates/users/user_pubkey_update.html:55 -msgid "Old public key" -msgstr "原来SSH密钥" - -#: users/templates/users/user_pubkey_update.html:63 -msgid "Fingerprint" -msgstr "指纹" - -#: users/templates/users/user_pubkey_update.html:69 -msgid "Update public key" -msgstr "更新密钥" - -#: users/templates/users/user_pubkey_update.html:72 -msgid "Or reset by server" -msgstr "或者重置并下载密钥" - -#: users/templates/users/user_pubkey_update.html:98 -msgid "" -"The new public key has been set successfully, Please download the " -"corresponding private key." -msgstr "新的公钥已设置成功,请下载对应的私钥" - -#: users/templates/users/user_update.html:4 -msgid "Update user" -msgstr "更新用户" - -#: users/templates/users/user_update.html:22 users/views/profile/reset.py:120 -msgid "User auth from {}, go there change password" -msgstr "用户认证源来自 {}, 请去相应系统修改密码" - -#: users/templates/users/user_update.html:32 -msgid "User auth from {}, ssh key login is not supported" -msgstr "用户认证源来自 {}, 不支持使用 SSH Key 登录" - #: users/templates/users/user_verify_mfa.html:11 msgid "" "The account protection has been opened, please complete the following " @@ -4438,7 +4285,7 @@ msgstr "" "
\n" " " -#: users/utils.py:116 users/views/profile/reset.py:80 +#: users/utils.py:116 users/views/profile/reset.py:126 msgid "Reset password success" msgstr "重置密码成功" @@ -4667,7 +4514,7 @@ msgstr "多因子认证禁用成功,返回登录页面" msgid "Password update" msgstr "密码更新" -#: users/views/profile/password.py:60 users/views/profile/reset.py:127 +#: users/views/profile/password.py:60 users/views/profile/reset.py:105 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" @@ -4679,33 +4526,41 @@ msgstr "用户名或密码无效" msgid "Public key update" msgstr "密钥更新" -#: users/views/profile/reset.py:46 +#: users/views/profile/reset.py:40 +msgid "Send reset password message" +msgstr "发送重置密码邮件" + +#: users/views/profile/reset.py:41 +msgid "Send reset password mail success, login your mail box and follow it " +msgstr "" +"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" + +#: users/views/profile/reset.py:52 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/profile/reset.py:52 +#: users/views/profile/reset.py:58 msgid "" "The user is from {}, please go to the corresponding system to change the " "password" msgstr "用户来自 {} 请去相应系统修改密码" -#: users/views/profile/reset.py:66 -msgid "Send reset password message" -msgstr "发送重置密码邮件" - -#: users/views/profile/reset.py:67 -msgid "Send reset password mail success, login your mail box and follow it " -msgstr "" -"发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" - -#: users/views/profile/reset.py:81 -msgid "Reset password success, return to login page" -msgstr "重置密码成功,返回到登录页面" - -#: users/views/profile/reset.py:105 users/views/profile/reset.py:115 +#: users/views/profile/reset.py:83 users/views/profile/reset.py:93 msgid "Token invalid or expired" msgstr "Token错误或失效" +#: users/views/profile/reset.py:98 +msgid "User auth from {}, go there change password" +msgstr "用户认证源来自 {}, 请去相应系统修改密码" + +#: users/views/profile/reset.py:111 +msgid "* The new password cannot be the last {} passwords" +msgstr "* 新密码不能是最近 {} 次的密码" + +#: users/views/profile/reset.py:127 +msgid "Reset password success, return to login page" +msgstr "重置密码成功,返回到登录页面" + #: xpack/plugins/change_auth_plan/meta.py:9 #: xpack/plugins/change_auth_plan/models.py:89 #: xpack/plugins/change_auth_plan/models.py:184 @@ -5182,6 +5037,295 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" +#, fuzzy +#~| msgid "Agent Secret" +#~ msgid "WeCom Secret" +#~ msgstr "凭证密钥(secret)" + +#~ msgid "Server performance" +#~ msgstr "服务器性能" + +#~ msgid "Corporation Secret" +#~ msgstr "凭证密钥(Secret)" + +#~ msgid "Terminal command alert" +#~ msgstr "终端命令告警" + +#, python-format +#~ msgid "" +#~ "\n" +#~ " Command: %(command)s\n" +#~ "
\n" +#~ " Asset: %(host_name)s (%(host_ip)s)\n" +#~ "
\n" +#~ " User: %(user)s\n" +#~ "
\n" +#~ " Level: %(risk_level)s\n" +#~ "
\n" +#~ " Session: session detail\n" +#~ "
\n" +#~ " " +#~ msgstr "" +#~ "\n" +#~ " 命令: %(command)s\n" +#~ "
\n" +#~ " 资产: %(host_name)s (%(host_ip)s)\n" +#~ "
\n" +#~ " 用户: %(user)s\n" +#~ "
\n" +#~ " 等级: %(risk_level)s\n" +#~ "
\n" +#~ " 会话: 会话详情\n" +#~ "
\n" +#~ " " + +#~ msgid "Batch command alert" +#~ msgstr "批量命令告警" + +#, python-format +#~ msgid "" +#~ "\n" +#~ "
\n" +#~ " Assets: %(assets)s\n" +#~ "
\n" +#~ " User: %(user)s\n" +#~ "
\n" +#~ " Level: %(risk_level)s\n" +#~ "
\n" +#~ "\n" +#~ " ----------------- Commands ---------------- " +#~ "
\n" +#~ " %(command)s
\n" +#~ " ----------------- Commands ---------------- " +#~ "
\n" +#~ " " +#~ msgstr "" +#~ "\n" +#~ "
\n" +#~ " 资产: %(assets)s\n" +#~ "
\n" +#~ " 用户: %(user)s\n" +#~ "
\n" +#~ " 等级: %(risk_level)s\n" +#~ "
\n" +#~ "\n" +#~ " ----------------- 命令 ----------------
\n" +#~ " %(command)s
\n" +#~ " ----------------- 命令 ----------------
\n" +#~ " " + +#~ msgid "Ops" +#~ msgstr "选项" + +#~ msgid "Command Alert" +#~ msgstr "命令告警" + +#~ msgid "APP key" +#~ msgstr "APPKEY" + +#~ msgid "APP secret" +#~ msgstr "LDAP 地址" + +#~ msgid "Auth" +#~ msgstr "认证" + +#~ msgid "The administrator require you to change your password this time" +#~ msgstr "管理员要求您本次修改密码" + +#~ msgid "Security and Role" +#~ msgstr "角色安全" + +#~ msgid "Select properties that need to be modified" +#~ msgstr "选择需要修改属性" + +#~ msgid "Select all" +#~ msgstr "全选" + +#~ msgid "Create user" +#~ msgstr "创建用户" + +#~ msgid "Force enabled" +#~ msgstr "强制启用" + +#~ msgid "Date joined" +#~ msgstr "创建日期" + +#~ msgid "Last login" +#~ msgstr "最后登录" + +#~ msgid "Last password updated" +#~ msgstr "最后更新密码" + +#~ msgid "Quick modify" +#~ msgstr "快速修改" + +#~ msgid "Force enabled MFA" +#~ msgstr "强制启用多因子认证" + +#~ msgid "Reset MFA" +#~ msgstr "重置多因子认证" + +#~ msgid "Send reset password mail" +#~ msgstr "发送重置密码邮件" + +#~ msgid "Send" +#~ msgstr "发送" + +#~ msgid "Send reset ssh key mail" +#~ msgstr "发送重置密钥邮件" + +#~ msgid "Unblock user" +#~ msgstr "解除登录限制" + +#~ msgid "Unblock" +#~ msgstr "解除" + +#~ msgid "Join user groups" +#~ msgstr "添加到用户组" + +#~ msgid "Join" +#~ msgstr "加入" + +#~ msgid "Goto profile page enable MFA" +#~ msgstr "请去个人信息页面启用自己的多因子认证" + +#~ msgid "An e-mail has been sent to the user`s mailbox." +#~ msgstr "已发送邮件到用户邮箱" + +#~ msgid "Are you sure?" +#~ msgstr "你确认吗?" + +#~ msgid "This will reset the user password and send a reset mail" +#~ msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" + +#~ msgid "" +#~ "The reset-ssh-public-key E-mail has been sent successfully. Please inform " +#~ "the user to update his new ssh public key." +#~ msgstr "重设密钥邮件将会发送到用户邮箱" + +#~ msgid "Reset SSH public key" +#~ msgstr "重置SSH密钥" + +#~ msgid "This will reset the user public key and send a reset mail" +#~ msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" + +#~ msgid "Successfully updated the SSH public key." +#~ msgstr "更新SSH密钥成功" + +#~ msgid "User SSH public key update" +#~ msgstr "SSH密钥" + +#~ msgid "After unlocking the user, the user can log in normally." +#~ msgstr "解除用户登录限制后,此用户即可正常登录" + +#~ msgid "Reset user MFA success" +#~ msgstr "重置用户多因子认证成功" + +#~ msgid "App type" +#~ msgstr "应用类型" + +#~ msgid "User group detail" +#~ msgstr "用户组详情" + +#~ msgid "Add user" +#~ msgstr "添加用户" + +#~ msgid "Add" +#~ msgstr "添加" + +#~ msgid "Create user group" +#~ msgstr "创建用户组" + +#~ msgid "Delete selected" +#~ msgstr "批量删除" + +#~ msgid "Remove selected" +#~ msgstr "批量移除" + +#~ msgid "Update selected" +#~ msgstr "批量更新" + +#~ msgid "Deactive selected" +#~ msgstr "禁用所选" + +#~ msgid "Active selected" +#~ msgstr "激活所选" + +#~ msgid "Remove" +#~ msgstr "移除" + +#~ msgid "This will delete the selected users !!!" +#~ msgstr "删除选中用户 !!!" + +#~ msgid "User Deleting failed." +#~ msgstr "用户删除失败" + +#~ msgid "User Delete" +#~ msgstr "删除" + +#~ msgid "This will remove the selected users !!" +#~ msgstr "移除选中用户 !!!" + +#~ msgid "User Removing failed." +#~ msgstr "用户移除失败" + +#~ msgid "User Remove" +#~ msgstr "移除" + +#~ msgid "Are you sure about removing it?" +#~ msgstr "您确定移除吗?" + +#~ msgid "Remove the success" +#~ msgstr "移除成功" + +#~ msgid "User is expired" +#~ msgstr "用户已失效" + +#~ msgid "User is inactive" +#~ msgstr "用户已禁用" + +#~ msgid "Administrator Settings force MFA login" +#~ msgstr "管理员设置强制使用多因子认证" + +#~ msgid "Set MFA" +#~ msgstr "设置多因子认证" + +#~ msgid "Update MFA" +#~ msgstr "更改多因子认证" + +#~ msgid "Update password" +#~ msgstr "更改密码" + +#~ msgid "Update SSH public key" +#~ msgstr "更改SSH密钥" + +#~ msgid "Reset public key and download" +#~ msgstr "重置并下载SSH密钥" + +#~ msgid "Old public key" +#~ msgstr "原来SSH密钥" + +#~ msgid "Fingerprint" +#~ msgstr "指纹" + +#~ msgid "Update public key" +#~ msgstr "更新密钥" + +#~ msgid "Or reset by server" +#~ msgstr "或者重置并下载密钥" + +#~ msgid "" +#~ "The new public key has been set successfully, Please download the " +#~ "corresponding private key." +#~ msgstr "新的公钥已设置成功,请下载对应的私钥" + +#~ msgid "Update user" +#~ msgstr "更新用户" + +#~ msgid "User auth from {}, ssh key login is not supported" +#~ msgstr "用户认证源来自 {}, 不支持使用 SSH Key 登录" + #~ msgid "(Domain name support)" #~ msgstr "(支持域名)" diff --git a/apps/ops/apps.py b/apps/ops/apps.py index 01dfd05fa..8bdc04ce8 100644 --- a/apps/ops/apps.py +++ b/apps/ops/apps.py @@ -1,10 +1,12 @@ from __future__ import unicode_literals +from django.utils.translation import gettext_lazy as _ from django.apps import AppConfig class OpsConfig(AppConfig): name = 'ops' + verbose_name = _('Operations') def ready(self): from orgs.models import Organization diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 9327ce0a8..badbed2b4 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -14,11 +14,15 @@ class AdHocExecutionSerializer(serializers.ModelSerializer): class Meta: model = AdHocExecution - fields = [ - 'id', 'task', 'task_display', 'hosts_amount', 'adhoc', 'date_start', 'stat', - 'date_finished', 'timedelta', 'is_finished', 'is_success', 'result', 'summary', - 'short_id', 'adhoc_short_id', 'last_success', 'last_failure' + fields_mini = ['id'] + fields_small = fields_mini + [ + 'hosts_amount', 'timedelta', 'result', 'summary', 'short_id', + 'is_finished', 'is_success', + 'date_start', 'date_finished', ] + fields_fk = ['task', 'task_display', 'adhoc', 'adhoc_short_id',] + fields_custom = ['stat', 'last_success', 'last_failure'] + fields = fields_small + fields_fk + fields_custom @staticmethod def get_task(obj): @@ -52,11 +56,16 @@ class TaskSerializer(BulkOrgResourceModelSerializer): class Meta: model = Task - fields = [ - 'id', 'name', 'interval', 'crontab', 'is_periodic', - 'is_deleted', 'comment', 'date_created', - 'date_updated', 'latest_execution', 'summary', + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'interval', 'crontab', + 'is_periodic', 'is_deleted', + 'date_created', 'date_updated', + 'comment', ] + fields_fk = ['latest_execution'] + fields_custom = ['summary'] + fields = fields_small + fields_fk + fields_custom read_only_fields = [ 'is_deleted', 'date_created', 'date_updated', 'latest_adhoc', 'latest_execution', 'total_run_amount', @@ -77,12 +86,16 @@ class AdHocSerializer(serializers.ModelSerializer): class Meta: model = AdHoc - fields = [ - "id", "task", 'tasks', "pattern", "options", - "hosts", "run_as_admin", "run_as", "become", - "date_created", "short_id", - "become_display", + fields_mini = ['id'] + fields_small = fields_mini + [ + 'tasks', "pattern", "options", "run_as", + "become", "become_display", "short_id", + "run_as_admin", + "date_created", ] + fields_fk = ["task"] + fields_m2m = ["hosts"] + fields = fields_small + fields_fk + fields_m2m read_only_fields = [ 'date_created' ] @@ -99,8 +112,8 @@ class AdHocExecutionNestSerializer(serializers.ModelSerializer): class Meta: model = AdHocExecution fields = ( - 'last_success', 'last_failure', 'last_run', 'timedelta', 'is_finished', - 'is_success' + 'last_success', 'last_failure', 'last_run', 'timedelta', + 'is_finished', 'is_success' ) @@ -120,10 +133,15 @@ class CommandExecutionSerializer(serializers.ModelSerializer): class Meta: model = CommandExecution - fields = [ - 'id', 'hosts', 'run_as', 'command', 'result', 'log_url', - 'is_finished', 'date_created', 'date_finished' + fields_mini = ['id'] + fields_small = fields_mini + [ + 'command', 'result', 'log_url', + 'is_finished', + 'date_created', 'date_finished' ] + fields_fk = ['run_as'] + fields_m2m = ['hosts'] + fields = fields_small + fields_fk + fields_m2m read_only_fields = [ 'result', 'is_finished', 'log_url', 'date_created', 'date_finished' diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index 9c29659e4..c9d60bd18 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -6,12 +6,12 @@ from orgs.utils import current_org, tmp_to_org from common.cache import Cache, IntegerField from common.utils import get_logger from users.models import UserGroup, User -from assets.models import Node, AdminUser, SystemUser, Domain, Gateway +from assets.models import Node, AdminUser, SystemUser, Domain, Gateway, Asset +from terminal.models import Session from applications.models import Application from perms.models import AssetPermission, ApplicationPermission from .models import OrganizationMember - logger = get_logger(__file__) @@ -64,6 +64,9 @@ class OrgResourceStatisticsCache(OrgRelatedCache): asset_perms_amount = IntegerField(queryset=AssetPermission.objects) app_perms_amount = IntegerField(queryset=ApplicationPermission.objects) + total_count_online_users = IntegerField() + total_count_online_sessions = IntegerField() + def __init__(self, org): super().__init__() self.org = org @@ -86,3 +89,9 @@ class OrgResourceStatisticsCache(OrgRelatedCache): def compute_assets_amount(self): node = Node.org_root() return node.assets_amount + + def compute_total_count_online_users(self): + return len(set(Session.objects.filter(is_finished=False).values_list('user_id', flat=True))) + + def compute_total_count_online_sessions(self): + return Session.objects.filter(is_finished=False).count() diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index 482c622e1..1a6aca0b0 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -38,8 +38,10 @@ class OrgSerializer(ModelSerializer): list_serializer_class = AdaptedBulkListSerializer fields_mini = ['id', 'name'] fields_small = fields_mini + [ - 'is_default', 'is_root', 'comment', - 'created_by', 'date_created', 'resource_statistics' + 'resource_statistics', + 'is_default', 'is_root', + 'date_created', + 'comment', 'created_by', ] fields_m2m = ['users', 'admins', 'auditors'] @@ -79,7 +81,12 @@ class OrgMemberSerializer(BulkModelSerializer): class Meta: model = OrganizationMember - fields = ('id', 'org', 'user', 'role', 'org_display', 'user_display', 'role_display') + fields_mini = ['id'] + fields_small = fields_mini + [ + 'role', 'role_display' + ] + fields_fk = ['org', 'user', 'org_display', 'user_display',] + fields = fields_small + fields_fk use_model_bulk_create = True model_bulk_create_kwargs = {'ignore_conflicts': True} diff --git a/apps/orgs/signals_handler/cache.py b/apps/orgs/signals_handler/cache.py index c3c23efb4..5862d5edf 100644 --- a/apps/orgs/signals_handler/cache.py +++ b/apps/orgs/signals_handler/cache.py @@ -1,5 +1,5 @@ from django.db.models.signals import m2m_changed -from django.db.models.signals import post_save, pre_delete +from django.db.models.signals import post_save, pre_delete, pre_save from django.dispatch import receiver from orgs.models import Organization, OrganizationMember @@ -7,6 +7,7 @@ from assets.models import Node from perms.models import (AssetPermission, ApplicationPermission) from users.models import UserGroup, User from applications.models import Application +from terminal.models import Session from assets.models import Asset, AdminUser, SystemUser, Domain, Gateway from common.const.signals import POST_PREFIX from orgs.caches import OrgResourceStatisticsCache @@ -47,16 +48,17 @@ def on_org_user_changed_refresh_cache(sender, action, instance, reverse, pk_set, class OrgResourceStatisticsRefreshUtil: model_cache_field_mapper = { - ApplicationPermission: 'app_perms_amount', - AssetPermission: 'asset_perms_amount', - Application: 'applications_amount', - Gateway: 'gateways_amount', - Domain: 'domains_amount', - SystemUser: 'system_users_amount', - AdminUser: 'admin_users_amount', - Node: 'nodes_amount', - Asset: 'assets_amount', - UserGroup: 'groups_amount', + ApplicationPermission: ['app_perms_amount'], + AssetPermission: ['asset_perms_amount'], + Application: ['applications_amount'], + Gateway: ['gateways_amount'], + Domain: ['domains_amount'], + SystemUser: ['system_users_amount'], + AdminUser: ['admin_users_amount'], + Node: ['nodes_amount'], + Asset: ['assets_amount'], + UserGroup: ['groups_amount'], + Session: ['total_count_online_users', 'total_count_online_sessions'] } @classmethod @@ -64,13 +66,12 @@ class OrgResourceStatisticsRefreshUtil: cache_field_name = cls.model_cache_field_mapper.get(type(instance)) if cache_field_name: org_cache = OrgResourceStatisticsCache(instance.org) - org_cache.expire(cache_field_name) + org_cache.expire(*cache_field_name) -@receiver(post_save) -def on_post_save_refresh_org_resource_statistics_cache(sender, instance, created, **kwargs): - if created: - OrgResourceStatisticsRefreshUtil.refresh_if_need(instance) +@receiver(pre_save) +def on_post_save_refresh_org_resource_statistics_cache(sender, instance, **kwargs): + OrgResourceStatisticsRefreshUtil.refresh_if_need(instance) @receiver(pre_delete) diff --git a/apps/orgs/signals_handler/common.py b/apps/orgs/signals_handler/common.py index f59c4cb47..6beecd5cb 100644 --- a/apps/orgs/signals_handler/common.py +++ b/apps/orgs/signals_handler/common.py @@ -8,7 +8,6 @@ from django.dispatch import receiver from django.utils.functional import LazyObject from django.db.models.signals import m2m_changed from django.db.models.signals import post_save, post_delete, pre_delete -from django.utils.translation import ugettext as _ from orgs.utils import tmp_to_org from orgs.models import Organization, OrganizationMember @@ -19,7 +18,6 @@ from common.const.signals import PRE_REMOVE, POST_REMOVE from common.signals import django_ready from common.utils import get_logger from common.utils.connection import RedisPubSub -from common.exceptions import JMSException logger = get_logger(__file__) diff --git a/apps/perms/api/application/user_permission/common.py b/apps/perms/api/application/user_permission/common.py index 4e7d0b5f2..2a64d122d 100644 --- a/apps/perms/api/application/user_permission/common.py +++ b/apps/perms/api/application/user_permission/common.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- # -import uuid +import time + from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from rest_framework.views import APIView, Response +from rest_framework import status from rest_framework.generics import ( ListAPIView, get_object_or_404 ) @@ -12,7 +14,8 @@ from orgs.utils import tmp_to_root_org from applications.models import Application from perms.utils.application.permission import ( has_application_system_permission, - get_application_system_user_ids + get_application_system_user_ids, + validate_permission, ) from perms.api.asset.user_permission.mixin import RoleAdminMixin, RoleUserMixin from common.permissions import IsOrgAdminOrAppUser @@ -61,18 +64,13 @@ class ValidateUserApplicationPermissionApi(APIView): application_id = request.query_params.get('application_id', '') system_user_id = request.query_params.get('system_user_id', '') - try: - user_id = uuid.UUID(user_id) - application_id = uuid.UUID(application_id) - system_user_id = uuid.UUID(system_user_id) - except ValueError: - return Response({'msg': False}, status=403) + if not all((user_id, application_id, system_user_id)): + return Response({'has_permission': False, 'expire_at': int(time.time())}) - user = get_object_or_404(User, id=user_id) - application = get_object_or_404(Application, id=application_id) - system_user = get_object_or_404(SystemUser, id=system_user_id) + user = User.objects.get(id=user_id) + application = Application.objects.get(id=application_id) + system_user = SystemUser.objects.get(id=system_user_id) - if has_application_system_permission(user, application, system_user): - return Response({'msg': True}, status=200) - - return Response({'msg': False}, status=403) + has_permission, expire_at = validate_permission(user, application, system_user) + status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN + return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code) diff --git a/apps/perms/api/asset/user_permission/common.py b/apps/perms/api/asset/user_permission/common.py index 88b5422d9..1d09274c8 100644 --- a/apps/perms/api/asset/user_permission/common.py +++ b/apps/perms/api/asset/user_permission/common.py @@ -1,22 +1,23 @@ # -*- coding: utf-8 -*- # import uuid +import time from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from rest_framework.views import APIView, Response +from rest_framework import status from rest_framework.generics import ( ListAPIView, get_object_or_404, RetrieveAPIView, DestroyAPIView ) from orgs.utils import tmp_to_root_org -from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user +from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user, validate_permission from common.permissions import IsOrgAdminOrAppUser, IsOrgAdmin, IsValidUser from common.utils import get_logger, lazyproperty from perms.hands import User, Asset, SystemUser from perms import serializers -from perms.models import Action logger = get_logger(__name__) @@ -65,32 +66,22 @@ class ValidateUserAssetPermissionApi(APIView): def get_cache_policy(self): return 0 - def get_user(self): - user_id = self.request.query_params.get('user_id', '') - user = get_object_or_404(User, id=user_id) - return user - def get(self, request, *args, **kwargs): + user_id = self.request.query_params.get('user_id', '') asset_id = request.query_params.get('asset_id', '') system_id = request.query_params.get('system_user_id', '') action_name = request.query_params.get('action_name', '') - try: - asset_id = uuid.UUID(asset_id) - system_id = uuid.UUID(system_id) - except ValueError: - return Response({'msg': False}, status=403) + if not all((user_id, asset_id, system_id, action_name)): + return Response({'has_permission': False, 'expire_at': int(time.time())}) - asset = get_object_or_404(Asset, id=asset_id, is_active=True) - system_user = get_object_or_404(SystemUser, id=system_id) + user = User.objects.get(id=user_id) + asset = Asset.objects.valid().get(id=asset_id) + system_user = SystemUser.objects.get(id=system_id) - system_users_actions = get_asset_system_user_ids_with_actions_by_user(self.get_user(), asset) - actions = system_users_actions.get(system_user.id) - if actions is None: - return Response({'msg': False}, status=403) - if action_name in Action.value_to_choices(actions): - return Response({'msg': True}, status=200) - return Response({'msg': False}, status=403) + has_permission, expire_at = validate_permission(user, asset, system_user, action_name) + status_code = status.HTTP_200_OK if has_permission else status.HTTP_403_FORBIDDEN + return Response({'has_permission': has_permission, 'expire_at': int(expire_at)}, status=status_code) # TODO 删除 diff --git a/apps/perms/api/asset/user_permission/mixin.py b/apps/perms/api/asset/user_permission/mixin.py index 3b8733b3b..c0f25b8a4 100644 --- a/apps/perms/api/asset/user_permission/mixin.py +++ b/apps/perms/api/asset/user_permission/mixin.py @@ -3,8 +3,9 @@ from rest_framework.request import Request from common.permissions import IsOrgAdminOrAppUser, IsValidUser -from common.utils import lazyproperty from common.http import is_true +from common.mixins.api import RoleAdminMixin as _RoleAdminMixin +from common.mixins.api import RoleUserMixin as _RoleUserMixin from orgs.utils import tmp_to_root_org from users.models import User from perms.utils.asset.user_permission import UserGrantedTreeRefreshController @@ -20,24 +21,13 @@ class PermBaseMixin: return super().get(request, *args, **kwargs) -class RoleAdminMixin(PermBaseMixin): +class RoleAdminMixin(PermBaseMixin, _RoleAdminMixin): permission_classes = (IsOrgAdminOrAppUser,) - kwargs: dict - - @lazyproperty - def user(self): - user_id = self.kwargs.get('pk') - return User.objects.get(id=user_id) -class RoleUserMixin(PermBaseMixin): +class RoleUserMixin(PermBaseMixin, _RoleUserMixin): permission_classes = (IsValidUser,) - request: Request def get(self, request, *args, **kwargs): with tmp_to_root_org(): return super().get(request, *args, **kwargs) - - @lazyproperty - def user(self): - return self.request.user diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index f7551306e..822cc363f 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -72,7 +72,7 @@ class Action: def value_to_choices_display(cls, value): choices = cls.value_to_choices(value) return [str(dict(cls.choices())[i]) for i in choices] - + @classmethod def choices_to_value(cls, value): if not isinstance(value, list): @@ -143,6 +143,26 @@ class AssetPermission(BasePermission): assets = Asset.objects.filter(id__in=asset_ids) return assets + def users_display(self): + names = [user.username for user in self.users.all()] + return names + + def user_groups_display(self): + names = [group.name for group in self.user_groups.all()] + return names + + def assets_display(self): + names = [asset.hostname for asset in self.assets.all()] + return names + + def system_users_display(self): + names = [system_user.name for system_user in self.system_users.all()] + return names + + def nodes_display(self): + names = [node.full_value for node in self.nodes.all()] + return names + class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, models.JMSBaseModel): class NodeFrom(ChoiceSet): diff --git a/apps/perms/serializers/application/permission.py b/apps/perms/serializers/application/permission.py index 7be6ec9ca..ce820b858 100644 --- a/apps/perms/serializers/application/permission.py +++ b/apps/perms/serializers/application/permission.py @@ -20,16 +20,16 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer): class Meta: model = ApplicationPermission - mini_fields = ['id', 'name'] - small_fields = mini_fields + [ + fields_mini = ['id', 'name'] + 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' ] - m2m_fields = [ + fields_m2m = [ 'users', 'user_groups', 'applications', 'system_users', 'users_amount', 'user_groups_amount', 'applications_amount', 'system_users_amount', ] - fields = small_fields + m2m_fields + fields = fields_small + fields_m2m read_only_fields = ['created_by', 'date_created'] @classmethod diff --git a/apps/perms/serializers/asset/permission.py b/apps/perms/serializers/asset/permission.py index c676989df..dcaffb82f 100644 --- a/apps/perms/serializers/asset/permission.py +++ b/apps/perms/serializers/asset/permission.py @@ -3,7 +3,9 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from django.db.models import Prefetch + +from django.db.models import Prefetch, Q + from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import AssetPermission, Action @@ -41,21 +43,27 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): actions = ActionsField(required=False, allow_null=True) is_valid = serializers.BooleanField(read_only=True) is_expired = serializers.BooleanField(read_only=True, label=_('Is expired')) + users_display = serializers.ListField(child=serializers.CharField(), label=_('Users name'), required=False) + user_groups_display = serializers.ListField(child=serializers.CharField(), label=_('User groups name'), required=False) + assets_display = serializers.ListField(child=serializers.CharField(), label=_('Assets name'), required=False) + nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False) + system_users_display = serializers.ListField(child=serializers.CharField(), label=_('System users name'), required=False) class Meta: model = AssetPermission - mini_fields = ['id', 'name'] - small_fields = mini_fields + [ + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ 'is_active', 'is_expired', 'is_valid', 'actions', 'created_by', 'date_created', 'date_expired', 'date_start', 'comment' ] - m2m_fields = [ - 'users', 'user_groups', 'assets', 'nodes', 'system_users', + fields_m2m = [ + 'users', 'users_display', 'user_groups', 'user_groups_display', 'assets', 'assets_display', + 'nodes', 'nodes_display', 'system_users', 'system_users_display', 'users_amount', 'user_groups_amount', 'assets_amount', 'nodes_amount', 'system_users_amount', ] - fields = small_fields + m2m_fields + fields = fields_small + fields_m2m read_only_fields = ['created_by', 'date_created'] extra_kwargs = { 'is_expired': {'label': _('Is expired')}, @@ -71,11 +79,44 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related( - Prefetch('system_users', queryset=SystemUser.objects.only('id')), - Prefetch('user_groups', queryset=UserGroup.objects.only('id')), - Prefetch('users', queryset=User.objects.only('id')), - Prefetch('assets', queryset=Asset.objects.only('id')), - Prefetch('nodes', queryset=Node.objects.only('id')) - ) + queryset = queryset.prefetch_related('users', 'user_groups', 'assets', 'nodes', 'system_users') return queryset + + def to_internal_value(self, data): + # 系统用户是必填项 + system_users_display = data.pop('system_users_display', '') + for i in range(len(system_users_display)): + system_user = SystemUser.objects.filter(name=system_users_display[i]).first() + if system_user and system_user.id not in data['system_users']: + data['system_users'].append(system_user.id) + return super().to_internal_value(data) + + def perform_display_create(self, instance, **kwargs): + # 用户 + users_to_set = User.objects.filter( + Q(name__in=kwargs.get('users_display')) | Q(username__in=kwargs.get('users_display')) + ).distinct() + instance.users.add(*users_to_set) + # 用户组 + user_groups_to_set = UserGroup.objects.filter(name__in=kwargs.get('user_groups_display')).distinct() + instance.user_groups.add(*user_groups_to_set) + # 资产 + assets_to_set = Asset.objects.filter( + Q(ip__in=kwargs.get('assets_display')) | Q(hostname__in=kwargs.get('assets_display')) + ).distinct() + instance.assets.add(*assets_to_set) + # 节点 + nodes_to_set = Node.objects.filter(full_value__in=kwargs.get('nodes_display')).distinct() + instance.nodes.add(*nodes_to_set) + + def create(self, validated_data): + display = { + 'users_display' : validated_data.pop('users_display', ''), + 'user_groups_display' : validated_data.pop('user_groups_display', ''), + 'assets_display' : validated_data.pop('assets_display', ''), + 'nodes_display' : validated_data.pop('nodes_display', '') + } + instance = super().create(validated_data) + self.perform_display_create(instance, **display) + return instance + diff --git a/apps/perms/utils/application/permission.py b/apps/perms/utils/application/permission.py index 4e4c8113d..c4ebb5bdb 100644 --- a/apps/perms/utils/application/permission.py +++ b/apps/perms/utils/application/permission.py @@ -1,3 +1,5 @@ +import time + from django.db.models import Q from common.utils import get_logger @@ -6,6 +8,58 @@ from perms.models import ApplicationPermission logger = get_logger(__file__) +def get_user_all_app_perm_ids(user) -> set: + app_perm_ids = set() + user_perm_id = ApplicationPermission.users.through.objects \ + .filter(user_id=user.id) \ + .values_list('applicationpermission_id', flat=True) \ + .distinct() + app_perm_ids.update(user_perm_id) + + group_ids = user.groups.through.objects \ + .filter(user_id=user.id) \ + .values_list('usergroup_id', flat=True) \ + .distinct() + group_ids = list(group_ids) + groups_perm_id = ApplicationPermission.user_groups.through.objects \ + .filter(usergroup_id__in=group_ids) \ + .values_list('applicationpermission_id', flat=True) \ + .distinct() + app_perm_ids.update(groups_perm_id) + + app_perm_ids = ApplicationPermission.objects.filter( + id__in=app_perm_ids).valid().values_list('id', flat=True) + app_perm_ids = set(app_perm_ids) + return app_perm_ids + + +def validate_permission(user, application, system_user): + app_perm_ids = get_user_all_app_perm_ids(user) + app_perm_ids = ApplicationPermission.applications.through.objects.filter( + applicationpermission_id__in=app_perm_ids, + application_id=application.id + ).values_list('applicationpermission_id', flat=True) + + app_perm_ids = set(app_perm_ids) + + app_perm_ids = ApplicationPermission.system_users.through.objects.filter( + applicationpermission_id__in=app_perm_ids, + systemuser_id=system_user.id + ).values_list('applicationpermission_id', flat=True) + + app_perm_ids = set(app_perm_ids) + + app_perm = ApplicationPermission.objects.filter( + id__in=app_perm_ids + ).order_by('-date_expired').first() + + app_perm: ApplicationPermission + if app_perm: + return True, app_perm.date_expired.timestamp() + else: + return False, time.time() + + def get_application_system_user_ids(user, application): queryset = ApplicationPermission.objects.valid()\ .filter( diff --git a/apps/perms/utils/asset/permission.py b/apps/perms/utils/asset/permission.py index 197abc9d9..949ee6873 100644 --- a/apps/perms/utils/asset/permission.py +++ b/apps/perms/utils/asset/permission.py @@ -1,15 +1,61 @@ +import time from collections import defaultdict from django.db.models import Q from common.utils import get_logger -from perms.models import AssetPermission -from perms.hands import Asset, User, UserGroup, SystemUser +from perms.models import AssetPermission, Action +from perms.hands import Asset, User, UserGroup, SystemUser, Node from perms.utils.asset.user_permission import get_user_all_asset_perm_ids logger = get_logger(__file__) +def validate_permission(user, asset, system_user, action_name): + + if not system_user.protocol in asset.protocols_as_dict.keys(): + return False, time.time() + + asset_perm_ids = get_user_all_asset_perm_ids(user) + + asset_perm_ids_from_asset = AssetPermission.assets.through.objects.filter( + assetpermission_id__in=asset_perm_ids, + asset_id=asset.id + ).values_list('assetpermission_id', flat=True) + + nodes = asset.get_nodes() + node_keys = set() + for node in nodes: + ancestor_keys = node.get_ancestor_keys(with_self=True) + node_keys.update(ancestor_keys) + node_ids = Node.objects.filter(key__in=node_keys).values_list('id', flat=True) + + node_ids = set(node_ids) + + asset_perm_ids_from_node = AssetPermission.nodes.through.objects.filter( + assetpermission_id__in=asset_perm_ids, + node_id__in=node_ids + ).values_list('assetpermission_id', flat=True) + + asset_perm_ids = {*asset_perm_ids_from_asset, *asset_perm_ids_from_node} + + asset_perm_ids = AssetPermission.system_users.through.objects.filter( + assetpermission_id__in=asset_perm_ids, + systemuser_id=system_user.id + ).values_list('assetpermission_id', flat=True) + + asset_perm_ids = set(asset_perm_ids) + + asset_perms = AssetPermission.objects.filter( + id__in=asset_perm_ids + ).order_by('-date_expired') + + for asset_perm in asset_perms: + if action_name in Action.value_to_choices(asset_perm.actions): + return True, asset_perm.date_expired.timestamp() + return False, time.time() + + def get_asset_system_user_ids_with_actions(asset_perm_ids, asset: Asset): nodes = asset.get_nodes() node_keys = set() diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py index 151617be5..39e009ed5 100644 --- a/apps/settings/api/__init__.py +++ b/apps/settings/api/__init__.py @@ -1,2 +1,4 @@ from .common import * from .ldap import * +from .wecom import * +from .dingtalk import * diff --git a/apps/settings/api/common.py b/apps/settings/api/common.py index 8aafee063..bb3107dfd 100644 --- a/apps/settings/api/common.py +++ b/apps/settings/api/common.py @@ -112,6 +112,7 @@ class PublicSettingApi(generics.RetrieveAPIView): "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, "XPACK_LICENSE_IS_VALID": has_valid_xpack_license(), @@ -124,7 +125,9 @@ class PublicSettingApi(generics.RetrieveAPIView): '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, } } return instance @@ -140,6 +143,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView): 'ldap': serializers.LDAPSettingSerializer, 'email': serializers.EmailSettingSerializer, 'email_content': serializers.EmailContentSettingSerializer, + 'wecom': serializers.WeComSettingSerializer, + 'dingtalk': serializers.DingTalkSettingSerializer, } def get_serializer_class(self): @@ -162,6 +167,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView): 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 diff --git a/apps/settings/api/dingtalk.py b/apps/settings/api/dingtalk.py new file mode 100644 index 000000000..e560f8626 --- /dev/null +++ b/apps/settings/api/dingtalk.py @@ -0,0 +1,38 @@ +import requests + +from rest_framework.views import Response +from rest_framework.generics import GenericAPIView +from django.utils.translation import gettext_lazy as _ + +from common.permissions import IsSuperUser +from common.message.backends.dingtalk import URL + +from .. import serializers + + +class DingTalkTestingAPI(GenericAPIView): + permission_classes = (IsSuperUser,) + serializer_class = serializers.DingTalkSettingSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + dingtalk_appkey = serializer.validated_data['DINGTALK_APPKEY'] + dingtalk_agentid = serializer.validated_data['DINGTALK_AGENTID'] + dingtalk_appsecret = serializer.validated_data['DINGTALK_APPSECRET'] + + try: + params = {'appkey': dingtalk_appkey, 'appsecret': dingtalk_appsecret} + resp = requests.get(url=URL.GET_TOKEN, params=params) + if resp.status_code != 200: + return Response(status=400, data={'error': resp.json()}) + + data = resp.json() + errcode = data['errcode'] + if errcode != 0: + return Response(status=400, data={'error': data['errmsg']}) + + return Response(status=200, data={'msg': _('OK')}) + except Exception as e: + return Response(status=400, data={'error': str(e)}) diff --git a/apps/settings/api/wecom.py b/apps/settings/api/wecom.py new file mode 100644 index 000000000..3087efd1f --- /dev/null +++ b/apps/settings/api/wecom.py @@ -0,0 +1,38 @@ +import requests + +from rest_framework.views import Response +from rest_framework.generics import GenericAPIView +from django.utils.translation import gettext_lazy as _ + +from common.permissions import IsSuperUser +from common.message.backends.wecom import URL + +from .. import serializers + + +class WeComTestingAPI(GenericAPIView): + permission_classes = (IsSuperUser,) + serializer_class = serializers.WeComSettingSerializer + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + wecom_corpid = serializer.validated_data['WECOM_CORPID'] + wecom_agentid = serializer.validated_data['WECOM_AGENTID'] + wecom_corpsecret = serializer.validated_data['WECOM_SECRET'] + + try: + params = {'corpid': wecom_corpid, 'corpsecret': wecom_corpsecret} + resp = requests.get(url=URL.GET_TOKEN, params=params) + if resp.status_code != 200: + return Response(status=400, data={'error': resp.json()}) + + data = resp.json() + errcode = data['errcode'] + if errcode != 0: + return Response(status=400, data={'error': data['errmsg']}) + + return Response(status=200, data={'msg': _('OK')}) + except Exception as e: + return Response(status=400, data={'error': str(e)}) diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index c6a3c32ac..3aca30c10 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -6,7 +6,7 @@ from rest_framework import serializers __all__ = [ 'BasicSettingSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer', 'LDAPSettingSerializer', 'TerminalSettingSerializer', 'SecuritySettingSerializer', - 'SettingsSerializer' + 'SettingsSerializer', 'WeComSettingSerializer', 'DingTalkSettingSerializer', ] @@ -121,7 +121,11 @@ class TerminalSettingSerializer(serializers.Serializer): ('50', '50'), ) TERMINAL_PASSWORD_AUTH = serializers.BooleanField(required=False, label=_('Password auth')) - TERMINAL_PUBLIC_KEY_AUTH = serializers.BooleanField(required=False, label=_('Public key 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( @@ -163,6 +167,11 @@ class SecuritySettingSerializer(serializers.Serializer): 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 (the value of n here is the value filled in the input box)') + ) SECURITY_PASSWORD_MIN_LENGTH = serializers.IntegerField( min_value=6, max_value=30, required=True, label=_('Password minimum length') @@ -180,13 +189,29 @@ class SecuritySettingSerializer(serializers.Serializer): ) +class WeComSettingSerializer(serializers.Serializer): + WECOM_CORPID = serializers.CharField(max_length=256, required=True, label=_('Corporation ID(corpid)')) + WECOM_AGENTID = serializers.CharField(max_length=256, required=True, label=_("Agent ID(agentid)")) + WECOM_SECRET = serializers.CharField(max_length=256, required=True, label=_("Secret(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 SettingsSerializer( BasicSettingSerializer, EmailSettingSerializer, EmailContentSettingSerializer, LDAPSettingSerializer, TerminalSettingSerializer, - SecuritySettingSerializer + SecuritySettingSerializer, + WeComSettingSerializer, + DingTalkSettingSerializer, ): # encrypt_fields 现在使用 write_only 来判断了 diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index 0db9c7c54..86dfc6847 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -13,6 +13,8 @@ urlpatterns = [ path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'), path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'), path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'), + path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'), + path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'), path('setting/', api.SettingsApi.as_view(), name='settings-setting'), path('public/', api.PublicSettingApi.as_view(), name='public-setting'), diff --git a/apps/static/img/login_dingtalk_log.png b/apps/static/img/login_dingtalk_log.png new file mode 100644 index 000000000..61ef4e4d4 Binary files /dev/null and b/apps/static/img/login_dingtalk_log.png differ diff --git a/apps/static/img/login_wecom_log.png b/apps/static/img/login_wecom_log.png new file mode 100644 index 000000000..d5a58d0ba Binary files /dev/null and b/apps/static/img/login_wecom_log.png differ diff --git a/apps/templates/flash_message_standalone.html b/apps/templates/flash_message_standalone.html index 84ef58cb5..5265f14f9 100644 --- a/apps/templates/flash_message_standalone.html +++ b/apps/templates/flash_message_standalone.html @@ -4,14 +4,6 @@ {% block html_title %} {{ title }} {% endblock %} {% block title %} {{ title }}{% endblock %} -{% block custom_head_css_js %} - -{% endblock %} - {% block content %}
{% if errors %} @@ -30,8 +22,21 @@

{% endif %} diff --git a/apps/terminal/api/command.py b/apps/terminal/api/command.py index 814b2ecae..497e40fbe 100644 --- a/apps/terminal/api/command.py +++ b/apps/terminal/api/command.py @@ -17,6 +17,7 @@ from terminal.filters import CommandFilter from orgs.utils import current_org from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser from common.const.http import GET +from common.drf.api import JMSBulkModelViewSet from common.utils import get_logger from terminal.utils import send_command_alert_mail from terminal.serializers import InsecureCommandAlertSerializer @@ -94,7 +95,7 @@ class CommandQueryMixin: return date_from_st, date_to_st -class CommandViewSet(viewsets.ModelViewSet): +class CommandViewSet(JMSBulkModelViewSet): """接受app发送来的command log, 格式如下 { "user": "admin", diff --git a/apps/terminal/apps.py b/apps/terminal/apps.py index 5341369c7..f0cb05bf2 100644 --- a/apps/terminal/apps.py +++ b/apps/terminal/apps.py @@ -1,10 +1,12 @@ from __future__ import unicode_literals +from django.utils.translation import gettext_lazy as _ from django.apps import AppConfig class TerminalConfig(AppConfig): name = 'terminal' + verbose_name = _('Terminal') def ready(self): from . import signals_handler diff --git a/apps/terminal/backends/command/es.py b/apps/terminal/backends/command/es.py index d8197391d..27631d5bc 100644 --- a/apps/terminal/backends/command/es.py +++ b/apps/terminal/backends/command/es.py @@ -7,6 +7,7 @@ import pytz from uuid import UUID import inspect +from django.utils.translation import gettext_lazy as _ from django.db.models import QuerySet as DJQuerySet from elasticsearch import Elasticsearch from elasticsearch.helpers import bulk @@ -14,12 +15,18 @@ from elasticsearch.exceptions import RequestError from common.utils.common import lazyproperty from common.utils import get_logger +from common.exceptions import JMSException from .models import AbstractSessionCommand logger = get_logger(__file__) +class InvalidElasticsearch(JMSException): + default_code = 'invalid_elasticsearch' + default_detail = _('Invalid elasticsearch config') + + class CommandStore(): def __init__(self, config): hosts = config.get("HOSTS") @@ -27,12 +34,14 @@ class CommandStore(): self.index = config.get("INDEX") or 'jumpserver' self.doc_type = config.get("DOC_TYPE") or 'command_store' - ignore_verify_certs = kwargs.pop('ignore_verify_certs', False) + ignore_verify_certs = kwargs.pop('IGNORE_VERIFY_CERTS', False) if ignore_verify_certs: kwargs['verify_certs'] = None self.es = Elasticsearch(hosts=hosts, max_retries=0, **kwargs) def pre_use_check(self): + if not self.ping(timeout=3): + raise InvalidElasticsearch self._ensure_index_exists() def _ensure_index_exists(self): diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session.py index 89e338143..6d85759af 100644 --- a/apps/terminal/models/session.py +++ b/apps/terminal/models/session.py @@ -11,6 +11,7 @@ from django.core.files.storage import default_storage from django.core.cache import cache from assets.models import Asset +from users.models import User from orgs.mixins.models import OrgModelMixin from common.db.models import ChoiceSet from ..backends import get_multi_command_storage @@ -79,6 +80,10 @@ class Session(OrgModelMixin): def asset_obj(self): return Asset.objects.get(id=self.asset_id) + @property + def user_obj(self): + return User.objects.get(id=self.user_id) + @property def _date_start_first_has_replay_rdp_session(self): if self.__class__._DATE_START_FIRST_HAS_REPLAY_RDP_SESSION is None: diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index d66c3cfa3..854bfd240 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -17,14 +17,17 @@ class SessionSerializer(BulkOrgResourceModelSerializer): class Meta: model = Session list_serializer_class = AdaptedBulkListSerializer - fields = [ - "id", "user", "asset", "system_user", + fields_mini = ["id"] + fields_small = fields_mini + [ + "user", "asset", "system_user", "user_id", "asset_id", "system_user_id", - "login_from", "login_from_display", "remote_addr", - "is_success", "is_finished", "has_replay", "can_replay", - "can_join", "can_terminate", "protocol", "date_start", "date_end", - "terminal", + "login_from", "login_from_display", "remote_addr", "protocol", + "is_success", "is_finished", "has_replay", + "date_start", "date_end", ] + fields_fk = ["terminal",] + fields_custom = ["can_replay", "can_join", "can_terminate",] + fields = fields_small + fields_fk + fields_custom extra_kwargs = { "protocol": {'label': _('Protocol')}, 'user_id': {'label': _('User ID')}, diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index 7f5dec2fe..ff794eef9 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -181,9 +181,9 @@ class CommandStorageTypeESSerializer(serializers.Serializer): max_length=1024, default='jumpserver', label=_('Index'), allow_null=True ) DOC_TYPE = ReadableHiddenField(default='command', label=_('Doc type'), allow_null=True) - ignore_verify_certs = serializers.BooleanField( + IGNORE_VERIFY_CERTS = serializers.BooleanField( default=False, label=_('Ignore Certificate Verification'), - source='OTHER.ignore_verify_certs', allow_null=True, + source='OTHER.IGNORE_VERIFY_CERTS', allow_null=True, ) # mapping diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index 25b72ab2c..e4640e454 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -17,12 +17,15 @@ class StatusSerializer(serializers.ModelSerializer): ) class Meta: - fields = [ - 'id', + fields_mini = ['id'] + fields_write_only = ['sessions',] + fields_small = fields_mini + fields_write_only + [ 'cpu_load', 'memory_used', 'disk_used', - 'session_online', 'sessions', - 'terminal', 'date_created', + 'session_online', + 'date_created' ] + fields_fk = ['terminal'] + fields = fields_small + fields_fk extra_kwargs = { "cpu_load": {'default': 0}, "memory_used": {'default': 0}, @@ -40,12 +43,16 @@ class TerminalSerializer(BulkModelSerializer): class Meta: model = Terminal - fields = [ - 'id', 'name', 'type', 'remote_addr', 'http_port', 'ssh_port', - 'comment', 'is_accepted', "is_active", 'session_online', - 'is_alive', 'date_created', 'command_storage', 'replay_storage', - 'status', 'status_display', 'stat' + fields_mini = ['id', 'name'] + fields_small = fields_mini + [ + 'type', 'remote_addr', 'http_port', 'ssh_port', + 'session_online', 'command_storage', 'replay_storage', + 'is_accepted', "is_active", 'is_alive', + 'date_created', + 'comment', ] + fields_fk = ['status', 'status_display', 'stat'] + fields = fields_small + fields_fk read_only_fields = ['type', 'date_created'] @staticmethod diff --git a/apps/tickets/api/__init__.py b/apps/tickets/api/__init__.py index 6b519ef80..a6b5e39c6 100644 --- a/apps/tickets/api/__init__.py +++ b/apps/tickets/api/__init__.py @@ -3,3 +3,4 @@ from .ticket import * from .assignee import * from .comment import * +from .common import * diff --git a/apps/tickets/api/common.py b/apps/tickets/api/common.py new file mode 100644 index 000000000..fe5a5d1e9 --- /dev/null +++ b/apps/tickets/api/common.py @@ -0,0 +1,44 @@ +from django.shortcuts import get_object_or_404 +from rest_framework.response import Response +from rest_framework.generics import RetrieveDestroyAPIView + +from common.permissions import IsAppUser +from common.utils import lazyproperty +from orgs.utils import tmp_to_root_org +from ..models import Ticket + + +__all__ = ['GenericTicketStatusRetrieveCloseAPI'] + + +class GenericTicketStatusRetrieveCloseAPI(RetrieveDestroyAPIView): + permission_classes = (IsAppUser, ) + + def retrieve(self, request, *args, **kwargs): + if self.ticket.action_open: + status = 'await' + elif self.ticket.action_approve: + status = 'approve' + else: + status = 'reject' + data = { + 'status': status, + 'action': self.ticket.action, + 'processor': self.ticket.processor_display + } + return Response(data=data, status=200) + + def destroy(self, request, *args, **kwargs): + if self.ticket.status_open: + self.ticket.close(processor=self.ticket.applicant) + data = { + 'action': self.ticket.action, + 'status': self.ticket.status, + 'processor': self.ticket.processor_display + } + return Response(data=data, status=200) + + @lazyproperty + def ticket(self): + with tmp_to_root_org(): + return get_object_or_404(Ticket, pk=self.kwargs['pk']) diff --git a/apps/tickets/const.py b/apps/tickets/const.py index 591ead607..3397353d4 100644 --- a/apps/tickets/const.py +++ b/apps/tickets/const.py @@ -10,6 +10,7 @@ class TicketTypeChoices(TextChoices): apply_asset = 'apply_asset', _('Apply for asset') apply_application = 'apply_application', _('Apply for application') login_asset_confirm = 'login_asset_confirm', _('Login asset confirm') + command_confirm = 'command_confirm', _('Command confirm') class TicketActionChoices(TextChoices): diff --git a/apps/tickets/handler/command_confirm.py b/apps/tickets/handler/command_confirm.py new file mode 100644 index 000000000..2d66db2d8 --- /dev/null +++ b/apps/tickets/handler/command_confirm.py @@ -0,0 +1,32 @@ +from django.utils.translation import ugettext as _ +from .base import BaseHandler + + +class Handler(BaseHandler): + + # body + def _construct_meta_body_of_open(self): + apply_run_user = self.ticket.meta.get('apply_run_user') + apply_run_asset = self.ticket.meta.get('apply_run_asset') + apply_run_system_user = self.ticket.meta.get('apply_run_system_user') + apply_run_command = self.ticket.meta.get('apply_run_command') + apply_from_session_id = self.ticket.meta.get('apply_from_session_id') + apply_from_cmd_filter_rule_id = self.ticket.meta.get('apply_from_cmd_filter_rule_id') + apply_from_cmd_filter_id = self.ticket.meta.get('apply_from_cmd_filter_id') + + applied_body = '''{}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {}, + {}: {}, + '''.format( + _("Applied run user"), apply_run_user, + _("Applied run asset"), apply_run_asset, + _("Applied run system user"), apply_run_system_user, + _("Applied run command"), apply_run_command, + _("Applied from session"), apply_from_session_id, + _("Applied from command filter rules"), apply_from_cmd_filter_rule_id, + _("Applied from command filter"), apply_from_cmd_filter_id, + ) + return applied_body diff --git a/apps/tickets/migrations/0009_auto_20210426_1720.py b/apps/tickets/migrations/0009_auto_20210426_1720.py new file mode 100644 index 000000000..e584c2560 --- /dev/null +++ b/apps/tickets/migrations/0009_auto_20210426_1720.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2021-04-26 09:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0008_auto_20210311_1113'), + ] + + operations = [ + migrations.AlterField( + model_name='ticket', + name='type', + field=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'), + ), + ] diff --git a/apps/tickets/serializers/comment.py b/apps/tickets/serializers/comment.py index 4eaf49cf7..5bf57180e 100644 --- a/apps/tickets/serializers/comment.py +++ b/apps/tickets/serializers/comment.py @@ -21,9 +21,13 @@ class CommentSerializer(serializers.ModelSerializer): class Meta: model = Comment - fields = [ - 'id', 'ticket', 'body', 'user', 'user_display', 'date_created', 'date_updated' + fields_mini = ['id'] + fields_small = fields_mini + [ + 'body', 'user_display', + 'date_created', 'date_updated' ] + 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 ee8a402dd..12b576857 100644 --- a/apps/tickets/serializers/ticket/meta/meta.py +++ b/apps/tickets/serializers/ticket/meta/meta.py @@ -1,5 +1,7 @@ from tickets import const -from .ticket_type import apply_asset, apply_application, login_confirm, login_asset_confirm +from .ticket_type import ( + apply_asset, apply_application, login_confirm, login_asset_confirm, command_confirm +) __all__ = [ 'type_serializer_classes_mapping', @@ -35,5 +37,10 @@ type_serializer_classes_mapping = { '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: { + '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/command_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py new file mode 100644 index 000000000..eb631fe98 --- /dev/null +++ b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py @@ -0,0 +1,26 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + + +__all__ = [ + 'ApplySerializer', 'CommandConfirmSerializer', +] + + +class ApplySerializer(serializers.Serializer): + # 申请信息 + apply_run_user = serializers.CharField(required=True, label=_('Run user')) + apply_run_asset = serializers.CharField(required=True, label=_('Run asset')) + apply_run_system_user = serializers.CharField( + required=True, max_length=64, label=_('Run system user') + ) + apply_run_command = serializers.CharField(required=True, label=_('Run command')) + apply_from_session_id = serializers.UUIDField(required=False, label=_('From session')) + apply_from_cmd_filter_rule_id = serializers.UUIDField( + required=False, label=_('From cmd filter rule') + ) + apply_from_cmd_filter_id = serializers.UUIDField(required=False, label=_('From cmd filter')) + + +class CommandConfirmSerializer(ApplySerializer): + pass diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index fd776caea..fdf281b73 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -27,15 +27,17 @@ class TicketSerializer(OrgResourceModelSerializerMixin): class Meta: model = Ticket - fields = [ - 'id', 'title', 'type', 'type_display', - 'meta', 'action', 'action_display', 'status', 'status_display', - 'applicant', 'applicant_display', 'processor', 'processor_display', - 'assignees', 'assignees_display', 'comment', + 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', - 'org_id', 'org_name', - 'body' + 'comment', 'org_id', 'org_name', ] + fields_fk = ['applicant', 'processor',] + fields_m2m = ['assignees'] + fields = fields_small + fields_fk + fields_m2m def get_meta_serializer(self): default_serializer = serializers.Serializer(read_only=True) diff --git a/apps/users/migrations/0032_userpasswordhistory.py b/apps/users/migrations/0032_userpasswordhistory.py new file mode 100644 index 000000000..8fcf1d2d1 --- /dev/null +++ b/apps/users/migrations/0032_userpasswordhistory.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1 on 2021-04-27 12:43 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0031_auto_20201118_1801'), + ] + + operations = [ + migrations.CreateModel( + name='UserPasswordHistory', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('password', models.CharField(max_length=128)), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history_passwords', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + ), + ] diff --git a/apps/users/migrations/0033_user_need_update_password.py b/apps/users/migrations/0033_user_need_update_password.py new file mode 100644 index 000000000..8cc1740a9 --- /dev/null +++ b/apps/users/migrations/0033_user_need_update_password.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2021-04-28 11:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0032_userpasswordhistory'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='need_update_password', + field=models.BooleanField(default=False), + ), + ] diff --git a/apps/users/migrations/0034_auto_20210506_1448.py b/apps/users/migrations/0034_auto_20210506_1448.py new file mode 100644 index 000000000..df6257064 --- /dev/null +++ b/apps/users/migrations/0034_auto_20210506_1448.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1 on 2021-05-06 06:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0033_user_need_update_password'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='dingtalk_id', + field=models.CharField(default=None, max_length=128, null=True, unique=True), + ), + migrations.AddField( + model_name='user', + name='wecom_id', + field=models.CharField(default=None, max_length=128, null=True, unique=True), + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index fab14e252..715f02b9d 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -5,9 +5,13 @@ import uuid import base64 import string import random +import datetime + +from functools import partial from django.conf import settings from django.contrib.auth.models import AbstractUser +from django.contrib.auth.hashers import check_password, make_password from django.core.cache import cache from django.db import models from django.db.models import TextChoices @@ -32,6 +36,9 @@ logger = get_logger(__file__) class AuthMixin: + date_password_last_updated: datetime.datetime + is_local: bool + @property def password_raw(self): raise AttributeError('Password raw is not a readable attribute') @@ -62,8 +69,25 @@ class AuthMixin: def can_update_ssh_key(self): return self.can_use_ssh_key_login() - def can_use_ssh_key_login(self): - return self.is_local and settings.TERMINAL_PUBLIC_KEY_AUTH + @staticmethod + def can_use_ssh_key_login(): + return settings.TERMINAL_PUBLIC_KEY_AUTH + + def is_history_password(self, password): + allow_history_password_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT + history_passwords = self.history_passwords.all().order_by('-date_created')[:int(allow_history_password_count)] + + for history_password in history_passwords: + if check_password(password, history_password.password): + return True + else: + return False + + def save_history_password(self, password): + UserPasswordHistory.objects.create( + user=self, password=make_password(password), + date_created=self.date_password_last_updated + ) def is_public_key_valid(self): """ @@ -101,6 +125,7 @@ class AuthMixin: def reset_password(self, new_password): self.set_password(new_password) + self.need_update_password = False self.save() @property @@ -516,7 +541,10 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): cas = 'cas', 'CAS' SOURCE_BACKEND_MAPPING = { - Source.local: [settings.AUTH_BACKEND_MODEL, settings.AUTH_BACKEND_PUBKEY], + Source.local: [ + settings.AUTH_BACKEND_MODEL, settings.AUTH_BACKEND_PUBKEY, + settings.AUTH_BACKEND_WECOM, settings.AUTH_BACKEND_DINGTALK, + ], Source.ldap: [settings.AUTH_BACKEND_LDAP], Source.openid: [settings.AUTH_BACKEND_OIDC_PASSWORD, settings.AUTH_BACKEND_OIDC_CODE], Source.radius: [settings.AUTH_BACKEND_RADIUS], @@ -579,10 +607,21 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): auto_now_add=True, blank=True, null=True, verbose_name=_('Date password last updated') ) + need_update_password = models.BooleanField(default=False) + wecom_id = models.CharField(null=True, default=None, unique=True, max_length=128) + dingtalk_id = models.CharField(null=True, default=None, unique=True, max_length=128) def __str__(self): return '{0.name}({0.username})'.format(self) + @property + def is_wecom_bound(self): + return bool(self.wecom_id) + + @property + def is_dingtalk_bound(self): + return bool(self.dingtalk_id) + def get_absolute_url(self): return reverse('users:user-detail', args=(self.id,)) @@ -724,3 +763,11 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): if self.email and self.source == self.Source.local.value: return True return False + + +class UserPasswordHistory(models.Model): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + password = models.CharField(max_length=128) + user = models.ForeignKey("users.User", related_name='history_passwords', + on_delete=models.CASCADE, verbose_name=_('User')) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) diff --git a/apps/users/permissions.py b/apps/users/permissions.py new file mode 100644 index 000000000..03534d211 --- /dev/null +++ b/apps/users/permissions.py @@ -0,0 +1,10 @@ +from rest_framework import permissions + +from .utils import is_auth_password_time_valid + + +class IsAuthPasswdTimeValid(permissions.IsAuthenticated): + + def has_permission(self, request, view): + return super().has_permission(request, view) \ + and is_auth_password_time_valid(request.session) diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py index 1c5e99873..261dd6f01 100644 --- a/apps/users/serializers/profile.py +++ b/apps/users/serializers/profile.py @@ -30,12 +30,17 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer): raise serializers.ValidationError(msg) return value - @staticmethod - def validate_new_password(value): + def validate_new_password(self, value): from ..utils import check_password_rules if not check_password_rules(value): msg = _('Password does not match security rules') raise serializers.ValidationError(msg) + if self.instance.is_history_password(value): + limit_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT + msg = _('The new password cannot be the last {} passwords').format(limit_count) + raise serializers.ValidationError(msg) + else: + self.instance.save_history_password(value) return value def validate_new_password_again(self, value): @@ -101,7 +106,8 @@ class UserProfileSerializer(UserSerializer): class Meta(UserSerializer.Meta): fields = UserSerializer.Meta.fields + [ - 'public_key_comment', 'public_key_hash_md5', 'admin_or_audit_orgs', 'current_org_roles', + 'public_key_comment', 'public_key_hash_md5', + 'admin_or_audit_orgs', 'current_org_roles', 'guide_url', 'user_all_orgs' ] read_only_fields = [ @@ -164,6 +170,7 @@ class ChangeUserPasswordSerializer(serializers.ModelSerializer): model = User fields = ['password'] + class ResetOTPSerializer(serializers.Serializer): msg = serializers.CharField(read_only=True) diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 0d8e784ed..d7591360b 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -34,6 +34,7 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): is_expired = serializers.BooleanField(read_only=True, label=_('Is expired')) can_update = serializers.SerializerMethodField(label=_('Can update')) can_delete = serializers.SerializerMethodField(label=_('Can delete')) + can_public_key_auth = serializers.ReadOnlyField(source='can_use_ssh_key_login') org_roles = serializers.ListField( label=_('Organization role name'), allow_null=True, required=False, child=serializers.ChoiceField(choices=ORG_ROLE.choices), default=["User"] @@ -43,24 +44,36 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): model = User # mini 是指能识别对象的最小单元 fields_mini = ['id', 'name', 'username'] + # 只能写的字段, 这个虽然无法在框架上生效,但是更多对我们是提醒 + fields_write_only = [ + 'password', 'public_key', + ] # small 指的是 不需要计算的直接能从一张表中获取到的数据 - fields_small = fields_mini + [ - 'password', 'email', 'public_key', 'wechat', 'phone', 'mfa_level', 'mfa_enabled', - 'mfa_level_display', 'mfa_force_enabled', 'role_display', 'org_role_display', - 'total_role_display', 'comment', 'source', 'is_valid', 'is_expired', - 'is_active', 'created_by', 'is_first_login', - 'password_strategy', 'date_password_last_updated', 'date_expired', - 'avatar_url', 'source_display', 'date_joined', 'last_login' + fields_small = fields_mini + fields_write_only + [ + 'email', 'wechat', 'phone', 'mfa_level', + 'source', 'source_display', 'can_public_key_auth', 'need_update_password', + 'mfa_enabled', 'is_valid', 'is_expired', 'is_active', # 布尔字段 + 'date_expired', 'date_joined', 'last_login', # 日期字段 + 'created_by', 'comment', # 通用字段 + 'is_wecom_bound', 'is_dingtalk_bound', ] - fields = fields_small + [ - 'groups', 'role', 'groups_display', 'role_display', - 'can_update', 'can_delete', 'login_blocked', 'org_roles' + # 包含不太常用的字段,可以没有 + fields_verbose = fields_small + [ + 'total_role_display', 'org_role_display', + 'mfa_level_display', 'mfa_force_enabled', 'is_first_login', + 'date_password_last_updated', 'avatar_url', ] + # 外键的字段 + fields_fk = ['role', 'role_display'] + # 多对多字段 + fields_m2m = ['groups', 'groups_display', 'org_roles'] + # 在serializer 上定义的字段 + fields_custom = ['can_update', 'can_delete', 'login_blocked', 'password_strategy'] + fields = fields_verbose + fields_fk + fields_m2m + fields_custom read_only_fields = [ 'date_joined', 'last_login', 'created_by', 'is_first_login', ] - extra_kwargs = { 'password': {'write_only': True, 'required': False, 'allow_null': True, 'allow_blank': True}, 'public_key': {'write_only': True}, diff --git a/apps/users/templates/users/_user.html b/apps/users/templates/users/_user.html deleted file mode 100644 index dbcf7f804..000000000 --- a/apps/users/templates/users/_user.html +++ /dev/null @@ -1,88 +0,0 @@ -{% extends '_base_create_update.html' %} -{% load i18n %} -{% load static %} -{% load bootstrap3 %} -{% block form %} - {% if form.non_field_errors %} -
- {{ form.non_field_errors }} -
- {% endif %} -
- {% csrf_token %} -

{% trans 'Account' %}

- {% bootstrap_field form.name layout="horizontal" %} - {% bootstrap_field form.username layout="horizontal" %} - {% bootstrap_field form.email layout="horizontal" %} - {% bootstrap_field form.groups layout="horizontal" %} - -
- -

{% trans 'Auth' %}

- {% block password %}{% endblock %} - {% bootstrap_field form.mfa_level layout="horizontal" %} - {% bootstrap_field form.source layout="horizontal" %} - -
-

{% trans 'Security and Role' %}

- {% bootstrap_field form.role layout="horizontal" %} -
- -
-
- - {% if form.errors %} - - {% else %} - - {% endif %} -
- {{ form.date_expired.errors }} -
-
-
-

{% trans 'Profile' %}

- {% bootstrap_field form.phone layout="horizontal" %} - {% bootstrap_field form.wechat layout="horizontal" %} - {% bootstrap_field form.comment layout="horizontal" %} -
-
-
- - -
-
-
- -{% endblock %} -{% block custom_foot_js %} - - - - - - - -{% endblock %} diff --git a/apps/users/templates/users/user_bulk_update.html b/apps/users/templates/users/user_bulk_update.html deleted file mode 100644 index 28b0e8cb7..000000000 --- a/apps/users/templates/users/user_bulk_update.html +++ /dev/null @@ -1,70 +0,0 @@ -{% extends '_base_create_update.html' %} -{% load static %} -{% load bootstrap3 %} -{% load i18n %} - -{% block form %} -
-

{% trans 'Select properties that need to be modified' %}

-
- {% trans 'Select all' %} - {% for field in form %} - {% if field.name != 'users' %} - {{ field.label }} - {% endif %} - {% endfor %} -
-
-
- {% csrf_token %} - {% bootstrap_form form layout="horizontal" %} -
-
- - -
-
-
-{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_create.html b/apps/users/templates/users/user_create.html deleted file mode 100644 index 5e15b5469..000000000 --- a/apps/users/templates/users/user_create.html +++ /dev/null @@ -1,100 +0,0 @@ -{% extends 'users/_user.html' %} -{% load i18n %} -{% load bootstrap3 %} -{% block user_template_title %}{% trans "Create user" %}{% endblock %} -{% block password %} - {% bootstrap_field form.password_strategy layout="horizontal" %} - {% bootstrap_field form.password layout="horizontal" %} - {# 密码popover #} -
- -
- -{% endblock %} - diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html deleted file mode 100644 index 0b624b78a..000000000 --- a/apps/users/templates/users/user_detail.html +++ /dev/null @@ -1,550 +0,0 @@ -{% extends 'users/_base_user_detail.html' %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} - -{% block content_nav_delete_update %} -
  • - {% trans 'Update' %} -
  • -
  • - - {% trans 'Delete' %} - -
  • -{% endblock %} - -{% block content_table %} -
    -
    -
    - {{ object.name }} -
    - - - - - - - - - - -
    -
    -
    - - - - - - - - - - - - - - - - - - {% if user.phone %} - - - - - {% endif %} - {% if object.wechat %} - - - - - {% endif %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if object.can_update_password %} - - - - - {% endif %} - - - - - -
    - -
    {% trans 'Name' %}:{{ object.name }}
    {% trans 'Username' %}:{{ object.username }}
    {% trans 'Email' %}:{{ object.email }}
    {% trans 'Phone' %}:{{ object.phone }}
    {% trans 'Wechat' %}:{{ object.wechat }}
    {% trans 'Role' %}:{{ object.org_role_display }}
    {% trans 'MFA' %}: - {% if object.mfa_force_enabled %} - {% trans 'Force enabled' %} - {% elif object.mfa_enabled%} - {% trans 'Enabled' %} - {% else %} - {% trans 'Disabled' %} - {% endif %} -
    {% trans 'Source' %}:{{ object.get_source_display }}
    {% trans 'Date expired' %}:{{ object.date_expired|date:"Y-m-j H:i:s" }}
    {% trans 'Created by' %}:{{ object.created_by }}
    {% trans 'Date joined' %}:{{ object.date_joined|date:"Y-m-j H:i:s" }}
    {% trans 'Last login' %}:{{ object.last_login|date:"Y-m-j H:i:s" }}
    {% trans 'Last password updated' %}:{{ object.date_password_last_updated|date:"Y-m-j H:i:s" }}
    {% trans 'Comment' %}:{{ object.comment }}
    -
    -
    -
    -
    -
    -
    - {% trans 'Quick modify' %} -
    -
    - - - - - - - - - - - - - - - {% if object.can_update_password %} - - - - - {% endif %} - {% if object.can_update_ssh_key %} - - - - - {% endif %} - - - - - -
    {% trans 'Active' %}: - -
    -
    - - -
    -
    -
    -
    {% trans 'Force enabled MFA' %}: - -
    -
    - - -
    -
    -
    -
    {% trans 'Reset MFA' %}: - - - -
    {% trans 'Send reset password mail' %}: - - - -
    {% trans 'Send reset ssh key mail' %}: - - - -
    {% trans 'Unblock user' %} - - - -
    -
    -
    - {% if request.user.can_admin_current_org %} - - {% if object.can_user_current_org or object.can_admin_current_org %} -
    -
    - {% trans 'User group' %} -
    -
    - - - - - - - - - - - - {% for group in object.groups.all %} - - - - - {% endfor %} - -
    - -
    - -
    - {{ group.name }} - - -
    -
    -
    - {% endif %} - - {% if LICENSE_VALID and LOGIN_CONFIRM_ENABLE %} -
    -
    - {% trans 'Login confirm' %} -
    -
    - - - - - - - - - - - {% if object.get_login_confirm_setting %} - {% for u in object.login_confirm_setting.reviewers.all %} - - - - - {% endfor %} - {% endif %} - -
    - -
    - -
    - {{ u }} - - -
    -
    -
    - {% endif %} - - {% endif %} -
    - -{% include 'users/_user_update_pk_modal.html' %} - -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_granted_asset.html b/apps/users/templates/users/user_granted_asset.html deleted file mode 100644 index 523697153..000000000 --- a/apps/users/templates/users/user_granted_asset.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'users/_base_user_detail.html' %} -{% load bootstrap3 %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} - -{% block content_table %} -{% include 'users/_granted_assets.html' %} -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_granted_database_app.html b/apps/users/templates/users/user_granted_database_app.html deleted file mode 100644 index 7133a9217..000000000 --- a/apps/users/templates/users/user_granted_database_app.html +++ /dev/null @@ -1,100 +0,0 @@ -{% extends 'users/_base_user_detail.html' %} -{% load i18n static %} - -{% block custom_head_css_js %} - -{% endblock %} - -{% block content_table %} -
    -
    -
    - {{ object.name }} -
    - - - - - - - - - - -
    -
    -
    - - - - - - - - - - - - - -
    - - {% trans 'Name' %}{% trans 'Type' %}{% trans 'Host' %}{% trans 'Database' %}{% trans 'Comment' %}
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_granted_remote_app.html b/apps/users/templates/users/user_granted_remote_app.html deleted file mode 100644 index 19b8ab22c..000000000 --- a/apps/users/templates/users/user_granted_remote_app.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends 'users/_base_user_detail.html' %} -{% load i18n static %} - -{% block custom_head_css_js %} - -{% endblock %} - -{% block content_table %} -
    -
    -
    - {{ object.name }} -
    - - - - - - - - - - -
    -
    -
    - - - - - - - - - - - - -
    - - {% trans 'Name' %}{% trans 'App type' %}{% trans 'Asset' %}{% trans 'Comment' %}
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_group_create_update.html b/apps/users/templates/users/user_group_create_update.html deleted file mode 100644 index 09030eca8..000000000 --- a/apps/users/templates/users/user_group_create_update.html +++ /dev/null @@ -1,69 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} -{% load bootstrap3 %} - -{% block content %} -
    -
    -
    -
    -
    -
    {{ action }}
    -
    -
    - {% if form.non_field_errors %} -
    - {{ form.non_field_errors }} -
    - {% endif %} -
    - {% csrf_token %} - {% bootstrap_field form.name layout="horizontal" %} - {% bootstrap_field form.users layout="horizontal" %} - {% bootstrap_field form.comment layout="horizontal" %} -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    - {% include "users/_select_user_modal.html" %} -{% endblock %} -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_group_detail.html b/apps/users/templates/users/user_group_detail.html deleted file mode 100644 index cd65bb746..000000000 --- a/apps/users/templates/users/user_group_detail.html +++ /dev/null @@ -1,168 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} -{% block content %} -
    -
    -
    -
    - -
    -
    -
    -
    - {{ user_group.name }} - -
    -
    - - - - - - - - - - - - - - - - - - - -
    {% trans 'Name' %}:{{ user_group.name }}
    {% trans 'Create by' %}:{{ user_group.created_by }}
    {% trans 'Date created' %}:{{ user_group.date_created }}
    {% trans 'Comment' %}:{{ user_group.comment }}
    -
    -
    -
    -
    -
    -
    - {% trans 'User' %} -
    -
    - - - - - - - - - - - - {% for user in user_group.users.all %} - - - - - {% endfor %} - -
    - -
    - -
    {{ user.name }} - -
    -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_group_granted_asset.html b/apps/users/templates/users/user_group_granted_asset.html deleted file mode 100644 index 2180e6313..000000000 --- a/apps/users/templates/users/user_group_granted_asset.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends 'base.html' %} -{% load bootstrap3 %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} -{% block content %} -
    -
    -
    -
    - -
    - {% include 'users/_granted_assets.html' %} -
    -
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_group_list.html b/apps/users/templates/users/user_group_list.html deleted file mode 100644 index 5e354d3db..000000000 --- a/apps/users/templates/users/user_group_list.html +++ /dev/null @@ -1,112 +0,0 @@ -{% extends '_base_list.html' %} -{% load i18n static %} -{% block table_search %} - {% include '_csv_import_export.html' %} -{% endblock %} -{% block table_container %} - - - - - - - - - - - -
    - - {% trans 'Name' %}{% trans 'User' %}{% trans 'Comment' %}{% trans 'Action' %}
    -{% endblock %} - -{% block content_bottom_left %}{% endblock %} -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html deleted file mode 100644 index 28b30f73d..000000000 --- a/apps/users/templates/users/user_list.html +++ /dev/null @@ -1,280 +0,0 @@ -{% extends '_base_list.html' %} -{% load i18n static %} -{% block table_search %} - {% include '_csv_import_export.html' %} -{% endblock %} -{% block table_container %} - - - - - - - - - - - - - - - - -
    - - {% trans 'Name' %}{% trans 'Username' %}{% trans 'Role' %}{% trans 'User group' %}{% trans 'Source' %}{% trans 'Validity' %}{% trans 'Action' %}
    -
    -
    - -
    - -
    -
    -
    -{% endblock %} -{% block content_bottom_left %}{% endblock %} -{% block custom_foot_js %} - - -{% endblock %} - diff --git a/apps/users/templates/users/user_profile.html b/apps/users/templates/users/user_profile.html deleted file mode 100644 index d1f77c7d0..000000000 --- a/apps/users/templates/users/user_profile.html +++ /dev/null @@ -1,233 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} -{% block content %} -
    -
    -
    -
    - -
    -
    -
    -
    - {{ user.name }} - -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - {% if user.can_update_ssh_key %} - - - - - {% endif %} - - - - - - - - - - - - - - - - - {% if user.can_update_password %} - - - - - {% endif %} - - - - - - - - - - - - -
    - -
    {% trans 'Username' %}{{ user.username }}
    {% trans 'Name' %}{{ user.name }}
    {% trans 'Role' %}{{ user.get_role_display }}
    {% trans 'Email' %}{{ user.email }}
    {% trans 'Active' %}{{ user.is_active|yesno:"Yes,No,Unkown" }}
    {% trans 'Public key' %} - - - - - - - -
    - {{ user.public_key_obj.comment }} -
    - {{ user.public_key_obj.hash_md5 }} -
    -
    {% trans 'MFA' %} - {% if user.mfa_force_enabled %} - {% trans 'Force enable' %} - {% elif user.mfa_enabled%} - {% trans 'Enable' %} - {% else %} - {% trans 'Disable' %} - {% endif %} - {% if mfa_setting %} - ( {% trans 'Administrator Settings force MFA login' %} ) - {% endif %} -
    {% trans 'Source' %}{{ user.get_source_display }}
    {% trans 'Date joined' %}{{ user.date_joined|date:"Y-m-d H:i:s" }}
    {% trans 'Last login' %}{{ user.last_login|date:"Y-m-d H:i:s" }}
    {% trans 'Last password updated' %}{{ user.date_password_last_updated|date:"Y-m-d H:i:s" }}
    {% trans 'Date expired' %}{{ user.date_expired|date:"Y-m-d H:i:s" }}
    {% trans 'User groups' %} - - {% for group in user.groups.all %} - - - - {% endfor %} -
    - {{ group.name }} -
    -
    {% trans 'Comment' %}:{{ user.comment }}
    -
    -
    -
    -
    -
    -
    -
    -
    - {% trans 'Quick modify' %} -
    -
    - - - - - - - {% if request.user.mfa_enabled %} - - - - - {% endif %} - {% if request.user.can_update_password %} - - - - - {% endif %} - {% if request.user.can_update_ssh_key %} - - - - - - - - - {% endif %} - -
    {% trans 'Set MFA' %}: - - {% trans 'Disable' %} - {% else %} - {% url 'authentication:user-otp-disable-authentication' %} - ">{% trans 'Disable' %} - {% endif %} - {% else %} - {% url 'authentication:user-otp-enable-start' %} - ">{% trans 'Enable' %} - {% endif %} - - -
    {% trans 'Update MFA' %}: - - {% trans 'Update' %} - -
    {% trans 'Update password' %}: - - {% trans 'Update' %} - -
    {% trans 'Update SSH public key' %}: - - {% trans 'Update' %} - -
    {% trans 'Reset public key and download' %}: - - {% trans 'Reset' %} - -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_profile_update.html b/apps/users/templates/users/user_profile_update.html deleted file mode 100644 index e59edeccc..000000000 --- a/apps/users/templates/users/user_profile_update.html +++ /dev/null @@ -1,84 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} -{% load bootstrap3 %} - -{% block custom_head_css_js %} - - - - - -{% endblock %} -{% block content %} -
    -
    -
    -
    -
    - -
    -
    -
    -
    - {% csrf_token %} -

    {% trans 'Account' %}

    - {% bootstrap_field form.username layout="horizontal" %} - {% bootstrap_field form.name layout="horizontal" %} - {% bootstrap_field form.email layout="horizontal" %} - -
    -

    {% trans 'Profile' %}

    - {% bootstrap_field form.phone layout="horizontal" %} - {% bootstrap_field form.wechat layout="horizontal" %} -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - - -{% endblock %} diff --git a/apps/users/templates/users/user_pubkey_update.html b/apps/users/templates/users/user_pubkey_update.html deleted file mode 100644 index 4ab03f01c..000000000 --- a/apps/users/templates/users/user_pubkey_update.html +++ /dev/null @@ -1,102 +0,0 @@ -{% extends 'base.html' %} -{% load static %} -{% load i18n %} -{% load bootstrap3 %} - -{% block custom_head_css_js %} - - - - - -{% endblock %} -{% block content %} -
    -
    -
    -
    -
    - -
    -
    -
    -
    - {% csrf_token %} -

    {% trans 'Old public key' %}

    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -

    {% trans 'Update public key' %}

    - {% bootstrap_field form.public_key layout="horizontal" %} -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - - -{% endblock %} diff --git a/apps/users/templates/users/user_remote_app_permission.html b/apps/users/templates/users/user_remote_app_permission.html deleted file mode 100644 index d1f6aabb7..000000000 --- a/apps/users/templates/users/user_remote_app_permission.html +++ /dev/null @@ -1,168 +0,0 @@ -{% extends 'users/_base_user_detail.html' %} -{% load static %} -{% load i18n %} - -{% block custom_head_css_js %} - - -{% endblock %} - -{% block content_table %} -
    -
    -
    - {{ object.name }} -
    - - - - - - - - - - -
    -
    -
    - - - - - - - - - - - - - - - -
    {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'RemoteApp' %}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/apps/users/templates/users/user_update.html b/apps/users/templates/users/user_update.html deleted file mode 100644 index 182ec88aa..000000000 --- a/apps/users/templates/users/user_update.html +++ /dev/null @@ -1,117 +0,0 @@ -{% extends 'users/_user.html' %} -{% load i18n %} -{% load bootstrap3 %} -{% block user_template_title %}{% trans "Update user" %}{% endblock %} -{% block password %} - {% if object.can_update_password %} - {% bootstrap_field form.password layout="horizontal" %} - {# 密码popover #} -
    - -
    - {% else %} -
    - -
    - {% trans 'User auth from {}, go there change password' %} -
    -
    - {% endif %} - {% if object.can_update_ssh_key %} - {% bootstrap_field form.public_key layout="horizontal" %} - {% else %} -
    - -
    - {% trans 'User auth from {}, ssh key login is not supported' %} -
    -
    - {% endif %} -{% endblock %} - -{% block custom_foot_js %} - {{ block.super }} - -{% endblock %} diff --git a/apps/users/views/profile/reset.py b/apps/users/views/profile/reset.py index 73c34396b..356694020 100644 --- a/apps/users/views/profile/reset.py +++ b/apps/users/views/profile/reset.py @@ -1,18 +1,15 @@ # ~*~ coding: utf-8 ~*~ from __future__ import unicode_literals -from django.shortcuts import render from django.views.generic import RedirectView -from django.core.files.storage import default_storage from django.shortcuts import reverse, redirect from django.utils.translation import ugettext as _ from django.views.generic.base import TemplateView from django.conf import settings from django.urls import reverse_lazy -from formtools.wizard.views import SessionWizardView from django.views.generic import FormView -from common.utils import get_object_or_none +from common.utils import get_object_or_none, FlashMessageUtil from common.permissions import IsValidUser from common.mixins.views import PermissionsMixin from ...models import User @@ -24,9 +21,7 @@ from ... import forms __all__ = [ - 'UserLoginView', 'UserForgotPasswordSendmailSuccessView', - 'UserResetPasswordSuccessView', 'UserResetPasswordSuccessView', - 'UserResetPasswordView', 'UserForgotPasswordView', 'UserFirstLoginView', + 'UserLoginView', 'UserResetPasswordView', 'UserForgotPasswordView', 'UserFirstLoginView', ] @@ -39,6 +34,17 @@ class UserForgotPasswordView(FormView): template_name = 'users/forgot_password.html' form_class = forms.UserForgotPasswordForm + @staticmethod + def get_redirect_message_url(): + message_data = { + 'title': _('Send reset password message'), + 'message': _('Send reset password mail success, ' + 'login your mail box and follow it '), + 'redirect_url': reverse('authentication:login'), + } + url = FlashMessageUtil.gen_message_url(message_data) + return url + def form_valid(self, form): email = form.cleaned_data['email'] user = get_object_or_none(User, email=email) @@ -53,37 +59,9 @@ class UserForgotPasswordView(FormView): ).format(user.get_source_display()) form.add_error('email', error) return self.form_invalid(form) - send_reset_password_mail(user) - return redirect('authentication:forgot-password-sendmail-success') - - -class UserForgotPasswordSendmailSuccessView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get_context_data(self, **kwargs): - context = { - 'title': _('Send reset password message'), - 'messages': _('Send reset password mail success, ' - 'login your mail box and follow it '), - 'redirect_url': reverse('authentication:login'), - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class UserResetPasswordSuccessView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get_context_data(self, **kwargs): - context = { - 'title': _('Reset password success'), - 'messages': _('Reset password success, return to login page'), - 'redirect_url': reverse('authentication:login'), - 'auto_redirect': True, - } - kwargs.update(context) - return super().get_context_data(**kwargs) + url = self.get_redirect_message_url() + return redirect(url) class UserResetPasswordView(FormView): @@ -128,10 +106,29 @@ class UserResetPasswordView(FormView): form.add_error('new_password', error) return self.form_invalid(form) + if user.is_history_password(password): + limit_count = settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT + error = _('* The new password cannot be the last {} passwords').format(limit_count) + form.add_error('new_password', error) + return self.form_invalid(form) + else: + user.save_history_password(password) + user.reset_password(password) User.expired_reset_password_token(token) send_reset_password_success_mail(self.request, user) - return redirect('authentication:reset-password-success') + url = self.get_redirect_url() + return redirect(url) + + @staticmethod + def get_redirect_url(): + message_data = { + 'title': _('Reset password success'), + 'message': _('Reset password success, return to login page'), + 'redirect_url': reverse('authentication:login'), + 'auto_redirect': True, + } + return FlashMessageUtil.gen_message_url(message_data) class UserFirstLoginView(PermissionsMixin, TemplateView): diff --git a/jms b/jms index 24b71f4e4..6bd71849d 100755 --- a/jms +++ b/jms @@ -97,6 +97,14 @@ def check_migrations(): # sys.exit(1) +def expire_caches(): + apps_dir = os.path.join(BASE_DIR, 'apps') + code = subprocess.call("python manage.py expire_caches", shell=True, cwd=apps_dir) + + if code == 1: + return + + def perform_db_migrate(): logging.info("Check database structure change ...") os.chdir(os.path.join(BASE_DIR, 'apps')) @@ -116,6 +124,7 @@ def prepare(): check_database_connection() check_migrations() upgrade_db() + expire_caches() def check_pid(pid):